// Copyright Epic Games, Inc. All Rights Reserved. #include "CoreMinimal.h" #include "HAL/FileManager.h" #include "HAL/IConsoleManager.h" #include "HAL/PlatformProcess.h" #include "HAL/Runnable.h" #include "HAL/RunnableThread.h" #include "HttpManager.h" #include "Http.h" #include "Misc/CommandLine.h" #include "TestHarness.h" /** * 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 0.5f class FHttpModuleTestFixture { public: FHttpModuleTestFixture() : WebServerIp(TEXT("127.0.0.1")) { ParseSettingsFromCommandLine(); HttpModule = new FHttpModule(); IModuleInterface* Module = HttpModule; Module->StartupModule(); } virtual ~FHttpModuleTestFixture() { IModuleInterface* Module = HttpModule; Module->ShutdownModule(); delete Module; } void ParseSettingsFromCommandLine() { FParse::Value(FCommandLine::Get(), TEXT("web_server_ip"), WebServerIp); } const FString UrlWithInvalidPortToTestConnectTimeout() const { return FString::Format(TEXT("http://{0}:{1}"), { *WebServerIp, 8765 }); } const FString UrlBase() const { return FString::Format(TEXT("http://{0}:{1}"), { *WebServerIp, 8000 }); } 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) { return FString::Format(TEXT("{0}/streaming_download/{1}/{2}/"), { *UrlHttpTests(), Chunks, ChunkSize }); } FString WebServerIp; FHttpModule* HttpModule; }; TEST_CASE_METHOD(FHttpModuleTestFixture, "Shutdown http module without issue when there are ongoing http requests.", HTTP_TAG) { 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 = HttpModule->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); } class FWaitUntilQuitFromTestFixture : public FHttpModuleTestFixture { public: FWaitUntilQuitFromTestFixture() { } ~FWaitUntilQuitFromTestFixture() { WaitUntilAllHttpRequestsComplete(); } void WaitUntilAllHttpRequestsComplete() { while (!bQuitRequested) { HttpModule->GetHttpManager().Tick(TickFrequency); FPlatformProcess::Sleep(TickFrequency); } } float TickFrequency = 1.0f / 60; /*60 FPS*/; bool bQuitRequested = false; }; TEST_CASE_METHOD(FWaitUntilQuitFromTestFixture, "Http request can be reused", HTTP_TAG) { TSharedRef HttpRequest = HttpModule->CreateRequest(); HttpRequest->SetURL(UrlToTestMethods()); HttpRequest->SetVerb(TEXT("POST")); HttpRequest->OnProcessRequestComplete().BindLambda([this](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(bSucceeded); CHECK(HttpResponse->GetResponseCode() == 200); 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); bQuitRequested = true; }); HttpRequest->ProcessRequest(); }); HttpRequest->ProcessRequest(); } class FWaitUntilCompleteHttpFixture : public FHttpModuleTestFixture { public: FWaitUntilCompleteHttpFixture() { HttpModule->GetHttpManager().SetRequestAddedDelegate(FHttpManagerRequestAddedDelegate::CreateRaw(this, &FWaitUntilCompleteHttpFixture::OnRequestAdded)); HttpModule->GetHttpManager().SetRequestCompletedDelegate(FHttpManagerRequestCompletedDelegate::CreateRaw(this, &FWaitUntilCompleteHttpFixture::OnRequestCompleted)); } ~FWaitUntilCompleteHttpFixture() { WaitUntilAllHttpRequestsComplete(); HttpModule->GetHttpManager().SetRequestAddedDelegate(FHttpManagerRequestAddedDelegate()); HttpModule->GetHttpManager().SetRequestCompletedDelegate(FHttpManagerRequestCompletedDelegate()); } void OnRequestAdded(const FHttpRequestRef& Request) { ++OngoingRequests; } void OnRequestCompleted(const FHttpRequestRef& Request) { if (ensure(OngoingRequests > 0)) { --OngoingRequests; } } void WaitUntilAllHttpRequestsComplete() { while (!bRunningThreadRequest && OngoingRequests != 0) { HttpModule->GetHttpManager().Tick(TickFrequency); FPlatformProcess::Sleep(TickFrequency); } } uint32 OngoingRequests = 0; float TickFrequency = 1.0f / 60; /*60 FPS*/; bool bRunningThreadRequest = false; }; TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Http Methods", HTTP_TAG) { TSharedRef HttpRequest = HttpModule->CreateRequest(); HttpRequest->SetURL(UrlToTestMethods()); 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([](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(bSucceeded); REQUIRE(HttpResponse != nullptr); CHECK(HttpResponse->GetResponseCode() == 200); }); HttpRequest->ProcessRequest(); } TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Get large response content without chunks", HTTP_TAG) { TSharedRef HttpRequest = HttpModule->CreateRequest(); HttpRequest->SetURL(FString::Format(TEXT("{0}/get_large_response_without_chunks/{1}/"), { *UrlHttpTests(), 1024 * 1024/*bytes_number*/})); HttpRequest->SetVerb(TEXT("GET")); 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 request connect timeout", HTTP_TAG) { TSharedRef HttpRequest = HttpModule->CreateRequest(); HttpRequest->SetURL(UrlWithInvalidPortToTestConnectTimeout()); HttpRequest->SetVerb(TEXT("GET")); HttpRequest->SetTimeout(7); FDateTime StartTime = FDateTime::Now(); HttpRequest->OnProcessRequestComplete().BindLambda([StartTime](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(!bSucceeded); CHECK(HttpResponse == nullptr); // TODO: For now curl impl is using customized timeout instead of relying on native http timeout, // which doesn't get CURLE_COULDNT_CONNECT. Enable this after switching to native http timeout //CHECK(HttpRequest->GetStatus() == EHttpRequestStatus::Failed_ConnectionError); FTimespan Timespan = FDateTime::Now() - StartTime; float DurationInSeconds = Timespan.GetTotalSeconds(); CHECK(FMath::IsNearlyEqual(DurationInSeconds, 7, HTTP_TIME_DIFF_TOLERANCE)); }); HttpRequest->ProcessRequest(); } TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Streaming http download", HTTP_TAG) { uint32 Chunks = 3; uint32 ChunkSize = 1024*1024; TSharedRef HttpRequest = HttpModule->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->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->GetContentLength() == Chunks * ChunkSize); CHECK(HttpResponse->GetContent().IsEmpty()); CHECK(*TotalBytesReceived == Chunks * ChunkSize); }); } SECTION("Success with customized stream delegate") { FHttpRequestStreamDelegate Delegate; Delegate.BindLambda([TotalBytesReceived](void* Ptr, int64 Length) { *TotalBytesReceived += Length; return true; }); CHECK(HttpRequest->SetResponseBodyReceiveStreamDelegate(Delegate)); HttpRequest->OnProcessRequestComplete().BindLambda([Chunks, ChunkSize, TotalBytesReceived](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(bSucceeded); REQUIRE(HttpResponse != nullptr); CHECK(HttpResponse->GetResponseCode() == 200); 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") { 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") { FHttpRequestStreamDelegate Delegate; Delegate.BindLambda([TotalBytesReceived](void* Ptr, int64 Length) { *TotalBytesReceived += Length; return false; }); CHECK(HttpRequest->SetResponseBodyReceiveStreamDelegate(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); FileToWrite->FlushCache(); FileToWrite->Close(); TSharedRef FileToRead = MakeShareable(IFileManager::Get().CreateFileReader(*Filename)); CHECK(FileToRead->TotalSize() == Chunks * ChunkSize); IFileManager::Get().Delete(*Filename); }); } HttpRequest->ProcessRequest(); } 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 = HttpModule->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 download big file exceeds 32 bits", HTTP_TAG) { // 5 * 1024 * 1024 * 1024 BYTES = 5368709120 BYTES = 5 GB uint64 Chunks = 5 * 1024; uint64 ChunkSize = 1024 * 1024; TSharedRef HttpRequest = HttpModule->CreateRequest(); HttpRequest->SetURL(UrlStreamDownload(Chunks, ChunkSize)); HttpRequest->SetVerb(TEXT("GET")); TSharedRef TotalBytesReceived = MakeShared(0); FHttpRequestStreamDelegate Delegate; Delegate.BindLambda([TotalBytesReceived](void* Ptr, int64 Length) { *TotalBytesReceived += Length; return true; }); HttpRequest->SetResponseBodyReceiveStreamDelegate(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(); } TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Streaming http upload from memory", HTTP_TAG) { TSharedRef HttpRequest = HttpModule->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) { // 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 = HttpModule->CreateRequest(); HttpRequest->SetURL(FString::Format(TEXT("{0}/streaming_upload_put"), { *UrlHttpTests() })); 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(); } TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Streaming http upload from file by PUT can work well", HTTP_TAG) { FString Filename = FString(FPlatformProcess::UserSettingsDir()) / TEXT("TestStreamUpload.dat"); FArchive* RawFile = IFileManager::Get().CreateFileWriter(*Filename); CHECK(RawFile != nullptr); TSharedRef FileToWrite = MakeShareable(RawFile); const uint64 FileSize = 5*1024*1024; // 5MB char* FileData = (char*)FMemory::Malloc(FileSize); FMemory::Memset(FileData, 'd', FileSize); FileToWrite->Serialize(FileData, FileSize); FileToWrite->FlushCache(); FileToWrite->Close(); FMemory::Free(FileData); TSharedRef HttpRequest = HttpModule->CreateRequest(); HttpRequest->SetURL(FString::Format(TEXT("{0}/streaming_upload_put"), { *UrlHttpTests() })); 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, "Redirect enabled by default and can work well", HTTP_TAG) { TSharedRef HttpRequest = HttpModule->CreateRequest(); HttpRequest->SetURL(FString::Format(TEXT("{0}/redirect_from"), { *UrlHttpTests() })); HttpRequest->SetVerb(TEXT("GET")); HttpRequest->OnProcessRequestComplete().BindLambda([](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(bSucceeded); CHECK(HttpResponse->GetResponseCode() == 200); }); HttpRequest->ProcessRequest(); } class FWaitThreadedHttpFixture : public FWaitUntilCompleteHttpFixture, public FRunnable { public: DECLARE_DELEGATE(FRunActualTestCodeDelegate); FWaitThreadedHttpFixture() { bRunningThreadRequest = true; } // FRunnable interface virtual uint32 Run() override { ThreadCallback.ExecuteIfBound(); bRunningThreadRequest = false; return 0; } void StartTestHttpThread() { RunnableThread = TSharedPtr(FRunnableThread::Create(this, TEXT("Test Http Thread"))); } FRunActualTestCodeDelegate ThreadCallback; TSharedPtr RunnableThread; }; TEST_CASE_METHOD(FWaitThreadedHttpFixture, "Http streaming download request can work in non game thread", HTTP_TAG) { std::atomic bBlockingGameThreadTick = true; ThreadCallback.BindLambda([this, &bBlockingGameThreadTick]() { TSharedRef HttpRequest = HttpModule->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([&bBlockingGameThreadTick](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { // EHttpRequestDelegateThreadPolicy::CompleteOnHttpThread was used, so not in game thread here CHECK(!IsInGameThread()); CHECK(bSucceeded); CHECK(HttpResponse->GetResponseCode() == 200); bBlockingGameThreadTick = false; }); HttpRequest->ProcessRequest(); }); StartTestHttpThread(); while (bBlockingGameThreadTick) { FPlatformProcess::Sleep(TickFrequency); } } 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); } } } TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Http request pre check will fail", HTTP_TAG) { // Pre check will fail when domain is not allowed UE::TestHttp::SetupURLRequestFilter(HttpModule); TSharedRef HttpRequest = HttpModule->CreateRequest(); HttpRequest->SetVerb(TEXT("GET")); HttpRequest->SetURL(UrlToTestMethods()); SECTION("on game thread") { HttpRequest->OnProcessRequestComplete().BindLambda([](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(IsInGameThread()); CHECK(!bSucceeded); }); } SECTION("on http thread") { HttpRequest->SetDelegateThreadPolicy(EHttpRequestDelegateThreadPolicy::CompleteOnHttpThread); HttpRequest->OnProcessRequestComplete().BindLambda([](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(!IsInGameThread()); CHECK(!bSucceeded); }); } HttpRequest->ProcessRequest(); } TEST_CASE_METHOD(FWaitThreadedHttpFixture, "Threaded http request pre check will fail", HTTP_TAG) { ThreadCallback.BindLambda([this]() { // Pre check will fail when domain is not allowed UE::TestHttp::SetupURLRequestFilter(HttpModule); TSharedRef HttpRequest = HttpModule->CreateRequest(); HttpRequest->SetVerb(TEXT("GET")); HttpRequest->SetURL(UrlToTestMethods()); SECTION("on game thread") { HttpRequest->OnProcessRequestComplete().BindLambda([](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(IsInGameThread()); CHECK(!bSucceeded); }); } SECTION("on http thread") { HttpRequest->SetDelegateThreadPolicy(EHttpRequestDelegateThreadPolicy::CompleteOnHttpThread); HttpRequest->OnProcessRequestComplete().BindLambda([](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(!IsInGameThread()); CHECK(!bSucceeded); }); } HttpRequest->ProcessRequest(); }); StartTestHttpThread(); } TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Cancel http request connect before timeout", HTTP_TAG) { TSharedRef HttpRequest = HttpModule->CreateRequest(); HttpRequest->SetURL(UrlWithInvalidPortToTestConnectTimeout()); HttpRequest->SetVerb(TEXT("GET")); HttpRequest->SetTimeout(7); FDateTime StartTime = FDateTime::Now(); HttpRequest->OnProcessRequestComplete().BindLambda([StartTime](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) { CHECK(!bSucceeded); FTimespan Timespan = FDateTime::Now() - StartTime; float DurationInSeconds = Timespan.GetTotalSeconds(); CHECK(DurationInSeconds < 2); }); HttpRequest->ProcessRequest(); FPlatformProcess::Sleep(0.5); HttpRequest->CancelRequest(); } // TODO: Add cancel test, with multiple cancel calls