// Copyright Epic Games, Inc. All Rights Reserved. #include "CoreMinimal.h" #include "HAL/FileManager.h" #include "HAL/IConsoleManager.h" #include "HAL/PlatformProcess.h" #include "HAL/PlatformTime.h" #include "HAL/Runnable.h" #include "HAL/RunnableThread.h" #include "HttpConstants.h" #include "HttpManager.h" #include "HttpRetrySystem.h" #include "Http.h" #include "HttpPath.h" #include "HttpRouteHandle.h" #include "IHttpRouter.h" #include "HttpServerModule.h" #include "Logging/LogScopedVerbosityOverride.h" #include "Misc/CommandLine.h" #include "TestHarness.h" #include "TestMacros/Assertions.h" #include "Serialization/JsonSerializerMacros.h" #include "catch2/generators/catch_generators.hpp" /** * HTTP Tests * ----------------------------------------------------------------------------------------------- * * PURPOSE: * * Integration Tests to make sure all kinds of HTTP client features in C++ work well on different platforms, * including but not limited to error handing, retrying, threading, streaming, SSL and profiling. * * Refer to WebTests/README.md for more info about how to run these tests * * ----------------------------------------------------------------------------------------------- */ #define HTTP_TAG "[HTTP]" #define HTTP_TIME_DIFF_TOLERANCE_OF_REQUEST 0.5f #define HTTP_TEST_TIMEOUT_CHUNK_SIZE 16*1024 // Use a big chunk size so it triggers data received callback in time on all platforms extern TAutoConsoleVariable CVarHttpInsecureProtocolEnabled; extern TAutoConsoleVariable CVarHttpMaxConcurrentRequests; extern TAutoConsoleVariable CVarHttpEventLoopEnableChance; extern TAutoConsoleVariable CVarHttpUrlPatternsToMockFailure; extern TAutoConsoleVariable CVarHttpUrlPatternsToDisableFailedLog; class FMockHttpModule : public FHttpModule { public: using FHttpModule::HttpConnectionTimeout; using FHttpModule::HttpTotalTimeout; using FHttpModule::HttpActivityTimeout; }; class FHttpTestLogLevelInitializer { public: FHttpTestLogLevelInitializer() : OldVerbosity(LogHttp.GetVerbosity()) { FParse::Bool(FCommandLine::Get(), TEXT("very_verbose="), bVeryVerbose); if (bVeryVerbose) { LogHttp.SetVerbosity(ELogVerbosity::VeryVerbose); } } ~FHttpTestLogLevelInitializer() { ResumeLogVerbosity(); } void DisableWarningsInThisTest() { if (!bVeryVerbose) { LogHttp.SetVerbosity(ELogVerbosity::Error); } } void ResumeLogVerbosity() { if (OldVerbosity != LogHttp.GetVerbosity()) { LogHttp.SetVerbosity(OldVerbosity); } } bool bVeryVerbose = false; ELogVerbosity::Type OldVerbosity; }; class FMockRetryManager : public FHttpRetrySystem::FManager { public: using FHttpRetrySystem::FManager::FManager; using FHttpRetrySystem::FManager::RequestList; using FHttpRetrySystem::FManager::FHttpRetryRequestEntry; using FHttpRetrySystem::FManager::RetryTimeoutRelativeSecondsDefault; using FHttpRetrySystem::FManager::RetryLimitCountDefault; using FHttpRetrySystem::FManager::RetryLimitCountForConnectionErrorDefault; bool IsEmpty() { FScopeLock ScopeLock(&RequestListLock); return RequestList.IsEmpty(); } }; class FHttpModuleTestFixture { public: FHttpModuleTestFixture() : WebServerIp(TEXT("127.0.0.1")) , WebServerHttpPort(8000) , bRunHeavyTests(false) , bRetryEnabled(true) , bEventLoopEnabled(true) { ParseSettingsFromCommandLine(); CVarHttpEventLoopEnableChance->Set(bEventLoopEnabled ? 100 : 0); InitModule(); CVarHttpInsecureProtocolEnabled->Set(true); } void InitModule() { HttpModule = new FMockHttpModule(); HttpModule->StartupModule(); if (bRetryEnabled) { HttpRetryManager = MakeShared(FHttpRetrySystem::FRetryLimitCountSetting(0), FHttpRetrySystem::FRetryTimeoutRelativeSecondsSetting(/*RetryTimeoutRelativeSeconds*/)); } } void ShutdownModule() { HttpRetryManager = nullptr; if (HttpModule) { HttpModule->ShutdownModule(); delete HttpModule; HttpModule = nullptr; } } virtual ~FHttpModuleTestFixture() { ShutdownModule(); } void ParseSettingsFromCommandLine() { FParse::Value(FCommandLine::Get(), TEXT("web_server_ip="), WebServerIp); FParse::Bool(FCommandLine::Get(), TEXT("run_heavy_tests="), bRunHeavyTests); FParse::Bool(FCommandLine::Get(), TEXT("retry_enabled="), bRetryEnabled); FParse::Bool(FCommandLine::Get(), TEXT("eventloop_enabled="), bEventLoopEnabled); FParse::Value(FCommandLine::Get(), TEXT("web_server_unix_socket="), WebServerUnixSocket); } void DisableWarningsInThisTest() { HttpTestLogLevelInitializer.DisableWarningsInThisTest(); } void ResumeLogVerbosity() { HttpTestLogLevelInitializer.ResumeLogVerbosity(); } TSharedRef CreateRequest() { return bRetryEnabled ? HttpRetryManager->CreateRequest() : HttpModule->CreateRequest(); } void SetInvalidDomainToTestConnectTimeout() { WebServerIp = TEXT("10.255.255.1"); WebServerHttpPort = 8765; } // non-routable IP address with a random port const FString UrlDomain() const { return FString::Format(TEXT("{0}:{1}"), { *WebServerIp, WebServerHttpPort }); } const FString UrlBase() const { return FString::Format(TEXT("http://{0}"), { *UrlDomain() }); } const FString UrlHttpTests() const { return FString::Format(TEXT("{0}/webtests/httptests"), { *UrlBase() }); } const FString UrlToTestMethods() const { return FString::Format(TEXT("{0}/methods"), { *UrlHttpTests() }); } const FString UrlStreamDownload(uint32 Chunks, uint32 ChunkSize, uint32 ChunkLatency=0) { return FString::Format(TEXT("{0}/streaming_download/{1}/{2}/{3}/"), { *UrlHttpTests(), Chunks, ChunkSize, ChunkLatency }); } const FString UrlStreamUpload() { return FString::Format(TEXT("{0}/streaming_upload_put"), { *UrlHttpTests() }); } const FString UrlMockLatency(uint32 Latency) const { return FString::Format(TEXT("{0}/mock_latency/{1}/"), { *UrlHttpTests(), Latency }); } const FString UrlMockStatus(uint32 StatusCode) const { return FString::Format(TEXT("{0}/mock_status/{1}/"), { *UrlHttpTests(), StatusCode }); } const FString UrlUnixSocketHttpTests() const { return "http://localhost/webtests/unixsockettests"; } FString WebServerIp; FString WebServerUnixSocket; uint32 WebServerHttpPort; FMockHttpModule* HttpModule = nullptr; bool bRunHeavyTests; bool bRetryEnabled; bool bEventLoopEnabled; FHttpTestLogLevelInitializer HttpTestLogLevelInitializer; TSharedPtr HttpRetryManager; }; TEST_CASE_METHOD(FHttpModuleTestFixture, "Shutdown http module without issue when there are ongoing upload http requests.", HTTP_TAG) { DisableWarningsInThisTest(); uint32 ChunkSize = 1024 * 1024; TArray DataChunk; DataChunk.SetNum(ChunkSize); FMemory::Memset(DataChunk.GetData(), 'd', ChunkSize); for (int32 i = 0; i < 10; ++i) { IHttpRequest* LeakingHttpRequest = FPlatformHttp::ConstructRequest(); // Leaking in purpose to make sure it's ok TSharedRef HttpRequest = CreateRequest(); HttpRequest->SetURL(UrlToTestMethods()); HttpRequest->SetVerb(TEXT("PUT")); // TODO: Use some shared data, like cookie, openssl session etc. HttpRequest->SetContent(DataChunk); HttpRequest->OnProcessRequestComplete().BindLambda([](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(bSucceeded); }); HttpRequest->ProcessRequest(); } HttpModule->GetHttpManager().Tick(0.0f); } TEST_CASE("Get domain of url can work well", HTTP_TAG) { FString DefaultUrlDomain = FPlatformHttp::GetUrlDomain(TEXT("https://www.epicgames.com/the/relative/path/")); CHECK(DefaultUrlDomain == TEXT("www.epicgames.com")); } TEST_CASE_METHOD(FHttpModuleTestFixture, "Duplicated scheme protocol will trigger warning", HTTP_TAG) { // TODO: find a way to catch expected warning log instead DisableWarningsInThisTest(); TSharedRef HttpRequest = CreateRequest(); CHECK(HttpRequest->GetVerb() == TEXT("GET")); HttpRequest->SetURL(TEXT("https://https://www.epicgames.com")); HttpRequest->ProcessRequest(); } TEST_CASE_METHOD(FHttpModuleTestFixture, "Shutdown http module without issue when there are ongoing streaming http requests with timeout.", HTTP_TAG) { if (!bRunHeavyTests) { return; } // When use generator, it doesn't do the ctor and dtor of FHttpModuleTestFixture each time, so manually // shutdown and init here to shutdown module a lot of times ShutdownModule(); InitModule(); int32 NumRequests = GENERATE( 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100 ); //Output NumRequests when error occurs. UNSCOPED_INFO(NumRequests); HttpModule->HttpTotalTimeout = 2.0f; HttpModule->HttpActivityTimeout = 1.0f; DYNAMIC_SECTION(" making " << NumRequests << " requests") { DisableWarningsInThisTest(); uint32 ChunkSize = 1024 * 1024; TArray DataChunk; DataChunk.SetNum(ChunkSize); FMemory::Memset(DataChunk.GetData(), 'd', ChunkSize); for (int32 i = 0; i < NumRequests; ++i) { { TSharedRef HttpRequest = CreateRequest(); HttpRequest->SetURL(UrlToTestMethods()); HttpRequest->SetVerb(TEXT("PUT")); HttpRequest->SetContent(DataChunk); HttpRequest->OnProcessRequestComplete().BindLambda([](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(bSucceeded); }); HttpRequest->ProcessRequest(); } { TSharedRef HttpRequest = CreateRequest(); HttpRequest->SetURL(UrlStreamDownload(2, HTTP_TEST_TIMEOUT_CHUNK_SIZE, 2)); HttpRequest->OnProcessRequestComplete().BindLambda([](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(bSucceeded); }); HttpRequest->ProcessRequest(); } } HttpModule->GetHttpManager().Tick(0.0f); } ShutdownModule(); } class FWaitUntilCompleteHttpFixture : public FHttpModuleTestFixture { public: FWaitUntilCompleteHttpFixture() { HttpModule->GetHttpManager().SetRequestAddedDelegate(FHttpManagerRequestAddedDelegate::CreateRaw(this, &FWaitUntilCompleteHttpFixture::OnRequestAdded)); HttpModule->GetHttpManager().SetRequestCompletedDelegate(FHttpManagerRequestCompletedDelegate::CreateRaw(this, &FWaitUntilCompleteHttpFixture::OnRequestCompleted)); } ~FWaitUntilCompleteHttpFixture() { WaitUntilAllHttpRequestsComplete(); CHECK(ExpectingExtraCallbacks == 0); HttpModule->GetHttpManager().SetRequestAddedDelegate(FHttpManagerRequestAddedDelegate()); HttpModule->GetHttpManager().SetRequestCompletedDelegate(FHttpManagerRequestCompletedDelegate()); } void OnRequestAdded(const FHttpRequestRef& Request) { ++OngoingRequests; } void OnRequestCompleted(const FHttpRequestRef& Request) { ensure(--OngoingRequests >= 0); } void TickHttpManager() { double Now = FPlatformTime::Seconds(); static double LastTick = Now; double Duration = Now - LastTick; LastTick = Now; HttpModule->GetHttpManager().Tick(Duration); FPlatformProcess::Sleep(TickFrequency); } void WaitUntilAllHttpRequestsComplete() { while (HasOngoingRequest()) { TickHttpManager(); } // In case in http thread the http request complete and set OngoingRequests to 0, http manager never // had chance to Tick and remove the request TickHttpManager(); } bool HasOngoingRequest() const { return OngoingRequests != 0 || (bRetryEnabled && !HttpRetryManager->IsEmpty()); } std::atomic OngoingRequests = 0; float TickFrequency = 1.0f / 60; /*60 FPS*/; uint32 RetryLimitCount = 0; uint32 ExpectingExtraCallbacks = 0; }; TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Http method use Get by default", HTTP_TAG) { TSharedRef HttpRequest = CreateRequest(); CHECK(HttpRequest->GetVerb() == TEXT("GET")); HttpRequest->SetURL(UrlToTestMethods()); HttpRequest->OnProcessRequestComplete().BindLambda([](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(bSucceeded); REQUIRE(HttpResponse != nullptr); CHECK(HttpResponse->GetResponseCode() == 200); }); HttpRequest->ProcessRequest(); } TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Http Methods", HTTP_TAG) { TSharedRef HttpRequest = CreateRequest(); CHECK(HttpRequest->GetVerb() == TEXT("GET")); HttpRequest->SetURL(UrlToTestMethods()); #if UE_HTTP_SUPPORT_VERB_CONNECT const ANSICHAR* Method = GENERATE("GET", "POST", "DELETE", "PUT", "PATCH", "HEAD", "OPTIONS", "TRACE", "CONNECT"); #else // Can't use macro UE_HTTP_SUPPORT_VERB_CONNECT inside GENERATE macro, so define it here again const ANSICHAR* Method = GENERATE("GET", "POST", "DELETE", "PUT", "PATCH", "HEAD", "OPTIONS", "TRACE"); #endif DYNAMIC_SECTION("Test methods with " << Method << " request") { HttpRequest->SetVerb(FString(Method)); } HttpRequest->OnProcessRequestComplete().BindLambda([](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(bSucceeded); REQUIRE(HttpResponse != nullptr); CHECK(HttpResponse->GetResponseCode() == 200); }); HttpRequest->ProcessRequest(); } TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Can process https request", HTTP_TAG) { TSharedRef HttpRequest = CreateRequest(); HttpRequest->SetVerb(TEXT("GET")); HttpRequest->SetURL(TEXT("https://www.unrealengine.com/")); HttpRequest->OnProcessRequestComplete().BindLambda([](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(bSucceeded); REQUIRE(HttpResponse != nullptr); }); HttpRequest->ProcessRequest(); } TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Can mock connection error through CVar", HTTP_TAG) { DisableWarningsInThisTest(); CVarHttpUrlPatternsToMockFailure->Set(TEXT("epicgames.com->0 unrealengine.com->503")); float ExpectedTimeoutDuration = 2.0f; HttpModule->HttpConnectionTimeout = ExpectedTimeoutDuration; const double StartTime = FPlatformTime::Seconds(); TSharedRef HttpRequest = CreateRequest(); HttpRequest->SetURL(TEXT("https://www.epicgames.com/")); HttpRequest->OnProcessRequestComplete().BindLambda([StartTime, ExpectedTimeoutDuration](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(!bSucceeded); CHECK(!HttpResponse); CHECK(HttpRequest->GetFailureReason() == EHttpFailureReason::ConnectionError); const double DurationInSeconds = FPlatformTime::Seconds() - StartTime; CHECK(FMath::IsNearlyEqual(DurationInSeconds, ExpectedTimeoutDuration, HTTP_TIME_DIFF_TOLERANCE_OF_REQUEST)); CVarHttpUrlPatternsToMockFailure->Set(TEXT("")); }); HttpRequest->ProcessRequest(); } TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Can mock response failure through CVar", HTTP_TAG) { CVarHttpUrlPatternsToMockFailure->Set(TEXT("epicgames.com->0 unrealengine.com->503")); TSharedRef HttpRequest = CreateRequest(); HttpRequest->SetVerb(TEXT("GET")); HttpRequest->SetURL(TEXT("https://www.unrealengine.com/")); HttpRequest->OnProcessRequestComplete().BindLambda([](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(bSucceeded); REQUIRE(HttpResponse != nullptr); CHECK(HttpResponse->GetResponseCode() == 503); CVarHttpUrlPatternsToMockFailure->Set(TEXT("")); }); HttpRequest->ProcessRequest(); } TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Can process request with gzip support", HTTP_TAG) { uint32 DataLength = 1025; uint32 RepeatAt = 9; TSharedRef HttpRequest = CreateRequest(); HttpRequest->SetVerb(TEXT("GET")); HttpRequest->SetURL(FString::Format(TEXT("{0}/get_data_gzip/{1}/{2}/"), { *UrlHttpTests(), DataLength, RepeatAt })); HttpRequest->SetHeader(TEXT("Accept-Encoding"), TEXT("gzip")); HttpRequest->OnProcessRequestComplete().BindLambda([DataLength, RepeatAt](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(bSucceeded); REQUIRE(HttpResponse != nullptr); CHECK(HttpResponse->GetResponseCode() == 200); const TArray& Content = HttpResponse->GetContent(); CHECK(Content.Num() == DataLength); bool bAllMatch = true; // Make sure the data got read is in good state for (int32 i = 0; i < Content.Num(); ++i) { bAllMatch &= (Content[i] == '0' + (i % RepeatAt)); } CHECK(bAllMatch); }); HttpRequest->ProcessRequest(); } // Test the cases when http request wasn't added into http manager and completed, but user code reset the HttpRequest // in the complete callback, so it will be destroyed because that's the only reference. // Verify there is no more access from http code after the complete callback in such scenarios(FinishRequestNotInHttpManager) class FEarlyCompleteHttpFixture : public FWaitUntilCompleteHttpFixture { public: ~FEarlyCompleteHttpFixture() { while (HttpRequest.IsValid()) { TickHttpManager(); } } TSharedPtr HttpRequest; }; TEST_CASE_METHOD(FEarlyCompleteHttpFixture, "Can delete failed request during complete callback for mock failures", HTTP_TAG) { // When use retry manager, the request adapter will keep the shared ptr of the request impl, which can't reproduce the deletion if (bRetryEnabled) { return; } DisableWarningsInThisTest(); CVarHttpUrlPatternsToMockFailure->Set(TEXT("epicgames.com->0 unrealengine.com->503")); HttpRequest = CreateRequest(); SECTION("For mock failure with status code") { HttpRequest->SetURL(TEXT("https://www.unrealengine.com/")); } SECTION("For mock failure with connection error") { HttpModule->HttpConnectionTimeout = 2.0f; HttpRequest->SetURL(TEXT("https://www.epicgames.com/")); } HttpRequest->OnProcessRequestComplete().BindLambda([this](FHttpRequestPtr InHttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CVarHttpUrlPatternsToMockFailure->Set(TEXT("")); HttpRequest.Reset(); }); HttpRequest->ProcessRequest(); } TEST_CASE_METHOD(FEarlyCompleteHttpFixture, "Can delete failed request during complete callback in pre process phase", HTTP_TAG) { // When use retry manager, the request adapter will keep the shared ptr of the request impl, which can't reproduce the deletion if (bRetryEnabled) { return; } DisableWarningsInThisTest(); HttpRequest = CreateRequest(); SECTION("For pre check failure when verb was set to empty") { HttpRequest->SetURL(TEXT("https://www.unrealengine.com/")); HttpRequest->SetVerb(TEXT("")); } SECTION("For setup failure") { FString Filename = FString(FPlatformProcess::UserSettingsDir()) / TEXT("TestStreamUploadNonExist.dat"); HttpRequest->SetURL(UrlStreamUpload()); HttpRequest->SetVerb(TEXT("PUT")); HttpRequest->SetHeader(TEXT("Content-Disposition"), TEXT("attachment;filename=TestStreamUploadNonExist.dat")); HttpRequest->SetContentAsStreamedFile(Filename); } HttpRequest->OnProcessRequestComplete().BindLambda([this](FHttpRequestPtr InHttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { HttpRequest.Reset(); }); HttpRequest->ProcessRequest(); } TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Can complete successfully for different response codes", HTTP_TAG) { TSharedRef HttpRequest = CreateRequest(); HttpRequest->SetVerb(TEXT("GET")); int32 ExpectedStatusCode = 0; SECTION("For status 200") { ExpectedStatusCode = 200; } SECTION("For status 206") { ExpectedStatusCode = 206; } SECTION("For status 400") { ExpectedStatusCode = 400; } HttpRequest->SetURL(UrlMockStatus(ExpectedStatusCode)); HttpRequest->OnProcessRequestComplete().BindLambda([ExpectedStatusCode](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(bSucceeded); REQUIRE(HttpResponse != nullptr); CHECK(HttpResponse->GetResponseCode() == ExpectedStatusCode); }); HttpRequest->ProcessRequest(); } TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Can do blocking call", HTTP_TAG) { TSharedRef HttpRequest = CreateRequest(); HttpRequest->SetURL(UrlToTestMethods()); HttpRequest->ProcessRequestUntilComplete(); CHECK(HttpRequest->GetStatus() == EHttpRequestStatus::Succeeded); FHttpResponsePtr HttpResponse = HttpRequest->GetResponse(); REQUIRE(HttpResponse != nullptr); CHECK(HttpResponse->GetResponseCode() == 200); } TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Get large response content without chunks", HTTP_TAG) { TSharedRef HttpRequest = CreateRequest(); uint32 DataLength = 0; uint32 RepeatAt = 0; SECTION("case A") { DataLength = 1024 * 1024; RepeatAt = 10; } SECTION("cast B") { DataLength = 1025 * 1023; RepeatAt = 9; } HttpRequest->SetURL(FString::Format(TEXT("{0}/get_data_without_chunks/{1}/{2}/"), { *UrlHttpTests(), DataLength, RepeatAt})); HttpRequest->SetVerb(TEXT("GET")); HttpRequest->OnProcessRequestComplete().BindLambda([DataLength, RepeatAt](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(bSucceeded); REQUIRE(HttpResponse != nullptr); CHECK(HttpResponse->GetResponseCode() == 200); const TArray& Content = HttpResponse->GetContent(); CHECK(Content.Num() == DataLength); bool bAllMatch = true; // Make sure the data got read is in good state for (int32 i = 0; i < Content.Num(); ++i) { bAllMatch &= (Content[i] == '0' + (i % RepeatAt)); } CHECK(bAllMatch); }); HttpRequest->ProcessRequest(); } TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Http request connect timeout", HTTP_TAG) { DisableWarningsInThisTest(); HttpModule->HttpActivityTimeout = 3.0f; // Make sure this won't be triggered before establishing connection float ExpectedTimeoutDuration = 15.0f; HttpModule->HttpConnectionTimeout = ExpectedTimeoutDuration; TSharedRef HttpRequest = CreateRequest(); SetInvalidDomainToTestConnectTimeout(); HttpRequest->SetURL(UrlToTestMethods()); HttpRequest->SetVerb(TEXT("GET")); const double StartTime = FPlatformTime::Seconds(); HttpRequest->OnProcessRequestComplete().BindLambda([StartTime, ExpectedTimeoutDuration](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(!bSucceeded); CHECK(HttpResponse == nullptr); CHECK(HttpRequest->GetStatus() == EHttpRequestStatus::Failed); CHECK(HttpRequest->GetFailureReason() == EHttpFailureReason::ConnectionError); const double DurationInSeconds = FPlatformTime::Seconds() - StartTime; CHECK(FMath::IsNearlyEqual(DurationInSeconds, ExpectedTimeoutDuration, UE_HTTP_CONNECTION_TIMEOUT_MAX_DEVIATION)); }); HttpRequest->ProcessRequest(); } TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Streaming http download", HTTP_TAG) { uint32 Chunks = 3; uint32 ChunkSize = 1024*1024; TSharedRef HttpRequest = CreateRequest(); HttpRequest->SetURL(UrlStreamDownload(Chunks, ChunkSize)); HttpRequest->SetVerb(TEXT("GET")); TSharedRef TotalBytesReceived = MakeShared(0); SECTION("Success without stream provided") { HttpRequest->OnProcessRequestComplete().BindLambda([Chunks, ChunkSize](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(bSucceeded); REQUIRE(HttpResponse != nullptr); CHECK(HttpResponse->GetResponseCode() == 200); CHECK(!HttpResponse->GetAllHeaders().IsEmpty()); CHECK(HttpResponse->GetContentLength() == Chunks * ChunkSize); }); } SECTION("Success with customized stream") { class FTestHttpReceiveStream final : public FArchive { public: FTestHttpReceiveStream(TSharedRef InTotalBytesReceived) : TotalBytesReceived(InTotalBytesReceived) { } virtual void Serialize(void* V, int64 Length) override { *TotalBytesReceived += Length; } TSharedRef TotalBytesReceived; }; TSharedRef Stream = MakeShared(TotalBytesReceived); CHECK(HttpRequest->SetResponseBodyReceiveStream(Stream)); HttpRequest->OnProcessRequestComplete().BindLambda([Chunks, ChunkSize, TotalBytesReceived](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(bSucceeded); REQUIRE(HttpResponse != nullptr); CHECK(HttpResponse->GetResponseCode() == 200); CHECK(!HttpResponse->GetAllHeaders().IsEmpty()); CHECK(HttpResponse->GetContentLength() == Chunks * ChunkSize); CHECK(HttpResponse->GetContent().IsEmpty()); CHECK(*TotalBytesReceived == Chunks * ChunkSize); }); } SECTION("Success with customized stream delegate") { FHttpRequestStreamDelegateV2 Delegate; Delegate.BindLambda([TotalBytesReceived](void* Ptr, int64& Length) { *TotalBytesReceived += Length; }); CHECK(HttpRequest->SetResponseBodyReceiveStreamDelegateV2(Delegate)); HttpRequest->OnProcessRequestComplete().BindLambda([Chunks, ChunkSize, TotalBytesReceived](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(bSucceeded); REQUIRE(HttpResponse != nullptr); CHECK(HttpResponse->GetResponseCode() == 200); CHECK(!HttpResponse->GetAllHeaders().IsEmpty()); CHECK(HttpResponse->GetContentLength() == Chunks * ChunkSize); CHECK(HttpResponse->GetContent().IsEmpty()); CHECK(*TotalBytesReceived == Chunks * ChunkSize); }); } SECTION("Use customized stream to receive response body but failed when serialize") { DisableWarningsInThisTest(); class FTestHttpReceiveStream final : public FArchive { public: FTestHttpReceiveStream(TSharedRef InTotalBytesReceived) : TotalBytesReceived(InTotalBytesReceived) { } virtual void Serialize(void* V, int64 Length) override { *TotalBytesReceived += Length; SetError(); } TSharedRef TotalBytesReceived; }; TSharedRef Stream = MakeShared(TotalBytesReceived); CHECK(HttpRequest->SetResponseBodyReceiveStream(Stream)); HttpRequest->OnProcessRequestComplete().BindLambda([ChunkSize, TotalBytesReceived](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(!bSucceeded); CHECK(HttpResponse != nullptr); CHECK(*TotalBytesReceived <= ChunkSize); }); } SECTION("Use customized stream delegate to receive response body but failed when call") { DisableWarningsInThisTest(); FHttpRequestStreamDelegateV2 Delegate; Delegate.BindLambda([TotalBytesReceived](void* Ptr, int64& Length) { *TotalBytesReceived += Length; Length = 0; // Mark as no data was serialized successfully }); CHECK(HttpRequest->SetResponseBodyReceiveStreamDelegateV2(Delegate)); HttpRequest->OnProcessRequestComplete().BindLambda([ChunkSize, TotalBytesReceived](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(!bSucceeded); CHECK(HttpResponse != nullptr); CHECK(*TotalBytesReceived <= ChunkSize); }); } SECTION("Success with file stream to receive response body") { FString Filename = FString(FPlatformProcess::UserSettingsDir()) / TEXT("TestStreamDownload.dat"); FArchive* RawFile = IFileManager::Get().CreateFileWriter(*Filename); CHECK(RawFile != nullptr); TSharedRef FileToWrite = MakeShareable(RawFile); CHECK(HttpRequest->SetResponseBodyReceiveStream(FileToWrite)); HttpRequest->OnProcessRequestComplete().BindLambda([Chunks, ChunkSize, Filename, FileToWrite](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(bSucceeded); REQUIRE(HttpResponse != nullptr); CHECK(HttpResponse->GetContentLength() == Chunks * ChunkSize); CHECK(HttpResponse->GetContent().IsEmpty()); CHECK(HttpResponse->GetResponseCode() == 200); CHECK(!HttpResponse->GetAllHeaders().IsEmpty()); FileToWrite->FlushCache(); FileToWrite->Close(); TSharedRef FileToRead = MakeShareable(IFileManager::Get().CreateFileReader(*Filename)); CHECK(FileToRead->TotalSize() == Chunks * ChunkSize); FileToRead->Close(); IFileManager::Get().Delete(*Filename); }); } HttpRequest->ProcessRequest(); } // This user streaming class is supposed to be used to receive streaming data through function OnReceivedData // and it's not supposed to be called once destroyed class FUserStreamingClass { public: FUserStreamingClass() : TotalBytesReceived(new int64(0)) { } ~FUserStreamingClass() { delete TotalBytesReceived; TotalBytesReceived = nullptr; } void OnReceivedData(void* Ptr, int64& Length) { *TotalBytesReceived += Length; } int64* TotalBytesReceived; }; TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "In streaming downloading http request won't trigger response body receive delegate after canceling", HTTP_TAG) { TSharedRef HttpRequest = CreateRequest(); HttpRequest->SetURL(UrlStreamDownload(60, 1024*1024)); TSharedPtr UserInstance = MakeShared(); FHttpRequestStreamDelegateV2 Delegate; Delegate.BindThreadSafeSP(UserInstance.ToSharedRef(), &FUserStreamingClass::OnReceivedData); CHECK(HttpRequest->SetResponseBodyReceiveStreamDelegateV2(Delegate)); HttpRequest->OnProcessRequestComplete().BindLambda([](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(!bSucceeded); CHECK(HttpResponse != nullptr); }); HttpRequest->ProcessRequest(); while (*UserInstance->TotalBytesReceived == 0) // Make sure it started receiving data { FPlatformProcess::Sleep(0.001f); } CHECK(*UserInstance->TotalBytesReceived < 60 * 1024 * 1024); HttpRequest->CancelRequest(); CHECK(UserInstance.GetSharedReferenceCount() == 1); UserInstance.Reset(); } TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "In streaming downloading http request won't crash if shared ptr bound to delegate got destroyed", HTTP_TAG) { DisableWarningsInThisTest(); // Failed writing received data to disk/application TSharedRef HttpRequest = CreateRequest(); HttpRequest->SetURL(UrlStreamDownload(60, 1024*1024)); TSharedPtr UserInstance = MakeShared(); FHttpRequestStreamDelegateV2 Delegate; Delegate.BindThreadSafeSP(UserInstance.ToSharedRef(), &FUserStreamingClass::OnReceivedData); CHECK(HttpRequest->SetResponseBodyReceiveStreamDelegateV2(Delegate)); HttpRequest->ProcessRequest(); while (*UserInstance->TotalBytesReceived == 0) // Make sure it started receiving data { FPlatformProcess::Sleep(0.001f); } CHECK(*UserInstance->TotalBytesReceived < 60 * 1024 * 1024); CHECK(UserInstance.GetSharedReferenceCount() == 1); UserInstance.Reset(); } class FInvalidateDelegateShutdownFixture : public FHttpModuleTestFixture { public: FInvalidateDelegateShutdownFixture() { UserStreamingInstance = MakeShared(); } TSharedPtr UserStreamingInstance; }; TEST_CASE_METHOD(FInvalidateDelegateShutdownFixture, "Shutdown http module without issue when there are ongoing download http requests", HTTP_TAG) { DisableWarningsInThisTest(); for (int32 i = 0; i < 10; ++i) { TSharedRef HttpRequest = HttpModule->CreateRequest(); HttpRequest->SetURL(UrlStreamDownload(10, 1024*1024)); FHttpRequestStreamDelegateV2 Delegate; Delegate.BindThreadSafeSP(UserStreamingInstance.ToSharedRef(), &FUserStreamingClass::OnReceivedData); CHECK(HttpRequest->SetResponseBodyReceiveStreamDelegateV2(Delegate)); HttpRequest->OnProcessRequestComplete().BindLambda([](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(bSucceeded); }); HttpRequest->ProcessRequest(); } while (*UserStreamingInstance->TotalBytesReceived == 0) // Make sure it started receiving data { FPlatformProcess::Sleep(0.001f); } HttpModule->GetHttpManager().Tick(0.1f); } TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Can run parallel stream download requests", HTTP_TAG) { uint32 Chunks = 5; uint32 ChunkSize = 1024*1024; for (int i = 0; i < 3; ++i) { TSharedRef HttpRequest = CreateRequest(); HttpRequest->SetURL(UrlStreamDownload(Chunks, ChunkSize)); HttpRequest->SetVerb(TEXT("GET")); HttpRequest->OnProcessRequestComplete().BindLambda([Chunks, ChunkSize](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(HttpResponse->GetContentLength() == Chunks * ChunkSize); CHECK(bSucceeded); CHECK(HttpResponse->GetResponseCode() == 200); }); HttpRequest->ProcessRequest(); } } TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Can early wake up when adding new request", HTTP_TAG) { const double StartTime = FPlatformTime::Seconds(); // First request will wait for a long time before receiving any update TSharedRef HttpRequestSlow = FHttpModule::Get().CreateRequest(); HttpRequestSlow ->SetURL(UrlStreamDownload(1, HTTP_TEST_TIMEOUT_CHUNK_SIZE, /*ChunkLatency=*/3)); HttpRequestSlow->SetDelegateThreadPolicy(EHttpRequestDelegateThreadPolicy::CompleteOnHttpThread); HttpRequestSlow->OnProcessRequestComplete().BindLambda([](FHttpRequestPtr HttpRequestSlow, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(!bSucceeded); }); HttpRequestSlow->ProcessRequest(); FPlatformProcess::Sleep(1); // Second request will complete immediately TSharedRef HttpRequestFast = FHttpModule::Get().CreateRequest(); HttpRequestFast->SetURL(UrlStreamDownload(3, HTTP_TEST_TIMEOUT_CHUNK_SIZE, /*ChunkLatency=*/ 0)); HttpRequestFast->SetDelegateThreadPolicy(EHttpRequestDelegateThreadPolicy::CompleteOnHttpThread); HttpRequestFast->OnProcessRequestComplete().BindLambda([StartTime, HttpRequestSlow](FHttpRequestPtr HttpRequestFast, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(bSucceeded); REQUIRE(HttpResponse != nullptr); CHECK(HttpResponse->GetResponseCode() == 200); const double DurationInSeconds = FPlatformTime::Seconds() - StartTime; CHECK(DurationInSeconds < 2.0f); CHECK(HttpRequestSlow->GetStatus() == EHttpRequestStatus::Processing); HttpRequestSlow->CancelRequest(); }); HttpRequestFast->ProcessRequest(); } TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Can run a lot of small download requests with good performance", HTTP_TAG) { if (!bRunHeavyTests) { return; } const double StartTime = FPlatformTime::Seconds(); int32 NumRequests = 10000; FEvent* Event = FPlatformProcess::GetSynchEventFromPool(true); std::atomic RunningRequests = 0; for (int32 i = 0; i < NumRequests; ++i) { TSharedRef HttpRequest = FHttpModule::Get().CreateRequest(); HttpRequest->SetURL(UrlStreamDownload(3, HTTP_TEST_TIMEOUT_CHUNK_SIZE, /*ChunkLatency=*/ 0)); HttpRequest->SetDelegateThreadPolicy(EHttpRequestDelegateThreadPolicy::CompleteOnHttpThread); HttpRequest->OnProcessRequestComplete().BindLambda([Event, &RunningRequests](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(bSucceeded); REQUIRE(HttpResponse != nullptr); CHECK(HttpResponse->GetResponseCode() == 200); --RunningRequests; }); // Add requests constantly to validate early wake up performance instead of adding all at beginning while (RunningRequests > 20) { FPlatformProcess::Sleep(0.0f); } ++RunningRequests; HttpRequest->ProcessRequest(); } WaitUntilAllHttpRequestsComplete(); const double DurationInSeconds = FPlatformTime::Seconds() - StartTime; UE_LOG(LogHttp, Log, TEXT("%d small download requests spent %.2fs to complete!"), NumRequests, DurationInSeconds); } TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Can download big file exceeds 32 bits with good performance", HTTP_TAG) { if (!bRunHeavyTests) { return; } const double StartTime = FPlatformTime::Seconds(); // 5 * 1024 * 1024 * 1024 BYTES = 5368709120 BYTES = 5 GB uint64 DataSizeInGB = 5; uint64 Chunks = DataSizeInGB * 1024; uint64 ChunkSize = 1024 * 1024; TSharedRef HttpRequest = CreateRequest(); HttpRequest->SetURL(UrlStreamDownload(Chunks, ChunkSize)); HttpRequest->SetVerb(TEXT("GET")); TSharedRef TotalBytesReceived = MakeShared(0); FHttpRequestStreamDelegateV2 Delegate; Delegate.BindLambda([TotalBytesReceived](void* Ptr, int64& Length) { *TotalBytesReceived += Length; }); HttpRequest->SetResponseBodyReceiveStreamDelegateV2(Delegate); HttpRequest->OnProcessRequestComplete().BindLambda([Chunks, ChunkSize, TotalBytesReceived](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(bSucceeded); REQUIRE(HttpResponse != nullptr); CHECK(HttpResponse->GetContentLength() == Chunks * ChunkSize); CHECK(HttpResponse->GetContent().IsEmpty()); CHECK(*TotalBytesReceived == Chunks * ChunkSize); CHECK(HttpResponse->GetResponseCode() == 200); }); HttpRequest->ProcessRequest(); WaitUntilAllHttpRequestsComplete(); const double DurationInSeconds = FPlatformTime::Seconds() - StartTime; UE_LOG(LogHttp, Log, TEXT("%d GB of big download request spent %.2fs to complete!"), DataSizeInGB, DurationInSeconds); } TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Streaming http upload from memory", HTTP_TAG) { TSharedRef HttpRequest = CreateRequest(); HttpRequest->SetURL(FString::Format(TEXT("{0}/streaming_upload_post"), { *UrlHttpTests() })); HttpRequest->SetVerb(TEXT("POST")); const char* BoundaryLabel = "test_http_boundary"; HttpRequest->SetHeader(TEXT("Content-Type"), FString::Format(TEXT("multipart/form-data; boundary={0}"), { BoundaryLabel })); // Data will be sent by chunks in http request const uint32 FileSize = 10*1024*1024; char* FileData = (char*)FMemory::Malloc(FileSize + 1); FMemory::Memset(FileData, 'd', FileSize); FileData[FileSize + 1] = '\0'; TArray ContentData; const int32 ContentMaxSize = FileSize + 256/*max length of format string*/; ContentData.Reserve(ContentMaxSize); const int32 ContentLength = FCStringAnsi::Snprintf( (char*)ContentData.GetData(), ContentMaxSize, "--%s\r\n" "Content-Disposition: form-data; name=\"file\"; filename=\"bigfile.zip\"\r\n" "Content-Type: application/octet-stream\r\n\r\n" "%s\r\n" "--%s--", BoundaryLabel, FileData, BoundaryLabel); FMemory::Free(FileData); CHECK(ContentLength > 0); CHECK(ContentLength < ContentMaxSize); ContentData.SetNumUninitialized(ContentLength); HttpRequest->SetContent(MoveTemp(ContentData)); HttpRequest->OnProcessRequestComplete().BindLambda([](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(bSucceeded); REQUIRE(HttpResponse != nullptr); CHECK(HttpResponse->GetResponseCode() == 200); }); HttpRequest->ProcessRequest(); } class FTestHttpUploadStream final : public FArchive { public: FTestHttpUploadStream(uint64 InTotalSize) : FakeTotalSize(InTotalSize) { } virtual void Serialize(void* V, int64 Length) override { for (int64 i = 0; i < Length; ++i) { ((char*)V)[i] = 'd'; } CurrentPos += Length; } virtual int64 TotalSize() override { return FakeTotalSize; } virtual void Seek(int64 InPos) { CurrentPos = InPos; } virtual int64 Tell() override { return CurrentPos; } uint64 FakeTotalSize; uint64 CurrentPos = 0; }; TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Can upload big file exceeds 32 bits", HTTP_TAG) { if (!bRunHeavyTests) { return; } // TODO: Back to check later. xCurl 2206.4.0.0 doesn't work with file bigger than 32 bits // 5 * 1024 * 1024 * 1024 BYTES = 5368709120 BYTES = 5 GB //const uint64 TotalSize = 5368709120; //const uint64 TotalSize = 4294967296; //const uint64 TotalSize = 4294967295; //const uint64 TotalSize = 2147483648; const uint64 TotalSize = 2147483647; TSharedRef Stream = MakeShared(TotalSize); TSharedRef HttpRequest = CreateRequest(); HttpRequest->SetURL(UrlStreamUpload()); HttpRequest->SetVerb(TEXT("PUT")); HttpRequest->SetContentFromStream(Stream); HttpRequest->SetHeader(TEXT("Content-Disposition"), TEXT("attachment;filename=TestStreamUpload.dat")); HttpRequest->OnProcessRequestComplete().BindLambda([Stream, TotalSize](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(bSucceeded); REQUIRE(HttpResponse != nullptr); CHECK(HttpResponse->GetResponseCode() == 200); CHECK(Stream->CurrentPos == TotalSize); }); HttpRequest->ProcessRequest(); } namespace UE { namespace TestHttp { void WriteTestFile(const FString& TestFileName, uint64 TestFileSize) { FArchive* RawFile = IFileManager::Get().CreateFileWriter(*TestFileName); CHECK(RawFile != nullptr); TSharedRef FileToWrite = MakeShareable(RawFile); char* FileData = (char*)FMemory::Malloc(TestFileSize); FMemory::Memset(FileData, 'd', TestFileSize); FileToWrite->Serialize(FileData, TestFileSize); FileToWrite->FlushCache(); FileToWrite->Close(); FMemory::Free(FileData); } } } TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Http request activity timeout", HTTP_TAG) { DisableWarningsInThisTest(); float ActivityTimeoutSetting = 3.0f; HttpModule->HttpActivityTimeout = ActivityTimeoutSetting; TSharedPtr HttpRequest = CreateRequest(); SECTION("By default activity timeout from http module") { } SECTION("By customized activity timeout per http request which will override default settings from http module") { ActivityTimeoutSetting = 4.0f; HttpRequest->SetActivityTimeout(ActivityTimeoutSetting); } HttpRequest->SetURL(UrlStreamDownload(3/*Chunks*/, HTTP_TEST_TIMEOUT_CHUNK_SIZE, 5/*ChunkLatency*/)); HttpRequest->SetVerb(TEXT("GET")); const double StartTime = FPlatformTime::Seconds(); HttpRequest->OnProcessRequestComplete().BindLambda([StartTime, ActivityTimeoutSetting](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(!bSucceeded); CHECK(HttpRequest->GetStatus() == EHttpRequestStatus::Failed); CHECK(HttpRequest->GetFailureReason() == EHttpFailureReason::ConnectionError); const double DurationInSeconds = FPlatformTime::Seconds() - StartTime; #if UE_HTTP_ACTIVITY_TIMER_START_AFTER_RECEIVED_DATA // Unlike libCurl, currently there is an issue in xCurl that it triggers CURLINFO_HEADER_OUT even if can't // connect. Had to disable that code, make sure not to treat that event as connected // In a similar way on MacOS/iOS we don't get any notification until some data is received // So it takes 5s to receive the first chunk to be considered as connected, then start response timer and // take 3s to response timeout CHECK(FMath::IsNearlyEqual(DurationInSeconds, ActivityTimeoutSetting + 5, HTTP_TIME_DIFF_TOLERANCE_OF_REQUEST)); #else CHECK(FMath::IsNearlyEqual(DurationInSeconds, ActivityTimeoutSetting, HTTP_TIME_DIFF_TOLERANCE_OF_REQUEST)); #endif }); HttpRequest->ProcessRequest(); } TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Http request won't trigger activity timeout after cancelling", HTTP_TAG) { HttpModule->HttpActivityTimeout = 2.0f; TSharedPtr HttpRequest = CreateRequest(); HttpRequest->SetURL(UrlStreamDownload(3/*Chunks*/, HTTP_TEST_TIMEOUT_CHUNK_SIZE, 5/*ChunkLatency*/)); HttpRequest->SetVerb(TEXT("GET")); HttpRequest->SetDelegateThreadPolicy(EHttpRequestDelegateThreadPolicy::CompleteOnHttpThread); const double TimeToWaitBeforeCancel = 1.0f; const double StartTime = FPlatformTime::Seconds(); HttpRequest->OnProcessRequestComplete().BindLambda([StartTime, TimeToWaitBeforeCancel](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { const double DurationInSeconds = FPlatformTime::Seconds() - StartTime; CHECK(FMath::IsNearlyEqual(DurationInSeconds, TimeToWaitBeforeCancel, HTTP_TIME_DIFF_TOLERANCE_OF_REQUEST)); CHECK(!bSucceeded); CHECK(HttpRequest->GetStatus() == EHttpRequestStatus::Failed); CHECK(HttpRequest->GetFailureReason() == EHttpFailureReason::Cancelled); }); HttpRequest->ProcessRequest(); FPlatformProcess::Sleep(TimeToWaitBeforeCancel); HttpRequest->CancelRequest(); FPlatformProcess::Sleep(3.0f); // Just make sure there is no warning or assert triggered by the activity timeout callback } TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Http request won't trigger activity timeout after total timeout", HTTP_TAG) { DisableWarningsInThisTest(); HttpModule->HttpActivityTimeout = 2.0f; HttpModule->HttpTotalTimeout = 3.5f; TSharedPtr HttpRequest = CreateRequest(); HttpRequest->SetURL(UrlStreamDownload(5/*Chunks*/, HTTP_TEST_TIMEOUT_CHUNK_SIZE, 1/*ChunkLatency*/)); HttpRequest->SetVerb(TEXT("GET")); HttpRequest->SetDelegateThreadPolicy(EHttpRequestDelegateThreadPolicy::CompleteOnHttpThread); const double StartTime = FPlatformTime::Seconds(); HttpRequest->OnProcessRequestComplete().BindLambda([this](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(!bSucceeded); CHECK(HttpRequest->GetStatus() == EHttpRequestStatus::Failed); CHECK(HttpRequest->GetFailureReason() == EHttpFailureReason::TimedOut); ResumeLogVerbosity(); }); HttpRequest->ProcessRequest(); FPlatformProcess::Sleep(6.0f); // Just make sure there is no warning or assert triggered by the activity timeout callback } TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Http request receive won't timeout for streaming request", HTTP_TAG) { HttpModule->HttpActivityTimeout = 3.0f; TSharedPtr HttpRequest = CreateRequest(); HttpRequest->SetURL(UrlStreamDownload(3/*Chunks*/, HTTP_TEST_TIMEOUT_CHUNK_SIZE, 2/*ChunkLatency*/)); // Needs 6s to complete HttpRequest->SetVerb(TEXT("GET")); const double StartTime = FPlatformTime::Seconds(); HttpRequest->OnProcessRequestComplete().BindLambda([this, StartTime](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(bSucceeded); REQUIRE(HttpResponse != nullptr); CHECK(HttpResponse->GetResponseCode() == 200); const double DurationInSeconds = FPlatformTime::Seconds() - StartTime; CHECK(DurationInSeconds > HttpModule->HttpActivityTimeout); }); HttpRequest->ProcessRequest(); } TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Http request total timeout with get", HTTP_TAG) { DisableWarningsInThisTest(); float TotalTimeoutSetting = 3.0f; HttpModule->HttpConnectionTimeout = 5.0f; TSharedPtr HttpRequest = CreateRequest(); HttpRequest->SetURL(UrlMockLatency(10)); HttpRequest->SetVerb(TEXT("GET")); HttpRequest->SetTimeout(TotalTimeoutSetting); const double StartTime = FPlatformTime::Seconds(); HttpRequest->OnProcessRequestComplete().BindLambda([StartTime, TotalTimeoutSetting](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(!bSucceeded); CHECK(HttpRequest->GetStatus() == EHttpRequestStatus::Failed); CHECK(HttpRequest->GetFailureReason() == EHttpFailureReason::TimedOut); const double DurationInSeconds = FPlatformTime::Seconds() - StartTime; CHECK(FMath::IsNearlyEqual(DurationInSeconds, TotalTimeoutSetting, HTTP_TIME_DIFF_TOLERANCE_OF_REQUEST)); }); HttpRequest->ProcessRequest(); } TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Http request total timeout with streaming download", HTTP_TAG) { DisableWarningsInThisTest(); float TimeoutSetting = 3.0f; HttpModule->HttpActivityTimeout = 2.5f; // Make sure it won't fail because of receive timeout HttpModule->HttpTotalTimeout = TimeoutSetting; if (bRetryEnabled) { TimeoutSetting = 4.0f; // This will override http module default timeout HttpRetryManager->RetryTimeoutRelativeSecondsDefault = TimeoutSetting; } TSharedPtr HttpRequest; SECTION("Use default timeout from http module or retry manager depends on bRetryEnabled") { HttpRequest = CreateRequest(); } SECTION("Override from http request") { TimeoutSetting = 5.0f; // This will override default timeout in http module and retry manager if (bRetryEnabled) { HttpRequest = HttpRetryManager->CreateRequest(FHttpRetrySystem::FRetryLimitCountSetting(), TimeoutSetting); } else { HttpRequest = HttpModule->CreateRequest(); HttpRequest->SetTimeout(TimeoutSetting); } } HttpRequest->SetURL(UrlStreamDownload(4/*Chunks*/, HTTP_TEST_TIMEOUT_CHUNK_SIZE, 2/*ChunkLatency*/)); // Needs 8s to complete HttpRequest->SetVerb(TEXT("GET")); const double StartTime = FPlatformTime::Seconds(); HttpRequest->OnProcessRequestComplete().BindLambda([StartTime, TimeoutSetting](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(!bSucceeded); CHECK(HttpRequest->GetStatus() == EHttpRequestStatus::Failed); CHECK(HttpRequest->GetFailureReason() == EHttpFailureReason::TimedOut); const double DurationInSeconds = FPlatformTime::Seconds() - StartTime; CHECK(FMath::IsNearlyEqual(DurationInSeconds, TimeoutSetting, HTTP_TIME_DIFF_TOLERANCE_OF_REQUEST)); }); HttpRequest->ProcessRequest(); } TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Streaming http upload from file by PUT can work well", HTTP_TAG) { FString Filename = FString(FPlatformProcess::UserSettingsDir()) / TEXT("TestStreamUpload.dat"); UE::TestHttp::WriteTestFile(Filename, 5*1024*1024/*5MB*/); TSharedRef HttpRequest = CreateRequest(); HttpRequest->SetURL(UrlStreamUpload()); HttpRequest->SetVerb(TEXT("PUT")); HttpRequest->SetHeader(TEXT("Content-Disposition"), TEXT("attachment;filename=TestStreamUpload.dat")); HttpRequest->SetContentAsStreamedFile(Filename); HttpRequest->OnProcessRequestComplete().BindLambda([Filename](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(bSucceeded); CHECK(HttpResponse->GetResponseCode() == 200); IFileManager::Get().Delete(*Filename); }); HttpRequest->ProcessRequest(); } TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Streaming http upload from invalid file by PUT will fail", HTTP_TAG) { DisableWarningsInThisTest(); FString Filename = FString(FPlatformProcess::UserSettingsDir()) / TEXT("TestStreamUploadNonExist.dat"); TSharedRef HttpRequest = CreateRequest(); HttpRequest->SetURL(UrlStreamUpload()); HttpRequest->SetVerb(TEXT("PUT")); HttpRequest->SetHeader(TEXT("Content-Disposition"), TEXT("attachment;filename=TestStreamUploadNonExist.dat")); HttpRequest->SetContentAsStreamedFile(Filename); HttpRequest->OnProcessRequestComplete().BindLambda([Filename](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(!bSucceeded); CHECK(HttpRequest->GetFailureReason() == EHttpFailureReason::Other); CHECK(!HttpResponse); }); HttpRequest->ProcessRequest(); } TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Streaming uploading http request will re-open file when retry", HTTP_TAG) { if (!bRetryEnabled) { return; } DisableWarningsInThisTest(); FString Filename = FString(FPlatformProcess::UserSettingsDir()) / TEXT("TestStreamUploadRetry.dat"); UE::TestHttp::WriteTestFile(Filename, 1*1024*1024/*1MB*/); TSharedRef HttpRequest = HttpRetryManager->CreateRequest( 1/*InRetryLimitCountOverride*/, FHttpRetrySystem::FRetryTimeoutRelativeSecondsSetting()/*InRetryTimeoutRelativeSecondsOverride unused*/, {EHttpResponseCodes::TooManyRequests}/*InRetryResponseCodes*/ ); HttpRequest->SetURL(UrlMockStatus(EHttpResponseCodes::TooManyRequests)); HttpRequest->SetHeader(TEXT("Retry-After"), TEXT("1")); // Will be forwarded back in response HttpRequest->SetVerb(TEXT("PUT")); HttpRequest->SetHeader(TEXT("Content-Disposition"), TEXT("attachment;filename=TestStreamUploadRetry.dat")); HttpRequest->SetContentAsStreamedFile(Filename); ++ExpectingExtraCallbacks; HttpRequest->OnRequestWillRetry().BindLambda([this](FHttpRequestPtr Request, FHttpResponsePtr /*Response*/, float LockoutPeriod) { --ExpectingExtraCallbacks; Request->SetURL(UrlStreamUpload()); }); HttpRequest->OnProcessRequestComplete().BindLambda([Filename](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(bSucceeded); CHECK(HttpResponse->GetResponseCode() == 200); IFileManager::Get().Delete(*Filename); }); HttpRequest->ProcessRequest(); } TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Redirect enabled by default and can work well", HTTP_TAG) { TSharedRef HttpRequest = CreateRequest(); FString OriginalURL = FString::Format(TEXT("{0}/redirect_from"), { *UrlHttpTests() }); FString ExpectedURL = FString::Format(TEXT("{0}/redirect_to"), { *UrlHttpTests() }); HttpRequest->SetURL(OriginalURL); HttpRequest->SetVerb(TEXT("GET")); HttpRequest->OnProcessRequestComplete().BindLambda([OriginalURL, ExpectedURL](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(bSucceeded); CHECK(HttpResponse->GetResponseCode() == 200); CHECK(HttpResponse->GetURL() == OriginalURL); CHECK(HttpResponse->GetEffectiveURL() == ExpectedURL); }); HttpRequest->ProcessRequest(); } class FWaitUntilQuitFromTestFixture : public FWaitUntilCompleteHttpFixture { public: FWaitUntilQuitFromTestFixture() { } ~FWaitUntilQuitFromTestFixture() { WaitUntilQuitFromTest(); } void WaitUntilQuitFromTest() { while (!bQuitRequested) { TickHttpManager(); } } bool bQuitRequested = false; }; TEST_CASE_METHOD(FWaitUntilQuitFromTestFixture, "Http request can be reused", HTTP_TAG) { TSharedRef HttpRequest = CreateRequest(); HttpRequest->SetURL(UrlToTestMethods()); HttpRequest->SetVerb(TEXT("POST")); HttpRequest->OnProcessRequestComplete().BindLambda([this](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(bSucceeded); CHECK(HttpResponse->GetResponseCode() == 200); // Using a different URL uint32 Chunks = 3; uint32 ChunkSize = 1024; HttpRequest->SetURL(UrlStreamDownload(Chunks, ChunkSize)); HttpRequest->SetVerb(TEXT("GET")); HttpRequest->OnProcessRequestComplete().BindLambda([this, Chunks, ChunkSize](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(bSucceeded); REQUIRE(HttpResponse != nullptr); CHECK(HttpResponse->GetResponseCode() == 200); CHECK(HttpResponse->GetContentLength() == Chunks * ChunkSize); // Simulate retry with same URL info HttpRequest->OnProcessRequestComplete().BindLambda([this, Chunks, ChunkSize](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(bSucceeded); REQUIRE(HttpResponse != nullptr); CHECK(HttpResponse->GetResponseCode() == 200); CHECK(HttpResponse->GetContentLength() == Chunks * ChunkSize); bQuitRequested = true; }); HttpRequest->ProcessRequest(); }); HttpRequest->ProcessRequest(); }); HttpRequest->ProcessRequest(); } TEST_CASE_METHOD(FWaitUntilQuitFromTestFixture, "Http request can be reused when there is total timeout setting", HTTP_TAG) { DisableWarningsInThisTest(); TSharedRef HttpRequest = CreateRequest(); HttpRequest->SetURL(UrlMockLatency(3)); HttpRequest->SetTimeout(2); HttpRequest->OnProcessRequestComplete().BindLambda([this](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(!bSucceeded); CHECK(HttpRequest->GetFailureReason() == EHttpFailureReason::TimedOut); HttpRequest->SetURL(UrlMockLatency(1)); HttpRequest->ResetTimeoutStatus(); // Must do this in order to restart timeout HttpRequest->OnProcessRequestComplete().BindLambda([this](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(bSucceeded); bQuitRequested = true; }); HttpRequest->ProcessRequest(); }); HttpRequest->ProcessRequest(); } TEST_CASE_METHOD(FWaitUntilQuitFromTestFixture, "Make sure connection time out can work well for 2nd same http request", HTTP_TAG) { DisableWarningsInThisTest(); TSharedRef HttpRequest = CreateRequest(); float ConnectionTimeoutDuration = 2.0f; HttpModule->HttpConnectionTimeout = ConnectionTimeoutDuration; SetInvalidDomainToTestConnectTimeout(); HttpRequest->SetURL(UrlToTestMethods()); const double StartTime = FPlatformTime::Seconds(); HttpRequest->OnProcessRequestComplete().BindLambda([this, StartTime, ConnectionTimeoutDuration](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { TSharedRef HttpRequest2 = CreateRequest(); HttpRequest2->SetURL(UrlToTestMethods()); HttpRequest2->OnProcessRequestComplete().BindLambda([this, StartTime, ConnectionTimeoutDuration](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { bQuitRequested = true; const double DurationInSeconds = FPlatformTime::Seconds() - StartTime; CHECK(FMath::IsNearlyEqual(DurationInSeconds, ConnectionTimeoutDuration * 2, UE_HTTP_CONNECTION_TIMEOUT_MAX_DEVIATION * 2)); }); HttpRequest2->ProcessRequest(); }); HttpRequest->ProcessRequest(); } // Response shared ptr should be able to be kept by user code and valid to access without http request class FValidateResponseDependencyFixture : public FWaitUntilCompleteHttpFixture { public: DECLARE_DELEGATE(FValidateResponseDependencyDelegate); ~FValidateResponseDependencyFixture() { WaitUntilAllHttpRequestsComplete(); ValidateResponseDependencyDelegate.ExecuteIfBound(); } FValidateResponseDependencyDelegate ValidateResponseDependencyDelegate; }; TEST_CASE_METHOD(FValidateResponseDependencyFixture, "Http query with parameters", HTTP_TAG) { struct FQueryWithParamsResponse : public FJsonSerializable { int32 VarInt; FString VarStr; BEGIN_JSON_SERIALIZER JSON_SERIALIZE("var_int", VarInt); JSON_SERIALIZE("var_str", VarStr); END_JSON_SERIALIZER }; TSharedRef HttpRequest = HttpModule->CreateRequest(); FString UrlQueryWithParams = FString::Format(TEXT("{0}/query_with_params/?var_int=3&var_str=abc"), { *UrlHttpTests() }); HttpRequest->SetURL(UrlQueryWithParams); HttpRequest->SetVerb(TEXT("GET")); HttpRequest->OnProcessRequestComplete().BindLambda([this, UrlQueryWithParams](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(bSucceeded); REQUIRE(HttpResponse != nullptr); CHECK(HttpResponse->GetResponseCode() == 200); CHECK(HttpRequest->GetURL() == UrlQueryWithParams); // Validate GetContentAsUtf8StringView works well FQueryWithParamsResponse QueryWithParamsResponseThroughStringView; REQUIRE(QueryWithParamsResponseThroughStringView.FromJsonStringView(HttpResponse->GetContentAsUtf8StringView())); CHECK(FString::FromInt(QueryWithParamsResponseThroughStringView.VarInt) == HttpRequest->GetURLParameter(TEXT("var_int"))); CHECK(QueryWithParamsResponseThroughStringView.VarStr == HttpRequest->GetURLParameter(TEXT("var_str"))); CHECK(FString::FromInt(QueryWithParamsResponseThroughStringView.VarInt) == HttpResponse->GetURLParameter(TEXT("var_int"))); CHECK(QueryWithParamsResponseThroughStringView.VarStr == HttpResponse->GetURLParameter(TEXT("var_str"))); // Validate GetContentAsString works well FQueryWithParamsResponse QueryWithParamsResponse; REQUIRE(QueryWithParamsResponse.FromJson(HttpResponse->GetContentAsString())); CHECK(FString::FromInt(QueryWithParamsResponse.VarInt) == HttpRequest->GetURLParameter(TEXT("var_int"))); CHECK(QueryWithParamsResponse.VarStr == HttpRequest->GetURLParameter(TEXT("var_str"))); CHECK(FString::FromInt(QueryWithParamsResponse.VarInt) == HttpResponse->GetURLParameter(TEXT("var_int"))); CHECK(QueryWithParamsResponse.VarStr == HttpResponse->GetURLParameter(TEXT("var_str"))); ValidateResponseDependencyDelegate.BindLambda([HttpResponse, UrlQueryWithParams, QueryWithParamsResponse](){ // Validate all interfaces of http response can be called without accessing the destroyed http request CHECK(HttpResponse->GetResponseCode() == 200); CHECK(!HttpResponse->GetContent().IsEmpty()); CHECK(!HttpResponse->GetContentAsString().IsEmpty()); CHECK(HttpResponse->GetContentType() == TEXT("application/json")); CHECK(HttpResponse->GetHeader("Content-Type") == TEXT("application/json")); CHECK(!HttpResponse->GetAllHeaders().IsEmpty()); CHECK(HttpResponse->GetURL() == UrlQueryWithParams); CHECK(HttpResponse->GetURLParameter(TEXT("var_int")) == FString::FromInt(QueryWithParamsResponse.VarInt)); CHECK(HttpResponse->GetURLParameter(TEXT("var_str")) == QueryWithParamsResponse.VarStr); }); }); HttpRequest->ProcessRequest(); } class FThreadedHttpRunnable : public FRunnable { public: DECLARE_DELEGATE(FRunActualTestCodeDelegate); FRunActualTestCodeDelegate& OnRunFromThread() { return ThreadCallback; } // FRunnable interface virtual uint32 Run() override { ThreadCallback.ExecuteIfBound(); return 0; } void StartTestHttpThread(bool bBlockGameThread) { bBlockingGameThreadTick = bBlockGameThread; RunnableThread = TSharedPtr(FRunnableThread::Create(this, TEXT("Test Http Thread"))); while (bBlockingGameThreadTick) { float TickFrequency = 1.0f / 60; /*60 FPS*/; FPlatformProcess::Sleep(TickFrequency); } } void UnblockGameThread() { bBlockingGameThreadTick = false; } private: FRunActualTestCodeDelegate ThreadCallback; TSharedPtr RunnableThread; std::atomic bBlockingGameThreadTick = true; }; class FWaitThreadedHttpFixture : public FWaitUntilCompleteHttpFixture { public: ~FWaitThreadedHttpFixture() { WaitUntilAllHttpRequestsComplete(); } FThreadedHttpRunnable ThreadedHttpRunnable; }; TEST_CASE_METHOD(FWaitThreadedHttpFixture, "Http streaming download request can work in non game thread", HTTP_TAG) { ThreadedHttpRunnable.OnRunFromThread().BindLambda([this]() { TSharedRef HttpRequest = CreateRequest(); HttpRequest->SetURL(UrlStreamDownload(3/*Chunks*/, 1024/*ChunkSize*/)); HttpRequest->SetVerb(TEXT("GET")); HttpRequest->SetDelegateThreadPolicy(EHttpRequestDelegateThreadPolicy::CompleteOnHttpThread); class FTestHttpReceiveStream final : public FArchive { public: virtual void Serialize(void* V, int64 Length) override { // No matter what's the thread policy, Serialize always get called in http thread. CHECK(!IsInGameThread()); } }; CHECK(HttpRequest->SetResponseBodyReceiveStream(MakeShared())); HttpRequest->OnProcessRequestComplete().BindLambda([this](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { // EHttpRequestDelegateThreadPolicy::CompleteOnHttpThread was used, so not in game thread here CHECK(!IsInGameThread()); CHECK(bSucceeded); REQUIRE(HttpResponse != nullptr); CHECK(HttpResponse->GetResponseCode() == 200); CHECK(!HttpResponse->GetAllHeaders().IsEmpty()); ThreadedHttpRunnable.UnblockGameThread(); }); HttpRequest->ProcessRequest(); }); ThreadedHttpRunnable.StartTestHttpThread(true/*bBlockGameThread*/); } TEST_CASE_METHOD(FWaitThreadedHttpFixture, "Http download request progress callback can be received in http thread", HTTP_TAG) { std::atomic bRequestProgressTriggered = false; ThreadedHttpRunnable.OnRunFromThread().BindLambda([this, &bRequestProgressTriggered]() { TSharedRef HttpRequest = CreateRequest(); HttpRequest->SetURL(UrlStreamDownload(10/*Chunks*/, 1024*1024/*ChunkSize*/)); HttpRequest->SetVerb(TEXT("GET")); HttpRequest->SetDelegateThreadPolicy(EHttpRequestDelegateThreadPolicy::CompleteOnHttpThread); HttpRequest->OnRequestProgress64().BindLambda([this, &bRequestProgressTriggered](FHttpRequestPtr Request, uint64 /*BytesSent*/, uint64 BytesReceived) { if (!bRequestProgressTriggered) { // Only do these checks once, because when http request complete, this callback also get triggered CHECK(BytesReceived > 0); CHECK(BytesReceived < 10/*Chunks*/ * 1024*1024/*ChunkSize*/); CHECK(!IsInGameThread()); CHECK(Request->GetStatus() == EHttpRequestStatus::Processing); bRequestProgressTriggered = true; } }); HttpRequest->OnProcessRequestComplete().BindLambda([this](FHttpRequestPtr /*HttpRequest*/, FHttpResponsePtr /*HttpResponse */, bool bSucceeded) { CHECK(bSucceeded); ThreadedHttpRunnable.UnblockGameThread(); }); HttpRequest->ProcessRequest(); }); ThreadedHttpRunnable.StartTestHttpThread(true/*bBlockGameThread*/); CHECK(bRequestProgressTriggered); } TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Http request pre check will fail", HTTP_TAG) { DisableWarningsInThisTest(); TSharedRef HttpRequest = HttpModule->CreateRequest(); SECTION("when verb was set to empty") { HttpRequest->SetURL(UrlToTestMethods()); HttpRequest->SetVerb(TEXT("")); } SECTION("when url protocol is not valid") { HttpRequest->SetURL("http_abc://www.epicgames.com"); HttpRequest->SetVerb(TEXT("GET")); } SECTION("when url was not set") { HttpRequest->SetVerb(TEXT("GET")); } HttpRequest->OnProcessRequestComplete().BindLambda([](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(!bSucceeded); }); HttpRequest->ProcessRequest(); } class FValidateHeaderReceiveOrderFixture : public FWaitUntilCompleteHttpFixture { public: ~FValidateHeaderReceiveOrderFixture() { WaitUntilAllHttpRequestsComplete(); } std::atomic bHeaderReceived = false; std::atomic bCompleteCallbackTriggered = false; std::atomic bAnyDataReceived = false; }; TEST_CASE_METHOD(FValidateHeaderReceiveOrderFixture, "Http request header received callback will be called by thread policy", HTTP_TAG) { TSharedRef HttpRequest = CreateRequest(); HttpRequest->SetURL(UrlStreamDownload(2/*Chunks*/, 1024/*ChunkSize*/)); HttpRequest->SetVerb(TEXT("GET")); FHttpRequestStreamDelegateV2 StreamDelegate; StreamDelegate.BindLambda([this](void *InDataPtr, int64& InLength) { bAnyDataReceived = true; CHECK(!bCompleteCallbackTriggered); }); HttpRequest->SetResponseBodyReceiveStreamDelegateV2(StreamDelegate); SECTION("in http thread") { HttpRequest->SetDelegateThreadPolicy(EHttpRequestDelegateThreadPolicy::CompleteOnHttpThread); HttpRequest->OnHeaderReceived().BindLambda([this](FHttpRequestPtr Request, const FString& HeaderName, const FString& HeaderValue) { CHECK(!bAnyDataReceived); CHECK(!bCompleteCallbackTriggered); CHECK(!IsInGameThread()); bHeaderReceived = true; }); } SECTION("in game thread") { HttpRequest->SetDelegateThreadPolicy(EHttpRequestDelegateThreadPolicy::CompleteOnGameThread); HttpRequest->OnHeaderReceived().BindLambda([this](FHttpRequestPtr Request, const FString& HeaderName, const FString& HeaderValue) { // Data received delegate always triggered from http thread, so it could have been received, while header will be received // from game thread in this test section // CHECK(!bAnyDataReceived); CHECK(!bCompleteCallbackTriggered); CHECK(IsInGameThread()); bHeaderReceived = true; }); } HttpRequest->OnProcessRequestComplete().BindLambda([this](FHttpRequestPtr /*HttpRequest*/, FHttpResponsePtr /*HttpResponse */, bool bSucceeded) { CHECK(bAnyDataReceived); CHECK(bHeaderReceived); bCompleteCallbackTriggered = true; CHECK(bSucceeded); }); HttpRequest->ProcessRequest(); } class FValidateStatusCodeReceiveOrderFixture : public FWaitUntilCompleteHttpFixture { public: ~FValidateStatusCodeReceiveOrderFixture() { WaitUntilAllHttpRequestsComplete(); } std::atomic bStatusCodeReceived = false; std::atomic bCompleteCallbackTriggered = false; }; TEST_CASE_METHOD(FValidateStatusCodeReceiveOrderFixture, "Http request status code received callback will be called by thread policy", HTTP_TAG) { TSharedRef HttpRequest = CreateRequest(); HttpRequest->SetURL(UrlStreamDownload(20/*Chunks*/, 1024*1024/*ChunkSize*/)); HttpRequest->SetVerb(TEXT("GET")); SECTION("in http thread") { HttpRequest->SetDelegateThreadPolicy(EHttpRequestDelegateThreadPolicy::CompleteOnHttpThread); HttpRequest->OnStatusCodeReceived().BindLambda([this](FHttpRequestPtr Request, int32 StatusCode) { CHECK(StatusCode == 200); CHECK(!bCompleteCallbackTriggered); CHECK(!IsInGameThread()); bStatusCodeReceived = true; }); } SECTION("in game thread") { HttpRequest->SetDelegateThreadPolicy(EHttpRequestDelegateThreadPolicy::CompleteOnGameThread); HttpRequest->OnStatusCodeReceived().BindLambda([this](FHttpRequestPtr Request, int32 StatusCode) { CHECK(StatusCode == 200); CHECK(!bCompleteCallbackTriggered); CHECK(IsInGameThread()); bStatusCodeReceived = true; }); } HttpRequest->OnProcessRequestComplete().BindLambda([this](FHttpRequestPtr /*HttpRequest*/, FHttpResponsePtr /*HttpResponse */, bool bSucceeded) { CHECK(bStatusCodeReceived); bCompleteCallbackTriggered = true; CHECK(bSucceeded); }); HttpRequest->ProcessRequest(); } namespace UE { namespace TestHttp { void SetupURLRequestFilter(FHttpModule* HttpModule) { // Pre check will fail when domain is not allowed UE::Core::FURLRequestFilter::FRequestMap SchemeMap; SchemeMap.Add(TEXT("http"), TArray{TEXT("epicgames.com")}); UE::Core::FURLRequestFilter Filter{SchemeMap}; HttpModule->GetHttpManager().SetURLRequestFilter(Filter); } } } // Pre-check failed requests won't be added into http manager, so it can't rely on the requested added/completed callback in FWaitUntilCompleteHttpFixture TEST_CASE_METHOD(FWaitUntilQuitFromTestFixture, "Http request pre check will fail by thread policy", HTTP_TAG) { DisableWarningsInThisTest(); // Pre check will fail when domain is not allowed UE::TestHttp::SetupURLRequestFilter(HttpModule); TSharedRef HttpRequest = CreateRequest(); HttpRequest->SetVerb(TEXT("GET")); HttpRequest->SetURL(UrlToTestMethods()); SECTION("on game thread") { HttpRequest->OnProcessRequestComplete().BindLambda([this](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(IsInGameThread()); CHECK(!bSucceeded); bQuitRequested = true; }); } SECTION("on http thread") { HttpRequest->SetDelegateThreadPolicy(EHttpRequestDelegateThreadPolicy::CompleteOnHttpThread); HttpRequest->OnProcessRequestComplete().BindLambda([this](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(!IsInGameThread()); CHECK(!bSucceeded); bQuitRequested = true; }); } HttpRequest->ProcessRequest(); } class FWaitUntilQuitFromTestThreadedFixture : public FWaitUntilQuitFromTestFixture { public: ~FWaitUntilQuitFromTestThreadedFixture() { WaitUntilQuitFromTest(); } FThreadedHttpRunnable ThreadedHttpRunnable; }; // Pre-check failed requests won't be added into http manager, so it can't rely on the requested added/completed callback in FWaitUntilCompleteHttpFixture TEST_CASE_METHOD(FWaitUntilQuitFromTestThreadedFixture, "Threaded http request pre check will fail by thread policy", HTTP_TAG) { DisableWarningsInThisTest(); ThreadedHttpRunnable.OnRunFromThread().BindLambda([this]() { // Pre check will fail when domain is not allowed UE::TestHttp::SetupURLRequestFilter(HttpModule); TSharedRef HttpRequest = CreateRequest(); HttpRequest->SetVerb(TEXT("GET")); HttpRequest->SetURL(UrlToTestMethods()); SECTION("on game thread") { HttpRequest->OnProcessRequestComplete().BindLambda([this](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(IsInGameThread()); CHECK(!bSucceeded); bQuitRequested = true; }); } SECTION("on http thread") { HttpRequest->SetDelegateThreadPolicy(EHttpRequestDelegateThreadPolicy::CompleteOnHttpThread); HttpRequest->OnProcessRequestComplete().BindLambda([this](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(!IsInGameThread()); CHECK(!bSucceeded); bQuitRequested = true; }); } HttpRequest->ProcessRequest(); }); ThreadedHttpRunnable.StartTestHttpThread(false/*bBlockGameThread*/); } TEST_CASE_METHOD(FWaitUntilQuitFromTestFixture, "Cancel http request without ProcessRequest called", HTTP_TAG) { TSharedRef HttpRequest = CreateRequest(); HttpRequest->SetURL(UrlToTestMethods()); ++ExpectingExtraCallbacks; HttpRequest->OnProcessRequestComplete().BindLambda([this](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { --ExpectingExtraCallbacks; CHECK(!bSucceeded); CHECK(HttpRequest->GetFailureReason() == EHttpFailureReason::Cancelled); bQuitRequested = true; }); HttpRequest->CancelRequest(); } TEST_CASE_METHOD(FWaitThreadedHttpFixture, "Cancel http request with ProcessRequest called but before started from queue", HTTP_TAG) { CVarHttpMaxConcurrentRequests->Set(1); std::atomic bFirstRequestCompleted = false; FHttpManager& HttpManager = HttpModule->GetHttpManager(); const FHttpStats HttpStats = HttpManager.GetHttpStats(); CHECK(HttpStats.RequestsInQueue == 0); CHECK(HttpStats.MaxRequestsInQueue == 0); ThreadedHttpRunnable.OnRunFromThread().BindLambda([this, &bFirstRequestCompleted]() { TSharedRef HttpRequestRunning = CreateRequest(); HttpRequestRunning->SetURL(UrlStreamDownload(3/*Chunks*/, HTTP_TEST_TIMEOUT_CHUNK_SIZE, 1/*ChunkLatency*/)); HttpRequestRunning->SetDelegateThreadPolicy(EHttpRequestDelegateThreadPolicy::CompleteOnHttpThread); HttpRequestRunning->OnProcessRequestComplete().BindLambda([this, &bFirstRequestCompleted](FHttpRequestPtr HttpRequestQueuing, FHttpResponsePtr HttpResponse, bool bSucceeded) { bFirstRequestCompleted = true; ThreadedHttpRunnable.UnblockGameThread(); }); HttpRequestRunning->ProcessRequest(); TSharedRef HttpRequestQueuing = CreateRequest(); HttpRequestQueuing->SetURL(UrlStreamDownload(3/*Chunks*/, HTTP_TEST_TIMEOUT_CHUNK_SIZE, 1/*ChunkLatency*/)); HttpRequestQueuing->SetDelegateThreadPolicy(EHttpRequestDelegateThreadPolicy::CompleteOnHttpThread); HttpRequestQueuing->OnHeaderReceived().BindLambda([this](FHttpRequestPtr Request, const FString& HeaderName, const FString& HeaderValue) { // Should never be started CHECK(false); }); HttpRequestQueuing->OnRequestProgress64().BindLambda([this](FHttpRequestPtr Request, uint64 /*BytesSent*/, uint64 BytesReceived) { // Should never be started CHECK(false); }); ++ExpectingExtraCallbacks; HttpRequestQueuing->OnProcessRequestComplete().BindLambda([this, &bFirstRequestCompleted](FHttpRequestPtr HttpRequestQueuing, FHttpResponsePtr HttpResponse, bool bSucceeded) { --ExpectingExtraCallbacks; CHECK(!bSucceeded); CHECK(HttpRequestQueuing->GetFailureReason() == EHttpFailureReason::Cancelled); CHECK(!bFirstRequestCompleted); }); HttpRequestQueuing->ProcessRequest(); FPlatformProcess::Sleep(1); // Make sure the first request started FHttpManager& HttpManager = HttpModule->GetHttpManager(); const FHttpStats HttpStats = HttpManager.GetHttpStats(); CHECK(HttpStats.RequestsInQueue == 1); CHECK(HttpStats.MaxRequestsInQueue == 1); HttpRequestQueuing->CancelRequest(); }); ThreadedHttpRunnable.StartTestHttpThread(true/*bBlockGameThread*/); } TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Cancel http request connect before timeout", HTTP_TAG) { DisableWarningsInThisTest(); TSharedRef HttpRequest = CreateRequest(); SetInvalidDomainToTestConnectTimeout(); HttpRequest->SetURL(UrlToTestMethods()); HttpRequest->SetVerb(TEXT("GET")); HttpRequest->SetTimeout(7); const double StartTime = FPlatformTime::Seconds(); ++ExpectingExtraCallbacks; HttpRequest->OnProcessRequestComplete().BindLambda([this, StartTime](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { --ExpectingExtraCallbacks; CHECK(!bSucceeded); const double DurationInSeconds = FPlatformTime::Seconds() - StartTime; CHECK(DurationInSeconds < 2.0); CHECK(HttpRequest->GetFailureReason() == EHttpFailureReason::Cancelled); }); SECTION("ProcessRequest called") { HttpRequest->ProcessRequest(); FPlatformProcess::Sleep(0.5); } SECTION("ProcessRequest not called") { } HttpRequest->CancelRequest(); HttpRequest->CancelRequest(); // Duplicated calls to CancelRequest should be fine } TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Retry respect Retry-After header in response", HTTP_TAG) { if (!bRetryEnabled) { return; } DisableWarningsInThisTest(); TSharedRef HttpRequest = HttpRetryManager->CreateRequest( 1/*InRetryLimitCountOverride*/, FHttpRetrySystem::FRetryTimeoutRelativeSecondsSetting()/*InRetryTimeoutRelativeSecondsOverride unused*/, {EHttpResponseCodes::TooManyRequests, EHttpResponseCodes::ServiceUnavail}/*InRetryResponseCodes*/ ); SECTION("TooManyRequests") { HttpRequest->SetURL(UrlMockStatus(EHttpResponseCodes::TooManyRequests)); } uint32 RetryAfter = 4; HttpRequest->SetVerb(TEXT("GET")); HttpRequest->SetHeader(TEXT("Retry-After"), FString::Format(TEXT("{0}"), { RetryAfter })); // Will be forwarded back in response ++ExpectingExtraCallbacks; HttpRequest->OnRequestWillRetry().BindLambda([this, RetryAfter](FHttpRequestPtr /*Request*/, FHttpResponsePtr /*Response*/, float LockoutPeriod) { --ExpectingExtraCallbacks; CHECK(FMath::IsNearlyEqual(LockoutPeriod, (float)(RetryAfter))); }); const double StartTime = FPlatformTime::Seconds(); HttpRequest->OnProcessRequestComplete().BindLambda([RetryAfter, StartTime](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(bSucceeded); const double DurationInSeconds = FPlatformTime::Seconds() - StartTime; CHECK(FMath::IsNearlyEqual(DurationInSeconds, (float)(RetryAfter), HTTP_TIME_DIFF_TOLERANCE_OF_REQUEST)); }); HttpRequest->ProcessRequest(); } TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Request can time out during lock out", HTTP_TAG) { if (!bRetryEnabled) { return; } DisableWarningsInThisTest(); EHttpRequestDelegateThreadPolicy ThreadPolicyExpected = EHttpRequestDelegateThreadPolicy::CompleteOnGameThread; SECTION("From game thread") { } SECTION("From http thread") { ThreadPolicyExpected = EHttpRequestDelegateThreadPolicy::CompleteOnHttpThread; } TSharedRef HttpRequest = HttpRetryManager->CreateRequest( 1/*InRetryLimitCountOverride*/, FHttpRetrySystem::FRetryTimeoutRelativeSecondsSetting()/*InRetryTimeoutRelativeSecondsOverride unused*/, {EHttpResponseCodes::TooManyRequests}/*InRetryResponseCodes*/ ); HttpRequest->SetURL(UrlMockStatus(EHttpResponseCodes::TooManyRequests)); HttpRequest->SetTimeout(1.0f); HttpRequest->SetDelegateThreadPolicy(ThreadPolicyExpected); uint32 RetryAfter = 4; // Will be forwarded back in response HttpRequest->SetHeader(TEXT("Retry-After"), FString::Format(TEXT("{0}"), { RetryAfter })); const double StartTime = FPlatformTime::Seconds(); HttpRequest->OnProcessRequestComplete().BindLambda([StartTime, ThreadPolicyExpected](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { // When timeout during lock out period, it fails with result of last request before lock out CHECK(bSucceeded); REQUIRE(HttpResponse != nullptr); CHECK(HttpResponse->GetFailureReason() == EHttpFailureReason::None); CHECK(HttpResponse->GetResponseCode() == EHttpResponseCodes::TooManyRequests); CHECK(HttpResponse->GetContentLength() > 0); const double DurationInSeconds = FPlatformTime::Seconds() - StartTime; CHECK(FMath::IsNearlyEqual(DurationInSeconds, 1.0, HTTP_TIME_DIFF_TOLERANCE_OF_REQUEST)); CHECK((ThreadPolicyExpected == EHttpRequestDelegateThreadPolicy::CompleteOnGameThread && IsInGameThread() || ThreadPolicyExpected == EHttpRequestDelegateThreadPolicy::CompleteOnHttpThread && !IsInGameThread())); }); HttpRequest->ProcessRequest(); } TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Request can time out during retry request", HTTP_TAG) { if (!bRetryEnabled) { return; } DisableWarningsInThisTest(); TSharedRef HttpRequest = HttpRetryManager->CreateRequest( 1/*InRetryLimitCountOverride*/, FHttpRetrySystem::FRetryTimeoutRelativeSecondsSetting()/*InRetryTimeoutRelativeSecondsOverride unused*/, {EHttpResponseCodes::TooManyRequests}/*InRetryResponseCodes*/ ); HttpRequest->SetURL(UrlMockStatus(EHttpResponseCodes::TooManyRequests)); HttpRequest->SetTimeout(3.0f); uint32 RetryAfter = 2; // Will be forwarded back in response HttpRequest->SetHeader(TEXT("Retry-After"), FString::Format(TEXT("{0}"), { RetryAfter })); ++ExpectingExtraCallbacks; HttpRequest->OnRequestWillRetry().BindLambda([this](FHttpRequestPtr Request, FHttpResponsePtr /*Response*/, float LockoutPeriod) { --ExpectingExtraCallbacks; // Now retry with a latency during request Request->SetURL(UrlStreamDownload(3/*Chunks*/, HTTP_TEST_TIMEOUT_CHUNK_SIZE, 2/*ChunkLatency*/)); }); const double StartTime = FPlatformTime::Seconds(); HttpRequest->OnProcessRequestComplete().BindLambda([StartTime](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(HttpRequest->GetStatus() == EHttpRequestStatus::Failed); CHECK(HttpRequest->GetFailureReason() == EHttpFailureReason::TimedOut); // When timeout during retrying request, it fails with result of last request before retrying, to // keep it the same behavior when timeout during lockout CHECK(bSucceeded); REQUIRE(HttpResponse != nullptr); CHECK(HttpResponse->GetFailureReason() == EHttpFailureReason::None); CHECK(HttpResponse->GetResponseCode() == EHttpResponseCodes::TooManyRequests); CHECK(HttpResponse->GetContentLength() > 0); const double DurationInSeconds = FPlatformTime::Seconds() - StartTime; CHECK(FMath::IsNearlyEqual(DurationInSeconds, 3.0, HTTP_TIME_DIFF_TOLERANCE_OF_REQUEST)); }); HttpRequest->ProcessRequest(); } TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Request will not retry", HTTP_TAG) { if (!bRetryEnabled) { return; } DisableWarningsInThisTest(); TSharedRef HttpRequest = HttpRetryManager->CreateRequest(1/*InRetryLimitCountOverride*/); SECTION("When response code is not listed for retry") { HttpRequest->SetURL(UrlMockStatus(EHttpResponseCodes::TooManyRequests)); // Will be forwarded back in response HttpRequest->SetHeader(TEXT("Retry-After"), FString::Format(TEXT("{0}"), { 2 })); } SECTION("When there is any response and timed out during streaming download") { HttpRequest->SetURL(UrlStreamDownload(3/*Chunks*/, HTTP_TEST_TIMEOUT_CHUNK_SIZE, 2/*ChunkLatency*/)); HttpRequest->SetTimeout(3.0f); HttpRequest->OnProcessRequestComplete().BindLambda([](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(HttpRequest->GetStatus() == EHttpRequestStatus::Failed); CHECK(HttpRequest->GetFailureReason() == EHttpFailureReason::TimedOut); }); } HttpRequest->OnRequestWillRetry().BindLambda([this](FHttpRequestPtr Request, FHttpResponsePtr Response, float LockoutPeriod) { CHECK(false); }); HttpRequest->ProcessRequest(); } TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Retry immediately without lock out if connect failed and there are alt domains", HTTP_TAG) { if (!bRetryEnabled) { return; } DisableWarningsInThisTest(); HttpModule->HttpConnectionTimeout = 1.0f; TArray AltDomains; FString CorrectDomain = UrlDomain(); AltDomains.Add(CorrectDomain); FHttpRetrySystem::FRetryDomainsPtr RetryDomains = MakeShared(MoveTemp(AltDomains)); TSharedRef HttpRequest = HttpRetryManager->CreateRequest( 1/*InRetryLimitCountOverride*/, FHttpRetrySystem::FRetryTimeoutRelativeSecondsSetting()/*InRetryTimeoutRelativeSecondsOverride unused*/, { EHttpResponseCodes::TooManyRequests, EHttpResponseCodes::ServiceUnavail }/*InRetryResponseCodes*/, FHttpRetrySystem::FRetryVerbs(), RetryDomains ); SetInvalidDomainToTestConnectTimeout(); HttpRequest->SetURL(UrlToTestMethods()); HttpRequest->SetVerb(TEXT("GET")); ++ExpectingExtraCallbacks; HttpRequest->OnRequestWillRetry().BindLambda([this](FHttpRequestPtr /*Request*/, FHttpResponsePtr /*Response*/, float LockoutPeriod) { --ExpectingExtraCallbacks; CHECK(LockoutPeriod == 0); }); HttpRequest->OnProcessRequestComplete().BindLambda([](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(bSucceeded); REQUIRE(HttpResponse != nullptr); CHECK(HttpResponse->GetResponseCode() == 200); }); HttpRequest->ProcessRequest(); } TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Retry request with CompleteOnHttpThread policy won't add same duplicated request to http manager", HTTP_TAG) { if (!bRetryEnabled) { return; } DisableWarningsInThisTest(); HttpModule->HttpConnectionTimeout = 1.0f; FString OriginalUrl; TArray AltDomains; FString CorrectDomain = UrlDomain(); SECTION("With invalid domain") { SetInvalidDomainToTestConnectTimeout(); OriginalUrl = UrlToTestMethods(); } SECTION("With mock connection failure for valid domain") { CVarHttpUrlPatternsToMockFailure->Set(TEXT("www.epicgames.com->0")); OriginalUrl = UrlToTestMethods(); OriginalUrl = OriginalUrl.Replace(*UrlDomain(), TEXT("www.epicgames.com")); } AltDomains.Add(CorrectDomain); FHttpRetrySystem::FRetryDomainsPtr RetryDomains = MakeShared(MoveTemp(AltDomains)); TSharedRef HttpRequest = HttpRetryManager->CreateRequest( 1/*InRetryLimitCountOverride*/, FHttpRetrySystem::FRetryTimeoutRelativeSecondsSetting()/*InRetryTimeoutRelativeSecondsOverride unused*/, { EHttpResponseCodes::TooManyRequests, EHttpResponseCodes::ServiceUnavail }/*InRetryResponseCodes*/, FHttpRetrySystem::FRetryVerbs(), RetryDomains ); HttpRequest->SetURL(OriginalUrl); HttpRequest->SetVerb(TEXT("GET")); HttpRequest->SetDelegateThreadPolicy(EHttpRequestDelegateThreadPolicy::CompleteOnHttpThread); ++ExpectingExtraCallbacks; HttpRequest->OnRequestWillRetry().BindLambda([this](FHttpRequestPtr /*Request*/, FHttpResponsePtr /*Response*/, float LockoutPeriod) { --ExpectingExtraCallbacks; CHECK(LockoutPeriod == 0); }); HttpRequest->OnProcessRequestComplete().BindLambda([](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(bSucceeded); REQUIRE(HttpResponse != nullptr); CHECK(HttpResponse->GetResponseCode() == 200); }); HttpRequest->ProcessRequest(); CVarHttpUrlPatternsToMockFailure->Set(TEXT("")); } TEST_CASE_METHOD(FWaitUntilQuitFromTestFixture, "Will keep request reference with mock connection failure which using CompleteOnHttpThread policy until mocked connection timeout", HTTP_TAG) { DisableWarningsInThisTest(); CVarHttpUrlPatternsToMockFailure->Set(TEXT("www.epicgames.com->0")); HttpModule->HttpConnectionTimeout = 1.0f; TSharedRef HttpRequest = CreateRequest(); HttpRequest->SetURL(TEXT("https://www.epicgames.com")); HttpRequest->SetDelegateThreadPolicy(EHttpRequestDelegateThreadPolicy::CompleteOnHttpThread); ++ExpectingExtraCallbacks; HttpRequest->OnProcessRequestComplete().BindLambda([this](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { --ExpectingExtraCallbacks; CHECK(!bSucceeded); REQUIRE(HttpResponse == nullptr); bQuitRequested = true; }); HttpRequest->ProcessRequest(); CVarHttpUrlPatternsToMockFailure->Set(TEXT("")); } TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Retry will add default domain to alt domains", HTTP_TAG) { if (!bRetryEnabled) { return; } DisableWarningsInThisTest(); CVarHttpUrlPatternsToMockFailure->Set(TEXT("fakedomain->0")); HttpModule->HttpConnectionTimeout = 0.1f; TQueue* ExpectedDomainsOrderInRetry = new TQueue(); TArray AltDomains; FString DefautUrl = TEXT("https://www.fakedomain-default.com/webtests/httptests"); ExpectedDomainsOrderInRetry->Enqueue(TEXT("www.fakedomain-a.com")); ExpectedDomainsOrderInRetry->Enqueue(TEXT("www.fakedomain-b.com")); ExpectedDomainsOrderInRetry->Enqueue(TEXT("www.fakedomain-default.com")); ExpectedDomainsOrderInRetry->Enqueue(TEXT("www.fakedomain-a.com")); SECTION("When default domain is not listed in AltDomains") { AltDomains.Add(TEXT("www.fakedomain-a.com")); AltDomains.Add(TEXT("www.fakedomain-b.com")); } SECTION("When default domain configured as first in AltDomains, retry system will keep the order in the array intead of adding it") { AltDomains.Add(TEXT("www.fakedomain-default.com")); AltDomains.Add(TEXT("www.fakedomain-a.com")); AltDomains.Add(TEXT("www.fakedomain-b.com")); } SECTION("When default domain configured in the middle of AltDomains, retry system will move it to first in the array") { AltDomains.Add(TEXT("www.fakedomain-a.com")); AltDomains.Add(TEXT("www.fakedomain-default.com")); AltDomains.Add(TEXT("www.fakedomain-b.com")); } SECTION("When default domain configured in the end of AltDomains, retry system will move it to first in the array") { AltDomains.Add(TEXT("www.fakedomain-a.com")); AltDomains.Add(TEXT("www.fakedomain-b.com")); AltDomains.Add(TEXT("www.fakedomain-default.com")); } FHttpRetrySystem::FRetryDomainsPtr RetryDomains = MakeShared(MoveTemp(AltDomains)); TSharedRef HttpRequest = HttpRetryManager->CreateRequest( 4/*InRetryLimitCountOverride*/, FHttpRetrySystem::FRetryTimeoutRelativeSecondsSetting()/*InRetryTimeoutRelativeSecondsOverride unused*/, { EHttpResponseCodes::TooManyRequests }/*InRetryResponseCodes*/, FHttpRetrySystem::FRetryVerbs(), RetryDomains ); HttpRequest->SetURL(DefautUrl); ExpectingExtraCallbacks = 4; HttpRequest->OnRequestWillRetry().BindLambda([this, ExpectedDomainsOrderInRetry](FHttpRequestPtr HttpRequest, FHttpResponsePtr /*Response*/, float LockoutPeriod) { --ExpectingExtraCallbacks; CHECK(LockoutPeriod == 0); FString ExpectedDomain; ExpectedDomainsOrderInRetry->Dequeue(ExpectedDomain); FString ActualDomain = FPlatformHttp::GetUrlDomain(HttpRequest->GetURL()); CHECK(ExpectedDomain == ActualDomain); }); HttpRequest->OnProcessRequestComplete().BindLambda([ExpectedDomainsOrderInRetry](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(ExpectedDomainsOrderInRetry->IsEmpty()); delete ExpectedDomainsOrderInRetry; CVarHttpUrlPatternsToMockFailure->Set(TEXT("")); CHECK(!bSucceeded); REQUIRE(HttpResponse == nullptr); }); HttpRequest->ProcessRequest(); } TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Optionally retry limit can be set differently for connection error", HTTP_TAG) { if (!bRetryEnabled) { return; } DisableWarningsInThisTest(); HttpModule->HttpConnectionTimeout = 1.0f; FHttpRetrySystem::FExponentialBackoffCurve RetryBackoffCurve; RetryBackoffCurve.MinCoefficient = 1.0f; // no jitter TSharedRef HttpRequest = HttpRetryManager->CreateRequest( 3/*InRetryLimitCountOverride*/, FHttpRetrySystem::FRetryTimeoutRelativeSecondsSetting()/*InRetryTimeoutRelativeSecondsOverride unused*/, { EHttpResponseCodes::TooManyRequests, EHttpResponseCodes::ServiceUnavail }/*InRetryResponseCodes*/, FHttpRetrySystem::FRetryVerbs(), /*unused*/ FHttpRetrySystem::FRetryDomainsPtr(), /*unused*/ 1, /*InRetryLimitCountForConnectionErrorOverride*/ RetryBackoffCurve/*InExponentialBackoffCurve*/ ); float ExpectedTimeoutDuration = 0.0f; float TimeDiffTolerance = 0.0f; SECTION("RetryLimitCountForConnectionErrorDefault:1 will be used so retries for connection error take less time") { SetInvalidDomainToTestConnectTimeout(); HttpRequest->SetURL(UrlMockStatus(EHttpResponseCodes::TooManyRequests)); ExpectedTimeoutDuration = 6.0f; // each request will take 1s, 1st retry back off takes 4s TimeDiffTolerance = 2 * UE_HTTP_CONNECTION_TIMEOUT_MAX_DEVIATION; } SECTION("RetryLimitCountDefault:3 will be used so retries in general take long") { HttpRequest->SetURL(UrlMockStatus(EHttpResponseCodes::TooManyRequests)); HttpRequest->SetHeader(TEXT("Retry-After"), FString::Format(TEXT("{0}"), { 3 })); ExpectedTimeoutDuration = 9.0f; // each request will take 0s, 3 retry back offs, each back off takes 3s; TimeDiffTolerance = 3 * HTTP_TIME_DIFF_TOLERANCE_OF_REQUEST; } const double StartTime = FPlatformTime::Seconds(); HttpRequest->OnProcessRequestComplete().BindLambda([StartTime, ExpectedTimeoutDuration, TimeDiffTolerance](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { const double DurationInSeconds = FPlatformTime::Seconds() - StartTime; CHECK(FMath::IsNearlyEqual(DurationInSeconds, ExpectedTimeoutDuration, TimeDiffTolerance)); }); HttpRequest->ProcessRequest(); } TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Retry fallback with exponential lock out if there is no Retry-After header", HTTP_TAG) { if (!bRetryEnabled) { return; } DisableWarningsInThisTest(); TSharedRef HttpRequest = HttpRetryManager->CreateRequest( 2/*InRetryLimitCountOverride*/, FHttpRetrySystem::FRetryTimeoutRelativeSecondsSetting()/*InRetryTimeoutRelativeSecondsOverride unused*/, {EHttpResponseCodes::TooManyRequests}/*InRetryResponseCodes*/ ); HttpRequest->SetURL(UrlMockStatus(EHttpResponseCodes::TooManyRequests)); HttpRequest->SetVerb(TEXT("GET")); ExpectingExtraCallbacks = 2; HttpRequest->OnRequestWillRetry().BindLambda([this](FHttpRequestPtr Request, FHttpResponsePtr /*Response*/, float LockoutPeriod) { --ExpectingExtraCallbacks; // Default value in FExponentialBackoffCurve Compute(1) is 4 with default value in FBackoffJitterCoefficient applied CHECK(LockoutPeriod >= (4 * 0.5f)); CHECK(LockoutPeriod <= (4 * 1.0f)); Request->OnRequestWillRetry().BindLambda([this](FHttpRequestPtr /*Request*/, FHttpResponsePtr /*Response*/, float LockoutPeriod) { --ExpectingExtraCallbacks; // Default value in FExponentialBackoffCurve Compute(2) is 8 with default value in FBackoffJitterCoefficient applied CHECK(LockoutPeriod >= (8 * 0.5f)); CHECK(LockoutPeriod <= (8 * 1.0f)); }); }); HttpRequest->ProcessRequest(); } TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Dead lock test by retrying requests while completing requests", HTTP_TAG) { if (!bRetryEnabled) { return; } DisableWarningsInThisTest(); for (uint32 i = 0; i < 50; ++i) { TSharedRef HttpRequest = HttpRetryManager->CreateRequest( 5/*InRetryLimitCountOverride*/, FHttpRetrySystem::FRetryTimeoutRelativeSecondsSetting()/*InRetryTimeoutRelativeSecondsOverride unused*/, { EHttpResponseCodes::TooManyRequests }/*InRetryResponseCodes*/ ); HttpRequest->SetURL(UrlMockStatus(EHttpResponseCodes::TooManyRequests)); HttpRequest->SetHeader(TEXT("Retry-After"), FString::Format(TEXT("{0}"), { 0.1 })); HttpRequest->ProcessRequest(); } } class FThreadedBatchRequestsFixture : public FWaitThreadedHttpFixture { public: void LaunchBatchRequests(uint32 BatchSize) { for (uint32 i = 0; i < BatchSize; ++i) { TSharedRef HttpRequest = CreateRequest(); HttpRequest->SetURL(UrlStreamDownload(3, 1024*1024)); HttpRequest->SetVerb(TEXT("GET")); HttpRequest->ProcessRequest(); } } void BlockUntilFlushed() { if (bRetryEnabled) { HttpRetryManager->BlockUntilFlushed(5.0); } else { HttpModule->GetHttpManager().Flush(EHttpFlushReason::Default); } } }; TEST_CASE_METHOD(FThreadedBatchRequestsFixture, "Retry manager and http manager is thread safe for flushing", HTTP_TAG) { DisableWarningsInThisTest(); ThreadedHttpRunnable.OnRunFromThread().BindLambda([this]() { LaunchBatchRequests(10); BlockUntilFlushed(); }); ThreadedHttpRunnable.StartTestHttpThread(false/*bBlockGameThread*/); LaunchBatchRequests(10); BlockUntilFlushed(); } TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Flush while activity timeout shouldn't dead lock", HTTP_TAG) { DisableWarningsInThisTest(); HttpModule->HttpActivityTimeout = 2.0f; TSharedPtr HttpRequest = CreateRequest(); HttpRequest->SetURL(UrlStreamDownload(3/*Chunks*/, HTTP_TEST_TIMEOUT_CHUNK_SIZE, 5/*ChunkLatency*/)); HttpRequest->OnProcessRequestComplete().BindLambda([](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(HttpRequest->GetStatus() == EHttpRequestStatus::Failed); CHECK(HttpRequest->GetFailureReason() == EHttpFailureReason::ConnectionError); }); HttpRequest->ProcessRequest(); HttpModule->GetHttpManager().Flush(EHttpFlushReason::FullFlush); } TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "OnBeforeFork can clear http requests", HTTP_TAG) { // Warning: Sleeping 0.500s to wait for 1 outstanding Http Requests. DisableWarningsInThisTest(); { TSharedPtr HttpRequest = CreateRequest(); HttpRequest->SetURL(UrlStreamDownload(3/*Chunks*/, HTTP_TEST_TIMEOUT_CHUNK_SIZE, 0/*ChunkLatency*/)); HttpRequest->OnProcessRequestComplete().BindLambda([](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(bSucceeded); }); HttpRequest->ProcessRequest(); } CHECK(OngoingRequests == 1); HttpModule->GetHttpManager().OnBeforeFork(); CHECK(OngoingRequests == 0); HttpModule->GetHttpManager().OnAfterFork(); } // TODO: find a way to catch expected error log instead //TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Will trigger check if flush during http complete delegate in game thread", HTTP_TAG) //{ // TSharedPtr HttpRequest = CreateRequest(); // HttpRequest->SetURL(UrlToTestMethods()); // HttpRequest->OnProcessRequestComplete().BindLambda([this](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { // REQUIRE_CHECK_MSG("It's not supported to call Flush during http request complete delegate!", HttpModule->GetHttpManager().Flush(EHttpFlushReason::FullFlush)); // }); // HttpRequest->ProcessRequest(); //} // //TEST_CASE_METHOD(FWaitThreadedHttpFixture, "Will trigger check if flush during http complete delegate in http thread", HTTP_TAG) //{ // ThreadedHttpRunnable.OnRunFromThread().BindLambda([this]() { // TSharedPtr HttpRequest = CreateRequest(); // HttpRequest->SetURL(UrlToTestMethods()); // HttpRequest->OnProcessRequestComplete().BindLambda([this](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { // REQUIRE_CHECK_MSG("It's not supported to call Flush during http request complete delegate!", HttpModule->GetHttpManager().Flush(EHttpFlushReason::FullFlush)); // }); // HttpRequest->ProcessRequest(); // }); // // ThreadedHttpRunnable.StartTestHttpThread(/*bBlockGameThread=*/false); //} // //TEST_CASE_METHOD(FWaitThreadedHttpFixture, "Will NOT trigger check if flush from another thread during http complete delegate", HTTP_TAG) //{ // TSharedRef HttpRequest = CreateRequest(); // HttpRequest->SetURL(UrlToTestMethods()); // HttpRequest->OnProcessRequestComplete().BindLambda([this](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { // CHECK(bSucceeded); // FPlatformProcess::Sleep(1.0); // }); // HttpRequest->ProcessRequest(); // // ThreadedHttpRunnable.OnRunFromThread().BindLambda([this]() { // FPlatformProcess::Sleep(0.5); // Make sure the request reached OnProcessRequestComplete // HttpModule->GetHttpManager().Flush(EHttpFlushReason::FullFlush); // }); // ThreadedHttpRunnable.StartTestHttpThread(/*bBlockGameThread=*/false); //} TEST_CASE_METHOD(FWaitUntilQuitFromTestFixture, "Can disable failed log through CVar", HTTP_TAG) { // Only disable warning in this function scope for pre check warning log, but not disable warning when complete LOG_SCOPE_VERBOSITY_OVERRIDE(LogHttp, ELogVerbosity::Error); CVarHttpUrlPatternsToDisableFailedLog->Set(TEXT("unrealengine.com")); TSharedRef HttpRequest = CreateRequest(); HttpRequest->SetURL(TEXT("https://www.unrealengine.com/")); HttpRequest->SetVerb(TEXT("")); // Pre check will fail // Make sure when complete it's not in this function scope HttpRequest->SetDelegateThreadPolicy(EHttpRequestDelegateThreadPolicy::CompleteOnHttpThread); HttpRequest->OnProcessRequestComplete().BindLambda([this](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { bQuitRequested = true; CHECK(!bSucceeded); HttpModule->GetHttpManager().AddGameThreadTask([]() { // Have to clear CVar out of the test function scope, but also need to be in game thread. CVarHttpUrlPatternsToDisableFailedLog->Set(TEXT("")); }); }); HttpRequest->ProcessRequest(); } #if UE_HTTP_SUPPORT_LOCAL_SERVER TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Scheme besides http and https can work if allowed by settings", HTTP_TAG) { bool bShouldSucceed = false; SECTION("when allowed") { bShouldSucceed = true; } SECTION("when not allowed") { DisableWarningsInThisTest(); // Pre check will fail when scheme is not listed UE::TestHttp::SetupURLRequestFilter(HttpModule); } FString Filename = FString(FPlatformProcess::UserSettingsDir()) / TEXT("TestProtocolAllowed.dat"); UE::TestHttp::WriteTestFile(Filename, 10/*Bytes*/); TSharedRef HttpRequest = HttpModule->CreateRequest(); HttpRequest->SetURL(FString(TEXT("file://")) + Filename.Replace(TEXT(" "), TEXT("%20"))); HttpRequest->SetVerb(TEXT("GET")); HttpRequest->OnProcessRequestComplete().BindLambda([Filename, bShouldSucceed](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(bSucceeded == bShouldSucceed); IFileManager::Get().Delete(*Filename); }); HttpRequest->ProcessRequest(); } class FLocalHttpServerFixture : public FWaitUntilCompleteHttpFixture { public: FLocalHttpServerFixture() { HttpServerModule = new FHttpServerModule(); HttpServerModule->StartupModule(); HttpRouter = HttpServerModule->GetHttpRouter(LocalHttpServerPort); CHECK(HttpRouter.IsValid()); } void StartServerWithHandler(const FHttpPath& HttpPath, EHttpServerRequestVerbs Verb, FHttpRequestHandler RequestHandler) { CHECK(HttpRouteHandle == nullptr); HttpRouteHandle = HttpRouter->BindRoute(HttpPath, Verb, RequestHandler); HttpServerModule->StartAllListeners(); } ~FLocalHttpServerFixture() { while (HasOngoingRequest()) { HttpServerModule->Tick(TickFrequency); HttpModule->GetHttpManager().Tick(TickFrequency); FPlatformProcess::Sleep(TickFrequency); } HttpRouter->UnbindRoute(HttpRouteHandle); HttpRouter.Reset(); HttpServerModule->ShutdownModule(); delete HttpServerModule; } TSharedPtr HttpRouter; FHttpRouteHandle HttpRouteHandle; FHttpServerModule* HttpServerModule = nullptr; uint32 LocalHttpServerPort = 9000; }; TEST_CASE_METHOD(FLocalHttpServerFixture, "Local http server can serve large file", HTTP_TAG) { const uint32 FileSize = 100 * 1024 * 1024; // 100 MB seems good enough to repro SE_EWOULDBLOCK or SE_TRY_AGAIN on Mac StartServerWithHandler(FHttpPath(TEXT("/large_file")), EHttpServerRequestVerbs::VERB_GET, FHttpRequestHandler::CreateLambda([FileSize](const FHttpServerRequest& Request, const FHttpResultCallback& OnComplete) { TArray ResultData; ResultData.SetNum(FileSize); FMemory::Memset(ResultData.GetData(), 'd', FileSize); OnComplete(FHttpServerResponse::Create(MoveTemp(ResultData), TEXT("text/text"))); return true; })); // Start client request TSharedRef HttpRequest = CreateRequest(); HttpRequest->SetURL(TEXT("http://localhost:9000/large_file")); HttpRequest->SetVerb(TEXT("GET")); HttpRequest->OnProcessRequestComplete().BindLambda([FileSize](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(bSucceeded); REQUIRE(HttpResponse != nullptr); CHECK(HttpResponse->GetContentLength() == FileSize); }); HttpRequest->ProcessRequest(); } #endif // UE_HTTP_SUPPORT_LOCAL_SERVER TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Accessing request initial information without issue while request is running", HTTP_TAG) { for (int32 i = 0; i < 30; ++i) // Use 2 "for" loops so it doesn't trigger the warning the request waited for too long in the queue { TArray> Requests; for (int32 j = 0; j < 30; ++j) { TSharedRef HttpRequest = CreateRequest(); HttpRequest->SetHeader(TEXT("Custom-HeaderA"), TEXT("a")); HttpRequest->SetHeader(TEXT("Custom-HeaderB"), TEXT("b")); HttpRequest->SetHeader(TEXT("Custom-HeaderC"), TEXT("c")); HttpRequest->SetURL(UrlStreamDownload(3, HTTP_TEST_TIMEOUT_CHUNK_SIZE, 0)); HttpRequest->SetDelegateThreadPolicy(EHttpRequestDelegateThreadPolicy::CompleteOnHttpThread); HttpRequest->ProcessRequest(); Requests.Add(HttpRequest); } bool bRequestsStillRunning = true; while (bRequestsStillRunning) { bRequestsStillRunning = false; for (TSharedRef Request : Requests) { if (!EHttpRequestStatus::IsFinished(Request->GetStatus())) { bRequestsStillRunning = true; CHECK(!Request->GetAllHeaders().IsEmpty()); CHECK(!Request->GetURL().IsEmpty()); } } } } } TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Test platform request requests limits", HTTP_TAG "[LIMIT]") { bool bCheckCancel = GENERATE(false, true); int32 NumRequests = GENERATE(1, 10, 20, 50, 100, 200, 500, 1000); //Output NumRequests when error occurs. UNSCOPED_INFO(NumRequests); UNSCOPED_INFO(bCheckCancel); DYNAMIC_SECTION(" making " << NumRequests << " requests with bCheckCancel=" << bCheckCancel) { if (NumRequests > 50 && !bRunHeavyTests) { return; } TArray> Requests; for (int32 i = 0; i < NumRequests; ++i) { TSharedRef HttpRequest = FHttpModule::Get().CreateRequest(); // Requests server to serve 1024b chunks to allow time for cancel to happen HttpRequest->SetURL(UrlStreamDownload(3, HTTP_TEST_TIMEOUT_CHUNK_SIZE, /*ChunkLatency=*/bCheckCancel ? 1 : 0)); HttpRequest->SetVerb(TEXT("GET")); // Since catch2 uses std::srand, use std::rand here should make it deterministic when use same seed through --rng-seed if (std::rand() % 2) { HttpRequest->SetDelegateThreadPolicy(EHttpRequestDelegateThreadPolicy::CompleteOnHttpThread); } HttpRequest->OnProcessRequestComplete().BindLambda([bCheckCancel](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { //Only assert if response is successful on non-canceled requests if (!bCheckCancel) { CHECK(bSucceeded); CHECK(HttpResponse != nullptr); } }); HttpRequest->ProcessRequest(); Requests.Add(HttpRequest); } CHECK(Requests.Num() == NumRequests); if(bCheckCancel) { // Make sure requests are started in http thread FPlatformProcess::Sleep(0.1); for (auto Request : Requests) { Request->CancelRequest(); } } } } #if UE_HTTP_SUPPORT_UNIX_SOCKET TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Http Methods over Unix Domain Socket", HTTP_TAG) { if (WebServerUnixSocket.Len() == 0) { return; } TSharedRef HttpRequest = CreateRequest(); CHECK(HttpRequest->GetVerb() == TEXT("GET")); const int Number = FPlatformTime::Cycles(); HttpRequest->SetURL(FString::Format(TEXT("{0}/{1}"), { *UrlUnixSocketHttpTests(), Number })); HttpRequest->SetOption(HttpRequestOptions::UnixSocketPath, WebServerUnixSocket); SECTION("Default GET") { } SECTION("GET") { HttpRequest->SetVerb(TEXT("GET")); } SECTION("POST") { HttpRequest->SetVerb(TEXT("POST")); } SECTION("PUT") { HttpRequest->SetVerb(TEXT("PUT")); } SECTION("DELETE") { HttpRequest->SetVerb(TEXT("DELETE")); } HttpRequest->OnProcessRequestComplete().BindLambda([Number](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(bSucceeded); REQUIRE(HttpResponse != nullptr); CHECK(HttpResponse->GetResponseCode() == 200); FString ResponseContent = HttpResponse->GetContentAsString(); int NumberReturned = FCString::Atoi(*ResponseContent); CHECK(Number == NumberReturned); }); HttpRequest->ProcessRequest(); } #endif //UE_HTTP_SUPPORT_UNIX_SOCKET TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Http Methods over http2", HTTP_TAG) { TSharedRef HttpRequest = CreateRequest(); HttpRequest->SetURL(UrlToTestMethods()); HttpRequest->SetOption(HttpRequestOptions::HttpVersion, FHttpConstants::VERSION_2TLS); HttpRequest->OnProcessRequestComplete().BindLambda([](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(bSucceeded); REQUIRE(HttpResponse != nullptr); CHECK(HttpResponse->GetResponseCode() == 200); }); HttpRequest->ProcessRequest(); }