You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
Reason: Currently when shutdown and flush in http manager, it gives up and cancels remaining requests after waiting a short period. When cancel, it holds the RequestLock, and in FHttpRequestCommon::CancelRequest, it tries to access HttpTaskTimerHandleCriticalSection to stop activity timer. In the mean time, the request itself could be in activity or total timeout callback, which is holding the HttpTaskTimerHandleCriticalSection, and trying to AbortRequest which will try to access RequstLock in HttpManager.IsValidRequest. [REVIEW] [at]michael.kirzinger [at]michael.atchison [at]rafa.lecina #rb michael.atchison, Michael.Kirzinger, Rafa.Lecina #tests Reproduced with test case in WebTests and fixed it. Tried launch and quit game on Win64. [CL 32281657 by lorry li in ue5-main branch]
2161 lines
73 KiB
C++
2161 lines
73 KiB
C++
// 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 "HttpManager.h"
|
|
#include "HttpRetrySystem.h"
|
|
#include "Http.h"
|
|
#include "HttpPath.h"
|
|
#include "HttpRouteHandle.h"
|
|
#include "IHttpRouter.h"
|
|
#include "HttpServerModule.h"
|
|
#include "Misc/CommandLine.h"
|
|
#include "TestHarness.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<bool> CVarHttpInsecureProtocolEnabled;
|
|
extern TAutoConsoleVariable<bool> CVarHttpRetrySystemNonGameThreadSupportEnabled;
|
|
|
|
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)
|
|
{
|
|
ParseSettingsFromCommandLine();
|
|
|
|
bRetryEnabled &= CVarHttpRetrySystemNonGameThreadSupportEnabled.GetValueOnAnyThread();
|
|
|
|
InitModule();
|
|
|
|
CVarHttpInsecureProtocolEnabled->Set(true);
|
|
}
|
|
|
|
void InitModule()
|
|
{
|
|
HttpModule = new FMockHttpModule();
|
|
IModuleInterface* Module = HttpModule;
|
|
Module->StartupModule();
|
|
if (bRetryEnabled)
|
|
{
|
|
HttpRetryManager = MakeShared<FMockRetryManager>(FHttpRetrySystem::FRetryLimitCountSetting(0), FHttpRetrySystem::FRetryTimeoutRelativeSecondsSetting(/*RetryTimeoutRelativeSeconds*/));
|
|
}
|
|
}
|
|
|
|
void ShutdownModule()
|
|
{
|
|
HttpRetryManager = nullptr;
|
|
|
|
IModuleInterface* Module = HttpModule;
|
|
if (Module)
|
|
{
|
|
Module->ShutdownModule();
|
|
delete Module;
|
|
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);
|
|
}
|
|
|
|
void DisableWarningsInThisTest()
|
|
{
|
|
HttpTestLogLevelInitializer.DisableWarningsInThisTest();
|
|
}
|
|
|
|
void ResumeLogVerbosity()
|
|
{
|
|
HttpTestLogLevelInitializer.ResumeLogVerbosity();
|
|
}
|
|
|
|
TSharedRef<IHttpRequest> CreateRequest()
|
|
{
|
|
return bRetryEnabled ? HttpRetryManager->CreateRequest() : HttpModule->CreateRequest();
|
|
}
|
|
|
|
const FString UrlWithInvalidPortToTestConnectTimeout() const { return TEXT("http://10.255.255.1:8765"); } // non-routable IP address with a random port
|
|
const FString UrlBase() const { return FString::Format(TEXT("http://{0}:{1}"), { *WebServerIp, WebServerHttpPort }); }
|
|
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 }); }
|
|
|
|
FString WebServerIp;
|
|
uint32 WebServerHttpPort;
|
|
FMockHttpModule* HttpModule = nullptr;
|
|
bool bRunHeavyTests;
|
|
bool bRetryEnabled;
|
|
FHttpTestLogLevelInitializer HttpTestLogLevelInitializer;
|
|
TSharedPtr<FMockRetryManager> 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<uint8> 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<IHttpRequest> 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_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<uint8> DataChunk;
|
|
DataChunk.SetNum(ChunkSize);
|
|
FMemory::Memset(DataChunk.GetData(), 'd', ChunkSize);
|
|
|
|
for (int32 i = 0; i < NumRequests; ++i)
|
|
{
|
|
{
|
|
TSharedRef<IHttpRequest> 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<IHttpRequest> 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<int32> OngoingRequests = 0;
|
|
float TickFrequency = 1.0f / 60; /*60 FPS*/;
|
|
|
|
uint32 RetryLimitCount = 0;
|
|
uint32 ExpectingExtraCallbacks = 0;
|
|
};
|
|
|
|
TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Http Methods", HTTP_TAG)
|
|
{
|
|
TSharedRef<IHttpRequest> HttpRequest = CreateRequest();
|
|
CHECK(HttpRequest->GetVerb() == TEXT("GET"));
|
|
|
|
HttpRequest->SetURL(UrlToTestMethods());
|
|
|
|
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([](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<IHttpRequest> 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 complete successfully for different response codes", HTTP_TAG)
|
|
{
|
|
TSharedRef<IHttpRequest> 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<IHttpRequest> 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<IHttpRequest> 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<uint8>& 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<IHttpRequest> HttpRequest = CreateRequest();
|
|
|
|
HttpRequest->SetURL(UrlWithInvalidPortToTestConnectTimeout());
|
|
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<IHttpRequest> HttpRequest = CreateRequest();
|
|
HttpRequest->SetURL(UrlStreamDownload(Chunks, ChunkSize));
|
|
HttpRequest->SetVerb(TEXT("GET"));
|
|
|
|
TSharedRef<int64> TotalBytesReceived = MakeShared<int64>(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<int64> InTotalBytesReceived)
|
|
: TotalBytesReceived(InTotalBytesReceived)
|
|
{
|
|
}
|
|
|
|
virtual void Serialize(void* V, int64 Length) override
|
|
{
|
|
*TotalBytesReceived += Length;
|
|
}
|
|
|
|
TSharedRef<int64> TotalBytesReceived;
|
|
};
|
|
|
|
TSharedRef<FTestHttpReceiveStream> Stream = MakeShared<FTestHttpReceiveStream>(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")
|
|
{
|
|
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->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<int64> InTotalBytesReceived)
|
|
: TotalBytesReceived(InTotalBytesReceived)
|
|
{
|
|
}
|
|
|
|
virtual void Serialize(void* V, int64 Length) override
|
|
{
|
|
*TotalBytesReceived += Length;
|
|
SetError();
|
|
}
|
|
|
|
TSharedRef<int64> TotalBytesReceived;
|
|
};
|
|
|
|
TSharedRef<FTestHttpReceiveStream> Stream = MakeShared<FTestHttpReceiveStream>(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();
|
|
|
|
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<FArchive> 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<FArchive> 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;
|
|
}
|
|
|
|
bool OnReceivedData(void* Ptr, int64 Length)
|
|
{
|
|
*TotalBytesReceived += Length;
|
|
return true;
|
|
}
|
|
|
|
int64* TotalBytesReceived;
|
|
};
|
|
|
|
TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "In streaming downloading http request won't trigger response body receive delegate after canceling", HTTP_TAG)
|
|
{
|
|
TSharedRef<IHttpRequest> HttpRequest = CreateRequest();
|
|
HttpRequest->SetURL(UrlStreamDownload(30, 1024*1024));
|
|
|
|
TSharedPtr<FUserStreamingClass> UserInstance = MakeShared<FUserStreamingClass>();
|
|
|
|
FHttpRequestStreamDelegate Delegate;
|
|
Delegate.BindThreadSafeSP(UserInstance.ToSharedRef(), &FUserStreamingClass::OnReceivedData);
|
|
CHECK(HttpRequest->SetResponseBodyReceiveStreamDelegate(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);
|
|
CHECK(UserInstance.GetSharedReferenceCount() == 1);
|
|
HttpRequest->CancelRequest();
|
|
UserInstance.Reset();
|
|
}
|
|
|
|
class FInvalidateDelegateShutdownFixture : public FHttpModuleTestFixture
|
|
{
|
|
public:
|
|
FInvalidateDelegateShutdownFixture()
|
|
{
|
|
UserStreamingInstance = new FUserStreamingClass;
|
|
}
|
|
|
|
~FInvalidateDelegateShutdownFixture()
|
|
{
|
|
delete UserStreamingInstance;
|
|
UserStreamingInstance = nullptr;
|
|
}
|
|
|
|
FUserStreamingClass* 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<IHttpRequest> HttpRequest = HttpModule->CreateRequest();
|
|
HttpRequest->SetURL(UrlStreamDownload(10, 1024*1024));
|
|
FHttpRequestStreamDelegate Delegate;
|
|
Delegate.BindRaw(UserStreamingInstance, &FUserStreamingClass::OnReceivedData);
|
|
CHECK(HttpRequest->SetResponseBodyReceiveStreamDelegate(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<IHttpRequest> 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 download big file exceeds 32 bits", HTTP_TAG)
|
|
{
|
|
if (!bRunHeavyTests)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// 5 * 1024 * 1024 * 1024 BYTES = 5368709120 BYTES = 5 GB
|
|
uint64 Chunks = 5 * 1024;
|
|
uint64 ChunkSize = 1024 * 1024;
|
|
|
|
TSharedRef<IHttpRequest> HttpRequest = CreateRequest();
|
|
HttpRequest->SetURL(UrlStreamDownload(Chunks, ChunkSize));
|
|
HttpRequest->SetVerb(TEXT("GET"));
|
|
|
|
TSharedRef<int64> TotalBytesReceived = MakeShared<int64>(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<IHttpRequest> 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<uint8> 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<FTestHttpUploadStream> Stream = MakeShared<FTestHttpUploadStream>(TotalSize);
|
|
|
|
TSharedRef<IHttpRequest> 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<FArchive> 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 ReceiveTimeoutSetting = 3.0f;
|
|
HttpModule->HttpActivityTimeout = ReceiveTimeoutSetting;
|
|
|
|
TSharedPtr<IHttpRequest> HttpRequest = CreateRequest();
|
|
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, ReceiveTimeoutSetting](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, ReceiveTimeoutSetting + 5, HTTP_TIME_DIFF_TOLERANCE_OF_REQUEST));
|
|
#else
|
|
CHECK(FMath::IsNearlyEqual(DurationInSeconds, ReceiveTimeoutSetting, 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<IHttpRequest> 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<IHttpRequest> 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<IHttpRequest> 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<IHttpRequest> 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<IHttpRequest> 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<IHttpRequest> 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, "Redirect enabled by default and can work well", HTTP_TAG)
|
|
{
|
|
TSharedRef<IHttpRequest> 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<IHttpRequest> 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();
|
|
}
|
|
|
|
#if UE_HTTP_CONNECTION_TIMEOUT_SUPPORT_RETRY
|
|
TEST_CASE_METHOD(FWaitUntilQuitFromTestFixture, "Make sure connection time out can work well for 2nd same http request", HTTP_TAG)
|
|
{
|
|
DisableWarningsInThisTest();
|
|
|
|
TSharedRef<IHttpRequest> HttpRequest = CreateRequest();
|
|
|
|
float ConnectionTimeoutDuration = 2.0f;
|
|
HttpModule->HttpConnectionTimeout = ConnectionTimeoutDuration;
|
|
|
|
HttpRequest->SetURL(UrlWithInvalidPortToTestConnectTimeout());
|
|
|
|
const double StartTime = FPlatformTime::Seconds();
|
|
|
|
HttpRequest->OnProcessRequestComplete().BindLambda([this, StartTime, ConnectionTimeoutDuration](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) {
|
|
TSharedRef<IHttpRequest> HttpRequest2 = CreateRequest();
|
|
HttpRequest2->SetURL(UrlWithInvalidPortToTestConnectTimeout());
|
|
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();
|
|
}
|
|
#endif // UE_HTTP_CONNECTION_TIMEOUT_SUPPORT_RETRY
|
|
|
|
// 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<IHttpRequest> 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);
|
|
|
|
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>(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<FRunnableThread> RunnableThread;
|
|
std::atomic<bool> 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<IHttpRequest> 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<FTestHttpReceiveStream>()));
|
|
|
|
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<bool> bRequestProgressTriggered = false;
|
|
ThreadedHttpRunnable.OnRunFromThread().BindLambda([this, &bRequestProgressTriggered]() {
|
|
TSharedRef<IHttpRequest> 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<IHttpRequest> 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<bool> bHeaderReceived = false;
|
|
std::atomic<bool> bCompleteCallbackTriggered = false;
|
|
std::atomic<bool> bAnyDataReceived = false;
|
|
};
|
|
|
|
TEST_CASE_METHOD(FValidateHeaderReceiveOrderFixture, "Http request header received callback will be called by thread policy", HTTP_TAG)
|
|
{
|
|
TSharedRef<IHttpRequest> HttpRequest = CreateRequest();
|
|
HttpRequest->SetURL(UrlStreamDownload(2/*Chunks*/, 1024/*ChunkSize*/));
|
|
HttpRequest->SetVerb(TEXT("GET"));
|
|
|
|
FHttpRequestStreamDelegate StreamDelegate;
|
|
StreamDelegate.BindLambda([this](void *InDataPtr, int64 InLength) {
|
|
bAnyDataReceived = true;
|
|
CHECK(!bCompleteCallbackTriggered);
|
|
return true;
|
|
});
|
|
HttpRequest->SetResponseBodyReceiveStreamDelegate(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<bool> bStatusCodeReceived = false;
|
|
std::atomic<bool> bCompleteCallbackTriggered = false;
|
|
};
|
|
|
|
TEST_CASE_METHOD(FValidateStatusCodeReceiveOrderFixture, "Http request status code received callback will be called by thread policy", HTTP_TAG)
|
|
{
|
|
TSharedRef<IHttpRequest> 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<FString>{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<IHttpRequest> 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<IHttpRequest> 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(FWaitUntilCompleteHttpFixture, "Cancel http request connect before timeout", HTTP_TAG)
|
|
{
|
|
DisableWarningsInThisTest();
|
|
|
|
TSharedRef<IHttpRequest> HttpRequest = CreateRequest();
|
|
HttpRequest->SetURL(UrlWithInvalidPortToTestConnectTimeout());
|
|
HttpRequest->SetVerb(TEXT("GET"));
|
|
HttpRequest->SetTimeout(7);
|
|
const double StartTime = FPlatformTime::Seconds();
|
|
HttpRequest->OnProcessRequestComplete().BindLambda([StartTime](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) {
|
|
CHECK(!bSucceeded);
|
|
const double DurationInSeconds = FPlatformTime::Seconds() - StartTime;
|
|
CHECK(DurationInSeconds < 2.0);
|
|
CHECK(HttpRequest->GetFailureReason() == EHttpFailureReason::Cancelled);
|
|
});
|
|
HttpRequest->ProcessRequest();
|
|
FPlatformProcess::Sleep(0.5);
|
|
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<IHttpRequest> HttpRequest = HttpRetryManager->CreateRequest(
|
|
1/*InRetryLimitCountOverride*/,
|
|
FHttpRetrySystem::FRetryTimeoutRelativeSecondsSetting()/*InRetryTimeoutRelativeSecondsOverride unused*/,
|
|
{EHttpResponseCodes::TooManyRequests, EHttpResponseCodes::ServiceUnavail}/*InRetryResponseCodes*/
|
|
);
|
|
|
|
SECTION("TooManyRequests")
|
|
{
|
|
HttpRequest->SetURL(UrlMockStatus(EHttpResponseCodes::TooManyRequests));
|
|
}
|
|
SECTION("ServiceUnavail")
|
|
{
|
|
HttpRequest->SetURL(UrlMockStatus(EHttpResponseCodes::ServiceUnavail));
|
|
}
|
|
|
|
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<IHttpRequest> 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<IHttpRequest> 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, "Retry immediately without lock out if connect failed and there are alt domains", HTTP_TAG)
|
|
{
|
|
if (!bRetryEnabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
DisableWarningsInThisTest();
|
|
|
|
HttpModule->HttpConnectionTimeout = 1.0f;
|
|
|
|
TArray<FString> AltDomains;
|
|
AltDomains.Add(UrlToTestMethods());
|
|
FHttpRetrySystem::FRetryDomainsPtr RetryDomains = MakeShared<FHttpRetrySystem::FRetryDomains>(MoveTemp(AltDomains));
|
|
TSharedRef<IHttpRequest> HttpRequest = HttpRetryManager->CreateRequest(
|
|
1/*InRetryLimitCountOverride*/,
|
|
FHttpRetrySystem::FRetryTimeoutRelativeSecondsSetting()/*InRetryTimeoutRelativeSecondsOverride unused*/,
|
|
{ EHttpResponseCodes::TooManyRequests, EHttpResponseCodes::ServiceUnavail }/*InRetryResponseCodes*/,
|
|
FHttpRetrySystem::FRetryVerbs(),
|
|
RetryDomains
|
|
);
|
|
|
|
HttpRequest->SetURL(UrlWithInvalidPortToTestConnectTimeout());
|
|
HttpRequest->SetVerb(TEXT("GET"));
|
|
++ExpectingExtraCallbacks;
|
|
HttpRequest->OnRequestWillRetry().BindLambda([this](FHttpRequestPtr /*Request*/, FHttpResponsePtr /*Response*/, float LockoutPeriod) {
|
|
--ExpectingExtraCallbacks;
|
|
CHECK(LockoutPeriod == 0);
|
|
});
|
|
HttpRequest->ProcessRequest();
|
|
}
|
|
|
|
#if UE_HTTP_CONNECTION_TIMEOUT_SUPPORT_RETRY
|
|
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<IHttpRequest> 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")
|
|
{
|
|
HttpRequest->SetURL(UrlWithInvalidPortToTestConnectTimeout());
|
|
|
|
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();
|
|
}
|
|
#endif // UE_HTTP_CONNECTION_TIMEOUT_SUPPORT_RETRY
|
|
|
|
TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Retry fallback with exponential lock out if there is no Retry-After header", HTTP_TAG)
|
|
{
|
|
if (!bRetryEnabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
DisableWarningsInThisTest();
|
|
|
|
TSharedRef<IHttpRequest> 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<IHttpRequest> 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<IHttpRequest> 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<IHttpRequest> 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);
|
|
}
|
|
|
|
#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<IHttpRequest> 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();
|
|
IModuleInterface* Module = HttpServerModule;
|
|
Module->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();
|
|
|
|
IModuleInterface* Module = HttpServerModule;
|
|
Module->ShutdownModule();
|
|
delete Module;
|
|
}
|
|
|
|
TSharedPtr<IHttpRouter> 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<uint8> ResultData;
|
|
ResultData.SetNum(FileSize);
|
|
FMemory::Memset(ResultData.GetData(), 'd', FileSize);
|
|
|
|
OnComplete(FHttpServerResponse::Create(MoveTemp(ResultData), TEXT("text/text")));
|
|
return true;
|
|
}));
|
|
|
|
// Start client request
|
|
TSharedRef<IHttpRequest> 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, "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<TSharedRef<IHttpRequest>> Requests;
|
|
|
|
for (int32 i = 0; i < NumRequests; ++i)
|
|
{
|
|
TSharedRef<IHttpRequest> 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();
|
|
}
|
|
}
|
|
}
|
|
} |