Http-requests

Overview

This tutorial will go over the creation of a HttpService to interact with a back-end server that returns JSON.

Setup

Before anything, let's include some mandatory stuff into our build. Open up your build file in /Source/YourGameName/YourGameName.Build.cs

PrivateDependencyModuleNames.AddRange(new string[] { "Http", "Json", "JsonUtilities" });

What we're doing here is adding three dependencies; Http, Json, and JsonUtilities.


Let's get started.

Go ahead and create a new C++ file called HttpService with a Parent class of Actor.

Open up your HttpService.h file.


Includes

#include "Runtime/Online/HTTP/Public/Http.h"
#include "Json.h"
#include "JsonUtilities.h"

First we're going to bring in Http, Json, and JsonUtilities.


USTRUCTS()

We also want to create some USTRUCTS() to use for Json Serialization. You should really put these into another file later, since they do not belong inside of the HttpService

USTRUCT()
struct FRequest_Login {
	GENERATED_BODY()
	UPROPERTY() FString email;
	UPROPERTY() FString password;

	FRequest_Login() {}
};

USTRUCT()
struct FResponse_Login {
	GENERATED_BODY()
	UPROPERTY() int id;
	UPROPERTY() FString name;
	UPROPERTY() FString hash;

	FResponse_Login() {}
};

We're going to use the two structs above to Serialize and Deserialize json strings.


.h File

UCLASS(Blueprintable, hideCategories = (Rendering, Replication, Input, Actor, "Actor Tick"))
class <YOUR_GAME_NAME>_API AHttpService : public AActor
{
	GENERATED_BODY()
private:
	FHttpModule* Http;
	FString ApiBaseUrl = "http://localhost:5000/api/";

	FString AuthorizationHeader = TEXT("Authorization");
	void SetAuthorizationHash(FString Hash, TSharedRef<IHttpRequest>& Request);

	TSharedRef<IHttpRequest> RequestWithRoute(FString Subroute);
	void SetRequestHeaders(TSharedRef<IHttpRequest>& Request);

	TSharedRef<IHttpRequest> GetRequest(FString Subroute);
	TSharedRef<IHttpRequest> PostRequest(FString Subroute, FString ContentJsonString);
	void Send(TSharedRef<IHttpRequest>& Request);

	bool ResponseIsValid(FHttpResponsePtr Response, bool bWasSuccessful);

	template <typename StructType>
	void GetJsonStringFromStruct(StructType FilledStruct, FString& StringOutput);
	template <typename StructType>
	void GetStructFromJsonString(FHttpResponsePtr Response, StructType& StructOutput);
public:
	AHttpService();
	virtual void BeginPlay() override;

	void Login(FRequest_Login LoginCredentials);
	void LoginResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful);
};

Let's look at some of the important variables.

Declaration Description
FHttpModule* Http We hold our reference to the Http Module here
FString ApiBaseUrl The base URL of your API.
FString AuthorizationHeader This is the authorization header you use on your back-end. It could be Authorization, X-Requested-With, or a variation of other header keys.


.cpp File

AHttpService::AHttpService(){ PrimaryActorTick.bCanEverTick = false; }
void AHttpService::BeginPlay() { 
	Super::BeginPlay(); 
	Http = &FHttpModule::Get(); 
	
        // You can uncomment this out for testing.
	//FRequest_Login LoginCredentials;
	//LoginCredentials.email = TEXT("asdf@asdf.com");
	//LoginCredentials.password = TEXT("asdfasdf");
	//Login(LoginCredentials);
}




/**************************************************************************************************************************/




TSharedRef<IHttpRequest> AHttpService::RequestWithRoute(FString Subroute) {
	TSharedRef<IHttpRequest> Request = Http->CreateRequest();
	Request->SetURL(ApiBaseUrl + Subroute);
	SetRequestHeaders(Request);
	return Request;
}

void AHttpService::SetRequestHeaders(TSharedRef<IHttpRequest>& Request) {
	Request->SetHeader(TEXT("User-Agent"), TEXT("X-UnrealEngine-Agent"));
	Request->SetHeader(TEXT("Content-Type"), TEXT("application/json"));
	Request->SetHeader(TEXT("Accepts"), TEXT("application/json"));
}

TSharedRef<IHttpRequest> AHttpService::GetRequest(FString Subroute) {
	TSharedRef<IHttpRequest> Request = RequestWithRoute(Subroute);
	Request->SetVerb("GET");
	return Request;
}

TSharedRef<IHttpRequest> AHttpService::PostRequest(FString Subroute, FString ContentJsonString) {
	TSharedRef<IHttpRequest> Request = RequestWithRoute(Subroute);
	Request->SetVerb("POST");
	Request->SetContentAsString(ContentJsonString);
	return Request;
}

void AHttpService::Send(TSharedRef<IHttpRequest>& Request) {
	Request->ProcessRequest();
}

bool AHttpService::ResponseIsValid(FHttpResponsePtr Response, bool bWasSuccessful) {
	if (!bWasSuccessful || !Response.IsValid()) return false;
	if (EHttpResponseCodes::IsOk(Response->GetResponseCode())) return true;
	else {
		UE_LOG(LogTemp, Warning, TEXT("Http Response returned error code: %d"), Response->GetResponseCode());
		return false;
	}
}

void AHttpService::SetAuthorizationHash(FString Hash, TSharedRef<IHttpRequest>& Request) {
	Request->SetHeader(AuthorizationHeader, Hash);
}



/**************************************************************************************************************************/



template <typename StructType>
void AHttpService::GetJsonStringFromStruct(StructType FilledStruct, FString& StringOutput) {
	FJsonObjectConverter::UStructToJsonObjectString(StructType::StaticStruct(), &FilledStruct, StringOutput, 0, 0);
}

template <typename StructType>
void AHttpService::GetStructFromJsonString(FHttpResponsePtr Response, StructType& StructOutput) {
	StructType StructData;
	FString JsonString = Response->GetContentAsString();
	FJsonObjectConverter::JsonObjectStringToUStruct<StructType>(JsonString, &StructOutput, 0, 0);
}



/**************************************************************************************************************************/



void AHttpService::Login(FRequest_Login LoginCredentials) {
	FString ContentJsonString;
	GetJsonStringFromStruct<FRequest_Login>(LoginCredentials, ContentJsonString);

	TSharedRef<IHttpRequest> Request = PostRequest("user/login", ContentJsonString);
	Request->OnProcessRequestComplete().BindUObject(this, &AHttpService::LoginResponse);
	Send(Request);
}

void AHttpService::LoginResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) {
	if (!ResponseIsValid(Response, bWasSuccessful)) return;

	FResponse_Login LoginResponse;
	GetStructFromJsonString<FResponse_Login>(Response, LoginResponse);

	UE_LOG(LogTemp, Warning, TEXT("Id is: %d"), LoginResponse.id);
	UE_LOG(LogTemp, Warning, TEXT("Name is: %s"), *LoginResponse.name);
}


Constructor

In the constructor we're simply turning off Tick since it's not needed.


BeginPlay

In the BeginPlay method we want to bind the Http Module to our Http variable for later use. There's also an example Login() call you can enable for testing.


RequestWithRoute

You should never call this method directly!

This method sets up the base Request we will be using. It takes in a Subroute as a parameter and calls the SetRequestHeaders() method.

The Subroute is relative to your BaseApiUrl, so if you wanted to get something from localhost:5000/api/user/1 you would put "user/1" as the route.


SetRequestHeaders

Here we set up some basic headers such as Content-Type and Accepts. These make sure we're always using Json.


GetRequest and PostRequest

These are the methods you call instead of RequestWithRoute!

These two methods are very similar. They take in a route ( and a json string for POST ), set up a Request, and return it to you.

You might be asking why they aren't just one method. In the name of clean code the less parameters a method has and the less error checking it has to do because of those parameters the better. If these were one method with a FString Verb parameter we would have to add error checking for possible verbs, optional parameters for JsonInputs, and just a mess of Mom's Spaghetti.


Send

This is really just some semantics for cleaner code as well.


ResponseIsValid

Here we are checking a few things.

Checking For Description
bWasSuccessful UE4's Http Response comes back with this parameter. It comes into play when a server flat out refuses the connection. For instance if the server crashed, or is down. It should always be the first thing checked.
Response.IsValid() As far as I can tell this comes back false when the response code is 200 ( OK ) but the response itself is malformed in an unusable way.
EHttpResponseCodes::IsOk(Response->GetResponseCode()) If this is not true then we UE_LOG out the response code that came back from the server ( 401, 404, 500, etc ) and return false.


SetAuthorizationHash

This can be used to set the Authorization header on a Request as needed since APIs are stateless and have no recollection of who the user requesting information is. You can use this to authenticate users for authenticated requests.


Serializers and Deserializers

These are two <template> methods to make life easy when going from struct to json or vice versa.


GetJsonStringFromStruct

This takes a USTRUCT() Like the one we made before, FRequest_Login and turns it into a properly formatted Json string. The variable passed into the StringOutput will be filled with the Json.


GetStructFromJsonString

This takes a Json string and fills the StructOutput with the struct created from the Json.


Example Login

At the end of the .cpp file are example Login() and LoginResponse() methods to show the flow of using this Service.


Login

We're doing a couple things here.

  1. Creating a Json string from a struct
  2. Getting a Post Request Object with the route "user/login"
  3. Setting the method to be executed when the response returns ( or times out / fails )
  4. And finally actually Sending the request.

Request Json Example: { "email":"asdf@qwer.com", "password":"abcdefg1234" }


LoginResponse

Let's go through this one too.

  1. Make sure the response is valid before continuing.
  2. Get a struct from the Json string
  3. UE_LOG some tests to make sure our code is working.

Response Json Example: { "id":1, "name":"BoogaBooga", "hash":"asdf-qwer-zxcv-asdf" }


Making Requests Useful

Just logging out information to the console isn't very useful in real life situations. Let's modify the Login and LoginResponse methods to paint a better picture of actual usage.

void AHttpService::Login(
                    FRequest_Login LoginCredentials, 
                    ACustomPlayerState* PlayerState) {

	FString ContentJsonString;
	GetJsonStringFromStruct<FRequest_Login>(LoginCredentials, ContentJsonString);

	TSharedRef<IHttpRequest> Request = PostRequest("user/login", ContentJsonString);

        // We'll add the PlayerState to the bound response method so that we can use it later
	Request->OnProcessRequestComplete().BindUObject(this, &AHttpService::LoginResponse, PlayerState);
	Send(Request);
}

void AHttpService::LoginResponse(
                    FHttpRequestPtr Request, 
                    FHttpResponsePtr Response, 
                    bool bWasSuccessful, 
                    ACustomPlayerState* PlayerState) {

	if (!ResponseIsValid(Response, bWasSuccessful)) return;

	FResponse_Login LoginResponse;
	GetStructFromJsonString<FResponse_Login>(Response, LoginResponse);

        // We'll give back the information to the player's state so they can do something with it.
        PlayerState->OnLoginSuccess(LoginResponse);
}

Now we're passing in a PlayerState which can be used to set information on. Since we have that information now, we can even use Authorized methods. For example

void AHttpService::GetInventory(ACustomPlayerState* PlayerState) {
        int id = PlayerState->GetPlayerId();
        FString hash = PlayerState->GetAuthorizationHash();

	TSharedRef<IHttpRequest> Request = GetRequest("user/"+id+"/inventory");
	SetAuthorizationHash(hash, Request);

	Request->OnProcessRequestComplete().BindUObject(this, &AHttpService::GetInventoryResponse, PlayerState);
	Send(Request);
}


Hope you enjoyed this, and remember to keep your code clean

- JTPX

Rate this Tutorials:
5.00
(one vote)

Approved for Versions:4.0, 4.1