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...

Updated over 4 years ago Edit Page Revisions

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.

  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(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