├── HttpService.cpp ├── HttpService.h └── README.md /HttpService.cpp: -------------------------------------------------------------------------------- 1 | // Fill out your copyright notice in the Description page of Project Settings. 2 | 3 | #include ".h" 4 | #include "HttpService.h" 5 | 6 | 7 | 8 | AHttpService::AHttpService(){ PrimaryActorTick.bCanEverTick = false; } 9 | void AHttpService::BeginPlay() { 10 | Super::BeginPlay(); 11 | 12 | // We don't want clients to be able to run HTTP requests. Only servers. 13 | if (!HasAuthority()) return; 14 | Http = &FHttpModule::Get(); 15 | 16 | FRequest_Login LoginCredentials; 17 | LoginCredentials.email = TEXT("asdf@asdf.com"); 18 | LoginCredentials.password = TEXT("asdfasdf"); 19 | Login(LoginCredentials); 20 | } 21 | 22 | 23 | 24 | 25 | /**********************************************************************************************************************************************/ 26 | 27 | 28 | 29 | 30 | TSharedRef AHttpService::RequestWithRoute(FString Subroute) { 31 | TSharedRef Request = Http->CreateRequest(); 32 | Request->SetURL(ApiBaseUrl + Subroute); 33 | SetRequestHeaders(Request); 34 | return Request; 35 | } 36 | 37 | void AHttpService::SetRequestHeaders(TSharedRef& Request) { 38 | Request->SetHeader(TEXT("User-Agent"), TEXT("X-UnrealEngine-Agent")); 39 | Request->SetHeader(TEXT("Content-Type"), TEXT("application/json")); 40 | Request->SetHeader(TEXT("Accepts"), TEXT("application/json")); 41 | Request->SetHeader(AuthorizationHeader, AuthorizationHash); 42 | } 43 | 44 | TSharedRef AHttpService::GetRequest(FString Subroute) { 45 | TSharedRef Request = RequestWithRoute(Subroute); 46 | Request->SetVerb("GET"); 47 | return Request; 48 | } 49 | 50 | TSharedRef AHttpService::PostRequest(FString Subroute, FString ContentJsonString) { 51 | TSharedRef Request = RequestWithRoute(Subroute); 52 | Request->SetVerb("POST"); 53 | Request->SetContentAsString(ContentJsonString); 54 | return Request; 55 | } 56 | 57 | void AHttpService::Send(TSharedRef& Request) { 58 | Request->ProcessRequest(); 59 | } 60 | 61 | bool AHttpService::ResponseIsValid(FHttpResponsePtr Response, bool bWasSuccessful) { 62 | if (!bWasSuccessful || !Response.IsValid()) return false; 63 | if (EHttpResponseCodes::IsOk(Response->GetResponseCode())) return true; 64 | else { 65 | UE_LOG(LogTemp, Warning, TEXT("Http Response returned error code: %d"), Response->GetResponseCode()); 66 | return false; 67 | } 68 | } 69 | 70 | void AHttpService::SetAuthorizationHash(FString Hash, TSharedRef& Request) { 71 | AuthorizationHash = Hash; 72 | } 73 | 74 | 75 | 76 | /**********************************************************************************************************************************************/ 77 | 78 | 79 | 80 | template 81 | void AHttpService::GetJsonStringFromStruct(StructType FilledStruct, FString& StringOutput) { 82 | FJsonObjectConverter::UStructToJsonObjectString(StructType::StaticStruct(), &FilledStruct, StringOutput, 0, 0); 83 | } 84 | 85 | template 86 | void AHttpService::GetStructFromJsonString(FHttpResponsePtr Response, StructType& StructOutput) { 87 | StructType StructData; 88 | FString JsonString = Response->GetContentAsString(); 89 | FJsonObjectConverter::JsonObjectStringToUStruct(JsonString, &StructOutput, 0, 0); 90 | } 91 | 92 | 93 | 94 | /**********************************************************************************************************************************************/ 95 | 96 | 97 | 98 | void AHttpService::Login(FRequest_Login LoginCredentials) { 99 | FString ContentJsonString; 100 | GetJsonStringFromStruct(LoginCredentials, ContentJsonString); 101 | 102 | TSharedRef Request = PostRequest("user/login", ContentJsonString); 103 | Request->OnProcessRequestComplete().BindUObject(this, &AHttpService::LoginResponse); 104 | Send(Request); 105 | } 106 | 107 | void AHttpService::LoginResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) { 108 | if (!ResponseIsValid(Response, bWasSuccessful)) return; 109 | 110 | FResponse_Login LoginResponse; 111 | GetStructFromJsonString(Response, LoginResponse); 112 | 113 | SetAuthorizationHash(LoginResponse.hash); 114 | 115 | UE_LOG(LogTemp, Warning, TEXT("Id is: %d"), LoginResponse.id); 116 | UE_LOG(LogTemp, Warning, TEXT("Name is: %s"), *LoginResponse.name); 117 | } 118 | 119 | -------------------------------------------------------------------------------- /HttpService.h: -------------------------------------------------------------------------------- 1 | // Fill out your copyright notice in the Description page of Project Settings. 2 | 3 | #pragma once 4 | 5 | #include "GameFramework/Actor.h" 6 | #include "Runtime/Online/HTTP/Public/Http.h" 7 | #include "Json.h" 8 | #include "JsonUtilities.h" 9 | #include "HttpService.generated.h" 10 | 11 | USTRUCT() 12 | struct FRequest_Login { 13 | GENERATED_BODY() 14 | UPROPERTY() FString email; 15 | UPROPERTY() FString password; 16 | 17 | FRequest_Login() {} 18 | }; 19 | 20 | USTRUCT() 21 | struct FResponse_Login { 22 | GENERATED_BODY() 23 | UPROPERTY() int id; 24 | UPROPERTY() FString name; 25 | UPROPERTY() FString hash; 26 | 27 | FResponse_Login() {} 28 | }; 29 | 30 | UCLASS(Blueprintable, hideCategories = (Rendering, Replication, Input, Actor, "Actor Tick")) 31 | class TESTER2_API AHttpService : public AActor 32 | { 33 | GENERATED_BODY() 34 | private: 35 | FHttpModule* Http; 36 | FString ApiBaseUrl = "http://murk.dev/api/"; 37 | 38 | FString AuthorizationHeader = TEXT("Authorization"); 39 | FString AuthorizationHash = TEXT("asdfasdf"); 40 | void SetAuthorizationHash(FString Hash); 41 | 42 | TSharedRef RequestWithRoute(FString Subroute); 43 | void SetRequestHeaders(TSharedRef& Request); 44 | 45 | TSharedRef GetRequest(FString Subroute); 46 | TSharedRef PostRequest(FString Subroute, FString ContentJsonString); 47 | void Send(TSharedRef& Request); 48 | 49 | bool ResponseIsValid(FHttpResponsePtr Response, bool bWasSuccessful); 50 | 51 | template 52 | void GetJsonStringFromStruct(StructType FilledStruct, FString& StringOutput); 53 | template 54 | void GetStructFromJsonString(FHttpResponsePtr Response, StructType& StructOutput); 55 | public: 56 | AHttpService(); 57 | virtual void BeginPlay() override; 58 | 59 | void Login(FRequest_Login LoginCredentials); 60 | void LoginResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful); 61 | 62 | 63 | }; 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UE4 Tutorial - Http 2 | --- 3 | Http requests in UE4 are fairly straight forward, however here are a few pitfalls and suggestions: 4 | - **UE4 only accepts GET/PUT/POST requests.** This means you can not send DELETE/PATCH requests. 5 | - You can write your own serializers/deserializers, but I highly recommend the use of **`USTRUCT()`** and **`FJsonObjectConverter`** 6 | - I find it easiest to use the service as an Actor child. You should spawn it into the level once and reference it. 7 | 8 | 9 | 10 | 11 | #### Setup 12 | --- 13 | **Before you start** make sure you have included the required dependencies. 14 | 15 | ``` 16 | # Path: Source/YourProject/YourProject.Build.cs 17 | PrivateDependencyModuleNames.AddRange(new string[] { "Http", "Json", "JsonUtilities" }); 18 | ``` 19 | - **`Http`** is our trusty ue4 http implementation. 20 | - **`Json`** is the json conversion library 21 | - **`JsonUtilities`** has the `FJsonObjectConverter` we will be using to convert Json data to Struct data 22 | 23 | # 24 | # 25 | # 26 | # 27 | # 28 | --- 29 | > **At this point you could probably just grab the files and go over them, but here's a detailed explanation as well as some reasoning behind design decisions and a friendly reminder to keep code clean.** 30 | --- 31 | 32 | # 33 | # 34 | # 35 | # 36 | # 37 | # Table Of Contents 38 | - [**USTRUCTS()**](#ustructs) 39 | - [**Some Important Variables**](#some-important-variables) 40 | - [**Internal Methods**](#internal-methods) 41 | - [**Sending a Request**](#sending-a-request) 42 | - [**Checking for valid Responses**](#checking-for-valid-responses) 43 | - [**Serialization and Deserialization**](#serialization-and-deserialization) 44 | - [**Real World Example**](#okay-lets-look-at-some-real-world-http-requests) 45 | - [**Passing Back Data**](#passing-back-data) 46 | 47 | # 48 | # 49 | # 50 | # 51 | # 52 | 53 | --- 54 | ### USTRUCTS() 55 | `FRequest_Login` and `FResponse_Login` are both used to pass data back and forth between `UE4` and your `Back-End Server`. 56 | I wont be touching on back-end servers but I will be showing the `JSON` that will be sent and returned. 57 | 58 | # 59 | # 60 | 61 | **`FRequest_Login`** holds the `email` and `password` that we are using to log into our account. 62 | ``` 63 | USTRUCT() 64 | struct FRequest_Login { 65 | GENERATED_BODY() 66 | UPROPERTY() FString email; 67 | UPROPERTY() FString password; 68 | 69 | FRequest_Login() {} 70 | }; 71 | ``` 72 | **`JSON EXAMPLE: { "email":"some@email.com", "password":"strongpassword" }`** 73 | 74 | # 75 | # 76 | 77 | **`FResponse_Login`** holds the returned response from the login request. 78 | ``` 79 | USTRUCT() 80 | struct FResponse_Login { 81 | GENERATED_BODY() 82 | UPROPERTY() int id; 83 | UPROPERTY() FString name; 84 | UPROPERTY() FString hash; 85 | 86 | FResponse_Login() {} 87 | }; 88 | ``` 89 | **`JSON EXAMPLE: { "id":1, "name":"Batman", "hash":"asdf-qwer-dfgh-erty" }`** 90 | 91 | # 92 | # 93 | 94 | > **Note:** *You should provide a 'hash' property on every player login success. It will be used to verify their account on every subsequent request; since APIs dont hold state.* 95 | 96 | # 97 | # 98 | # 99 | # 100 | # 101 | --- 102 | ### Some important variables: 103 | - `FHttpModule* Http;` - Holds a reference to UE4's Http implementation. It's used to create request objects. 104 | - `FString ApiBaseUrl` - **You should replace this with your actual API url.** 105 | - `FString AuthorizationHeader` - This is the `key` for the Authentication header. Your back-end might expect a different form of this such as `X-Auth-Token`, `X-Requested-With` or something similar. 106 | 107 | # 108 | # 109 | # 110 | # 111 | # 112 | --- 113 | ### Internal Methods: 114 | These are just some methods that you can use to build eloquently written api calls. 115 | 116 | # 117 | # 118 | 119 | ``` 120 | TSharedRef RequestWithRoute(FString Subroute); 121 | void SetRequestHeaders(TSharedRef& Request); 122 | ``` 123 | Both `RequestWithRoute` and `SetRequestHeaders` are used to initialize `Http Request Objects`. 124 | **They shouldn't be called directly, only through the methods below.** 125 | 126 | # 127 | # 128 | 129 | ``` 130 | TSharedRef GetRequest(FString Subroute); 131 | TSharedRef PostRequest(FString Subroute, FString ContentJsonString); 132 | ``` 133 | `GetRequest` and `PostRequest` are the proper methods to call. 134 | *I intentionally left out `PutRequest` so that you may implement it using the same structure.* 135 | 136 | > *You might be asking **Why not just have one method that accepts a Verb?** - The simple answer is that inserting a string into a parameter is not only sloppy, but will also add error checking and useless complexity to a very simple method.* 137 | 138 | 139 | Let's take a look at the implementations of **`PostRequest`**, **`RequestWithRoute`** and **`SetRequestHeaders`** 140 | ``` 141 | TSharedRef AHttpService::PostRequest(FString Subroute, FString ContentJsonString) { 142 | TSharedRef Request = RequestWithRoute(Subroute); 143 | Request->SetVerb("POST"); 144 | Request->SetContentAsString(ContentJsonString); 145 | return Request; 146 | } 147 | 148 | TSharedRef AHttpService::RequestWithRoute(FString Subroute) { 149 | TSharedRef Request = Http->CreateRequest(); 150 | Request->SetURL(ApiBaseUrl + Subroute); 151 | SetRequestHeaders(Request); 152 | return Request; 153 | } 154 | 155 | void AHttpService::SetRequestHeaders(TSharedRef& Request) { 156 | Request->SetHeader(TEXT("User-Agent"), TEXT("X-UnrealEngine-Agent")); 157 | Request->SetHeader(TEXT("Content-Type"), TEXT("application/json")); 158 | Request->SetHeader(TEXT("Accepts"), TEXT("application/json")); 159 | Request->SetHeader(AuthorizationHeader, AuthorizationHash); 160 | } 161 | ``` 162 | As you can see, **`PostRequest`** is very simple and uses **`RequestWithRoute`** to build itself, keeping everything nice and clean. 163 | The flow for **`PostRequest`** goes as follows: 164 | - Get `Request Object` with a `subroute` and set it's `Headers` 165 | - Set the Verb to **`POST`** 166 | - Set the `RequestObjects`'s Content to a `Json formatted string` 167 | - Return the `RequestObject` 168 | 169 | # 170 | # 171 | # 172 | # 173 | # 174 | --- 175 | 176 | ### Sending a Request: 177 | ``` 178 | void Send(TSharedRef& Request); 179 | ``` 180 | This is really just a semantically named method for cleanliness. As you can see from the code below it really doesn't do much besides clean up the naming conventions and make the code more readable. 181 | ``` 182 | void AHttpService::Send(TSharedRef& Request) { 183 | Request->ProcessRequest(); 184 | } 185 | ``` 186 | # 187 | # 188 | # 189 | # 190 | # 191 | --- 192 | ### Checking for valid Responses: 193 | **`ResponseIsValid`** is used to deeply check if a response is valid. 194 | - `!bWasSuccessful` is returned from the Http request made by UE4. It's the first check because if it fails no further information will be given. 195 | - `!Response.IsValid()` is also returned from the UE4 request, and means that most likely the request succeeded, but the response can't be parsed. 196 | - If the `ResponseCode` is not **`Ok` ( 200 )** then we will also return false, as well as log out the code returned. 197 | ``` 198 | bool AHttpService::ResponseIsValid(FHttpResponsePtr Response, bool bWasSuccessful) { 199 | if (!bWasSuccessful || !Response.IsValid()) return false; 200 | if (EHttpResponseCodes::IsOk(Response->GetResponseCode())) return true; 201 | else { 202 | UE_LOG(LogTemp, Warning, TEXT("Http Response returned error code: %d"), Response->GetResponseCode()); 203 | return false; 204 | } 205 | } 206 | ``` 207 | # 208 | # 209 | # 210 | # 211 | # 212 | --- 213 | ### Serialization and Deserialization 214 | We're going to be using **`FJsonObjectConverter`** to convert json to scructs and structs to json. 215 | Like I said above I suggest you use `FJsonObjectConverter`. 216 | Here are some reasons why: 217 | - Support for TArray, even TArray 218 | - Support for Enum from string/int ( for example a json of { "itemType":"Weapon" } will become `EItemType itemType = EItemType::Weapon`; 219 | - Direct conversion into a USTRUCT() which is garbage collected. 220 | 221 | # 222 | # 223 | 224 | Let's look at the two methods that handle serialization and deserialization. 225 | ##### Get Json String From Struct: 226 | # 227 | ``` 228 | template 229 | void AHttpService::GetJsonStringFromStruct(StructType FilledStruct, FString& StringOutput) { 230 | FJsonObjectConverter::UStructToJsonObjectString(StructType::StaticStruct(), &FilledStruct, StringOutput, 0, 0); 231 | } 232 | ``` 233 | This method gets a Json Formatted String from a struct of type and binds it to the `StringOutput`. 234 | We see this in action in the [`Login()`](#for-the-login-request) method. 235 | 236 | ##### Get Struct From Json String: 237 | # 238 | ``` 239 | template 240 | void AHttpService::GetStructFromJsonString(FHttpResponsePtr Response, StructType& StructOutput) { 241 | StructType StructData; 242 | FString JsonString = Response->GetContentAsString(); 243 | FJsonObjectConverter::JsonObjectStringToUStruct(JsonString, &StructOutput, 0, 0); 244 | } 245 | ``` 246 | This method does the exact opposite of `GetJsonStringFromStruct()` and binds a Struct using the Json Formatted String to the `StructOutput`. 247 | We see this in action in the [`LoginResponse()`](#for-the-login-response) method. 248 | 249 | # 250 | # 251 | # 252 | # 253 | # 254 | --- 255 | ### Okay! Let's look at some real world Http Requests! 256 | I've included a simple login example in the file as well just to illustrate how this all comes together nicely and neatly. 257 | 258 | # 259 | # 260 | 261 | 262 | ### For the Login (Request) 263 | ``` 264 | void AHttpService::Login(FRequest_Login LoginCredentials) { 265 | FString ContentJsonString; 266 | GetJsonStringFromStruct(LoginCredentials, ContentJsonString); 267 | 268 | TSharedRef Request = PostRequest("user/login", ContentJsonString); 269 | Request->OnProcessRequestComplete().BindUObject(this, &AHttpService::LoginResponse); 270 | Send(Request); 271 | } 272 | ``` 273 | - `FString ContentJsonString` holds the returned json from the **`GetJsonStringFromStruct`** method. 274 | - `TSharedRef Request` holds the RequestObject that we get from the **`PostRequest`** method. Note that we are passing in a subroute relative to the `ApiBaseUrl` we put into the **`.h`** file. 275 | - **`Request->OnProcessRequestComplete().BindUObject(this, &AHttpService::LoginResponse);`** is highly important here. We use this to bind the Resquest's response to a method. 276 | - **`Send(Request);`** does just what it says. Hence the nicely semantic naming convention. 277 | 278 | ### Here's an example of the method being called: 279 | ``` 280 | CALLED FROM BeginPlay(): 281 | FRequest_Login LoginCredentials; 282 | LoginCredentials.email = TEXT("asdf@asdf.com"); 283 | LoginCredentials.password = TEXT("asdfasdf"); 284 | Login(LoginCredentials); 285 | ``` 286 | 287 | # 288 | # 289 | 290 | ### For the Login (Response) 291 | ``` 292 | void AHttpService::LoginResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) { 293 | if (!ResponseIsValid(Response, bWasSuccessful)) return; 294 | 295 | FResponse_Login LoginResponse; 296 | GetStructFromJsonString(Response, LoginResponse); 297 | 298 | SetAuthorizationHash(LoginResponse.hash); 299 | 300 | UE_LOG(LogTemp, Warning, TEXT("Id is: %d"), LoginResponse.id); 301 | UE_LOG(LogTemp, Warning, TEXT("Name is: %s"), *LoginResponse.name); 302 | } 303 | ``` 304 | Let's take a moment to look at the **Mandatory** parameters. 305 | - `FHttpRequestPtr Request` 306 | - `FHttpResponsePtr Response` 307 | - `bool bWasSuccessful` - If the response was successful at all. ( Will fail for instance if the server is down. ) 308 | 309 | You don't have to worry about passing these in, they are passed automatically by UE4. 310 | 311 | ``` 312 | if (!ResponseIsValid(Response, bWasSuccessful)) return; 313 | ``` 314 | We don't want to continue if the response is bad, so it's good practice to run this method. 315 | 316 | ``` 317 | FResponse_Login LoginResponse; 318 | GetStructFromJsonString(Response, LoginResponse); 319 | ``` 320 | Here we're binding the Json Response into our custom `FResponse_Login` Struct. 321 | > **Note** 322 | > A struct will simply not fill itself out if there are properties missing. Keep that in mind. 323 | 324 | # 325 | ``` 326 | SetAuthorizationHash(LoginResponse.hash); 327 | ``` 328 | We can set the hash for further requests here. Really though, we should be passing it back to the specific **Player** and bind it on each request, otherwise **every player will be the same user**. 329 | 330 | 331 | 332 | # 333 | # 334 | # 335 | # 336 | # 337 | 338 | 339 | --- 340 | ### Passing Back Data 341 | Here's how to pass back some data after the HTTP Request has succeeded. 342 | Let's take our `Login` and `LoginResponse` and revamp them a bit. 343 | 344 | # 345 | # 346 | ##### header file 347 | # 348 | 349 | ``` 350 | void Login(FRequest_Login LoginCredentials, ACustomPlayerState* PlayerState); 351 | void LoginResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful, ACustomPlayerState* PlayerState); 352 | ``` 353 | # 354 | # 355 | ##### cpp file 356 | # 357 | 358 | ``` 359 | void AHttpService::Login(FRequest_Login LoginCredentials, ACustomPlayerState* PlayerState) { 360 | FString ContentJsonString; 361 | GetJsonStringFromStruct(LoginCredentials, ContentJsonString); 362 | 363 | TSharedRef Request = PostRequest("user/login", ContentJsonString); 364 | Request->OnProcessRequestComplete().BindUObject(this, &AHttpService::LoginResponse, PlayerState); 365 | Send(Request); 366 | } 367 | 368 | void AHttpService::LoginResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful, ACustomPlayerState* PlayerState) { 369 | if (!ResponseIsValid(Response, bWasSuccessful)) return; 370 | 371 | FResponse_Login LoginResponse; 372 | GetStructFromJsonString(Response, LoginResponse); 373 | PlayerState->PlayerLoginSuccessful(LoginResponse); 374 | } 375 | ``` 376 | # 377 | Notice the `PlayerState` inside of the `Request->OnProcessRequestComplete().BindUObject(this, &AHttpService::LoginResponse, PlayerState);` now. 378 | 379 | We're passing a reference so the player state into the delegate to be used when the request finishes. 380 | 381 | **For the sake of clean code** you should not be doing any non-http logic here. 382 | Pass your response data somewhere else and handle it there. APIs tend to be quite large and if you put all of your logic inside of your HttpService it will be too large to handle in the future. 383 | 384 | 385 | 386 | 387 | --------------------------------------------------------------------------------