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. Ope...
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 _API AHttpService : public AActor
{
GENERATED_BODY()
private:
FHttpModule* Http;
FString ApiBaseUrl = "http://localhost:5000/api/";
FString AuthorizationHeader = TEXT("Authorization");
void SetAuthorizationHash(FString Hash, TSharedRef& Request);
TSharedRef RequestWithRoute(FString Subroute);
void SetRequestHeaders(TSharedRef& Request);
TSharedRef GetRequest(FString Subroute);
TSharedRef PostRequest(FString Subroute, FString ContentJsonString);
void Send(TSharedRef& Request);
bool ResponseIsValid(FHttpResponsePtr Response, bool bWasSuccessful);
template
void GetJsonStringFromStruct(StructType FilledStruct, FString& StringOutput);
template
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 AHttpService::RequestWithRoute(FString Subroute) {
TSharedRef Request = Http->CreateRequest();
Request->SetURL(ApiBaseUrl + Subroute);
SetRequestHeaders(Request);
return Request;
}
void AHttpService::SetRequestHeaders(TSharedRef& 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 AHttpService::GetRequest(FString Subroute) {
TSharedRef Request = RequestWithRoute(Subroute);
Request->SetVerb("GET");
return Request;
}
TSharedRef AHttpService::PostRequest(FString Subroute, FString ContentJsonString) {
TSharedRef Request = RequestWithRoute(Subroute);
Request->SetVerb("POST");
Request->SetContentAsString(ContentJsonString);
return Request;
}
void AHttpService::Send(TSharedRef& 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& Request) {
Request->SetHeader(AuthorizationHeader, Hash);
}
/**************************************************************************************************************************/
template
void AHttpService::GetJsonStringFromStruct(StructType FilledStruct, FString& StringOutput) {
FJsonObjectConverter::UStructToJsonObjectString(StructType::StaticStruct(), &FilledStruct, StringOutput, 0, 0);
}
template
void AHttpService::GetStructFromJsonString(FHttpResponsePtr Response, StructType& StructOutput) {
StructType StructData;
FString JsonString = Response->GetContentAsString();
FJsonObjectConverter::JsonObjectStringToUStruct(JsonString, &StructOutput, 0, 0);
}
/**************************************************************************************************************************/
void AHttpService::Login(FRequest_Login LoginCredentials) {
FString ContentJsonString;
GetJsonStringFromStruct(LoginCredentials, ContentJsonString);
TSharedRef 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(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 ** 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.
- Creating a Json string from a struct
- Getting a Post Request Object with the route "user/login"
- Setting the method to be executed when the response returns ( or times out / fails )
- And finally actually Sending the request.
Request Json Example: { "email":"asdf@qwer.com", "password":"abcdefg1234" }
LoginResponse
Let's go through this one too.
- Make sure the response is valid before continuing.
- Get a struct from the Json string
- 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(LoginCredentials, ContentJsonString);
TSharedRef 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(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 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