// Copyright Epic Games, Inc. All Rights Reserved. #include "TransactionallySafeHttpRequest.h" #include "Http.h" #include "PlatformHttp.h" #include "Containers/Array.h" #include "Containers/Map.h" #include "Containers/StringConv.h" #include "GenericPlatform/GenericPlatformHttp.h" #include "HAL/Platform.h" #include "Logging/StructuredLog.h" #include "Misc/Optional.h" #include "Misc/TVariant.h" #include "Templates/SharedPointer.h" class FTransactionallySafeHttpRequest::FClosedHttpRequest : public IHttpRequest { public: FClosedHttpRequest(FTransactionallySafeHttpRequest* Owner) { AutoRTFM::PushOnCommitHandler(this, [this, Owner] { TSharedPtr Req{FPlatformHttp::ConstructRequest()}; Req->SetDelegateThreadPolicy(ThreadPolicy); Req->SetVerb(MoveTemp(Verb)); if (!Url.IsEmpty()) { Req->SetURL(MoveTemp(Url)); } for (TPair& Option : Options) { Req->SetOption(Option.Key, MoveTemp(Option.Value)); } for (TPair& Header : Headers) { Req->SetHeader(MoveTemp(Header.Key), MoveTemp(Header.Value)); } if (ResponseBodyReceiveStream) { Req->SetResponseBodyReceiveStream(ResponseBodyReceiveStream.ToSharedRef()); } if (TimeoutSecs.IsSet()) { Req->SetTimeout(TimeoutSecs.GetValue()); } if (ActivityTimeoutSecs.IsSet()) { Req->SetActivityTimeout(ActivityTimeoutSecs.GetValue()); } if (CompleteDelegate.IsSet()) { Req->OnProcessRequestComplete() = MoveTemp(CompleteDelegate.GetValue()); } if (ProgressDelegate.IsSet()) { Req->OnRequestProgress64() = MoveTemp(ProgressDelegate.GetValue()); } if (WillRetryDelegate.IsSet()) { Req->OnRequestWillRetry() = MoveTemp(WillRetryDelegate.GetValue()); } if (HeaderReceivedDelegate.IsSet()) { Req->OnHeaderReceived() = MoveTemp(HeaderReceivedDelegate.GetValue()); } if (StatusCodeReceivedDelegate.IsSet()) { Req->OnStatusCodeReceived() = MoveTemp(StatusCodeReceivedDelegate.GetValue()); } if (Payload.IsType()) { Req->SetContent(MoveTemp(Payload.Get().Content)); } else if (Payload.IsType()) { Req->SetContentAsStreamedFile(Payload.Get().Filename); } else if (Payload.IsType()) { Req->SetContentFromStream(MoveTemp(Payload.Get().Stream)); } if (bProcessRequest) { Req->ProcessRequest(); } Owner->InnerRequest = MoveTemp(Req); }); } ~FClosedHttpRequest() { // If the request is destroyed before the transaction is committed, // there's nothing more to do. AutoRTFM::PopOnCommitHandler(this); } const FString& GetURL() const override { return Url; } void SetURL(const FString& InURL) override { Url = InURL; } void SetHeader(const FString& HeaderName, const FString& HeaderValue) override { Headers.Add(HeaderName, HeaderValue); } FString GetHeader(const FString& HeaderName) const override { const FString* Header = Headers.Find(HeaderName); return Header ? *Header : FString(); } void AppendToHeader(const FString& HeaderName, const FString& AdditionalHeaderValue) override { ensure(!"Implement AddToHeader() if it becomes necessary."); } TArray GetAllHeaders() const override { ensure(!"Implement GetAllHeaders() if it becomes necessary."); return TArray{}; } FString GetVerb() const override { return Verb; } void SetVerb(const FString& InVerb) override { Verb = InVerb; } FString GetOption(const FName Option) const override { const FString* OptionValue = Options.Find(Option); return OptionValue ? *OptionValue : FString(); } void SetOption(const FName Option, const FString& OptionValue) override { Options.Add(Option, OptionValue); } void SetContent(const TArray& InPayload) override { Payload.Set({ InPayload }); } void SetContent(TArray&& InPayload) override { Payload.Set({ MoveTemp(InPayload) }); } const TArray& GetContent() const override { return Payload.IsType() ? Payload.Get().Content : EmptyContent; } void SetContentAsString(const FString& ContentString) override { FTCHARToUTF8 Converter(*ContentString); TArray ContentStringAsUTF8; ContentStringAsUTF8.SetNum(Converter.Length()); FMemory::Memcpy(ContentStringAsUTF8.GetData(), (const uint8*)Converter.Get(), ContentStringAsUTF8.Num()); Payload.Set({ MoveTemp(ContentStringAsUTF8) }); } bool SetContentAsStreamedFile(const FString& Filename) override { Payload.Set({Filename}); return true; } bool SetContentFromStream(TSharedRef Stream) override { Payload.Set({MoveTemp(Stream)}); return true; } EHttpRequestStatus::Type GetStatus() const override { return EHttpRequestStatus::NotStarted; } const FString& GetEffectiveURL() const override { // The effective URL will always be an empty string at this point in the request lifecycle. return EmptyString; } FString GetURLParameter(const FString& ParameterName) const override { TOptional Param = FGenericPlatformHttp::GetUrlParameter(Url, ParameterName); return Param.Get(FString()); } uint64 GetContentLength() const override { if (Payload.IsType()) { return Payload.Get().Content.Num(); } if (Payload.IsType()) { return Payload.Get().Stream->TotalSize(); } return 0; } FString GetContentType() const override { return GetHeader(TEXT("Content-Type")); } bool SetResponseBodyReceiveStream(TSharedRef Stream) override { ResponseBodyReceiveStream = MoveTemp(Stream); return true; } EHttpFailureReason GetFailureReason() const override { return EHttpFailureReason::None; } const FHttpResponsePtr GetResponse() const override { return nullptr; } void Tick(float) override { ensure(!"Tick() shouldn't be called on a FClosedHttpRequest."); } float GetElapsedTime() const override { return 0.0f; } void SetDelegateThreadPolicy(EHttpRequestDelegateThreadPolicy InThreadPolicy) override { ThreadPolicy = InThreadPolicy; } EHttpRequestDelegateThreadPolicy GetDelegateThreadPolicy() const override { return ThreadPolicy; } bool ProcessRequest() override { bProcessRequest = true; return true; } void CancelRequest() override { check(!"CancelRequest() shouldn't be called on a FClosedHttpRequest."); } void ProcessRequestUntilComplete() override { // We can't do a blocking HTTP load inside of a transaction. We don't know if the transaction // will succeed or not at this point, so we can't issue the HTTP request. // If we reach this point, the code needs to be restructured to use a non-blocking load. check(!"ProcessRequestUntilComplete shouldn't be called on a FClosedHttpRequest."); abort(); } void SetTimeout(float InTimeoutSecs) override { TimeoutSecs = InTimeoutSecs; } void SetActivityTimeout(float InTimeoutSecs) override { ActivityTimeoutSecs = InTimeoutSecs; } void ClearTimeout() override { TimeoutSecs.Reset(); } void ResetTimeoutStatus() override { } TOptional GetTimeout() const override { return TimeoutSecs; } FHttpRequestCompleteDelegate& OnProcessRequestComplete() override { return CompleteDelegate.IsSet() ? CompleteDelegate.GetValue() : CompleteDelegate.Emplace(); } FHttpRequestProgressDelegate64& OnRequestProgress64() override { return ProgressDelegate.IsSet() ? ProgressDelegate.GetValue() : ProgressDelegate.Emplace(); } FHttpRequestWillRetryDelegate& OnRequestWillRetry() override { return WillRetryDelegate.IsSet() ? WillRetryDelegate.GetValue() : WillRetryDelegate.Emplace(); } FHttpRequestHeaderReceivedDelegate& OnHeaderReceived() override { return HeaderReceivedDelegate.IsSet() ? HeaderReceivedDelegate.GetValue() : HeaderReceivedDelegate.Emplace(); } FHttpRequestStatusCodeReceivedDelegate& OnStatusCodeReceived() override { return StatusCodeReceivedDelegate.IsSet() ? StatusCodeReceivedDelegate.GetValue() : StatusCodeReceivedDelegate.Emplace(); } private: FString Url; FString Verb = TEXT("GET"); TMap Options; TMap Headers; TSharedPtr ResponseBodyReceiveStream; EHttpRequestDelegateThreadPolicy ThreadPolicy = EHttpRequestDelegateThreadPolicy::CompleteOnGameThread; TOptional TimeoutSecs; TOptional ActivityTimeoutSecs; TOptional CompleteDelegate; TOptional ProgressDelegate; TOptional WillRetryDelegate; TOptional HeaderReceivedDelegate; TOptional StatusCodeReceivedDelegate; bool bProcessRequest = false; const TArray EmptyContent; const FString EmptyString; // The caller can specify a number of different payload types. struct NoPayload {}; struct RawPayload { TArray Content; }; struct FilePayload { FString Filename; }; struct StreamPayload { TSharedRef Stream; }; TVariant Payload; }; FTransactionallySafeHttpRequest::FTransactionallySafeHttpRequest() { InnerRequest = AutoRTFM::IsClosed() ? MakeShared(this) : TSharedPtr(FPlatformHttp::ConstructRequest()); } const FString& FTransactionallySafeHttpRequest::GetURL() const { return InnerRequest->GetURL(); } void FTransactionallySafeHttpRequest::SetURL(const FString& InURL) { InnerRequest->SetURL(InURL); } void FTransactionallySafeHttpRequest::SetHeader(const FString& HeaderName, const FString& HeaderValue) { InnerRequest->SetHeader(HeaderName, HeaderValue); } FString FTransactionallySafeHttpRequest::GetHeader(const FString& HeaderName) const { return InnerRequest->GetHeader(HeaderName); } TArray FTransactionallySafeHttpRequest::GetAllHeaders() const { return InnerRequest->GetAllHeaders(); } FString FTransactionallySafeHttpRequest::GetVerb() const { return InnerRequest->GetVerb(); } void FTransactionallySafeHttpRequest::SetVerb(const FString& InVerb) { InnerRequest->SetVerb(InVerb); } FString FTransactionallySafeHttpRequest::GetOption(const FName Option) const { return InnerRequest->GetOption(Option); } void FTransactionallySafeHttpRequest::SetOption(const FName Option, const FString& OptionValue) { InnerRequest->SetOption(Option, OptionValue); } void FTransactionallySafeHttpRequest::SetContent(const TArray& InPayload) { InnerRequest->SetContent(InPayload); } void FTransactionallySafeHttpRequest::SetContent(TArray&& InPayload) { InnerRequest->SetContent(MoveTemp(InPayload)); } const TArray& FTransactionallySafeHttpRequest::GetContent() const { return InnerRequest->GetContent(); } void FTransactionallySafeHttpRequest::SetContentAsString(const FString& ContentString) { InnerRequest->SetContentAsString(ContentString); } bool FTransactionallySafeHttpRequest::SetContentAsStreamedFile(const FString& Filename) { return InnerRequest->SetContentAsStreamedFile(Filename); } bool FTransactionallySafeHttpRequest::SetContentFromStream(TSharedRef Stream) { return InnerRequest->SetContentFromStream(Stream); } EHttpRequestStatus::Type FTransactionallySafeHttpRequest::GetStatus() const { return InnerRequest->GetStatus(); } const FString& FTransactionallySafeHttpRequest::GetEffectiveURL() const { return InnerRequest->GetEffectiveURL(); } FString FTransactionallySafeHttpRequest::GetURLParameter(const FString& ParameterName) const { return InnerRequest->GetURLParameter(ParameterName); } uint64 FTransactionallySafeHttpRequest::GetContentLength() const { return InnerRequest->GetContentLength(); } FString FTransactionallySafeHttpRequest::GetContentType() const { return InnerRequest->GetContentType(); } bool FTransactionallySafeHttpRequest::SetResponseBodyReceiveStream(TSharedRef Stream) { return InnerRequest->SetResponseBodyReceiveStream(MoveTemp(Stream)); } void FTransactionallySafeHttpRequest::AppendToHeader(const FString& HeaderName, const FString& AdditionalHeaderValue) { InnerRequest->AppendToHeader(HeaderName, AdditionalHeaderValue); } bool FTransactionallySafeHttpRequest::ProcessRequest() { return InnerRequest->ProcessRequest(); } void FTransactionallySafeHttpRequest::CancelRequest() { InnerRequest->CancelRequest(); } EHttpFailureReason FTransactionallySafeHttpRequest::GetFailureReason() const { return InnerRequest->GetFailureReason(); } const FHttpResponsePtr FTransactionallySafeHttpRequest::GetResponse() const { return InnerRequest->GetResponse(); } void FTransactionallySafeHttpRequest::Tick(float DeltaSeconds) { InnerRequest->Tick(DeltaSeconds); } float FTransactionallySafeHttpRequest::GetElapsedTime() const { return InnerRequest->GetElapsedTime(); } void FTransactionallySafeHttpRequest::SetDelegateThreadPolicy(EHttpRequestDelegateThreadPolicy InThreadPolicy) { InnerRequest->SetDelegateThreadPolicy(InThreadPolicy); } EHttpRequestDelegateThreadPolicy FTransactionallySafeHttpRequest::GetDelegateThreadPolicy() const { return InnerRequest->GetDelegateThreadPolicy(); } void FTransactionallySafeHttpRequest::SetTimeout(float InTimeoutSecs) { InnerRequest->SetTimeout(InTimeoutSecs); } void FTransactionallySafeHttpRequest::ClearTimeout() { InnerRequest->ClearTimeout(); } void FTransactionallySafeHttpRequest::ResetTimeoutStatus() { InnerRequest->ResetTimeoutStatus(); } TOptional FTransactionallySafeHttpRequest::GetTimeout() const { return InnerRequest->GetTimeout(); } void FTransactionallySafeHttpRequest::SetActivityTimeout(float InTimeoutSecs) { InnerRequest->SetActivityTimeout(InTimeoutSecs); } void FTransactionallySafeHttpRequest::ProcessRequestUntilComplete() { InnerRequest->ProcessRequestUntilComplete(); } FHttpRequestCompleteDelegate& FTransactionallySafeHttpRequest::OnProcessRequestComplete() { return InnerRequest->OnProcessRequestComplete(); } FHttpRequestProgressDelegate64& FTransactionallySafeHttpRequest::OnRequestProgress64() { return InnerRequest->OnRequestProgress64(); } FHttpRequestWillRetryDelegate& FTransactionallySafeHttpRequest::OnRequestWillRetry() { return InnerRequest->OnRequestWillRetry(); } FHttpRequestHeaderReceivedDelegate& FTransactionallySafeHttpRequest::OnHeaderReceived() { return InnerRequest->OnHeaderReceived(); } FHttpRequestStatusCodeReceivedDelegate& FTransactionallySafeHttpRequest::OnStatusCodeReceived() { return InnerRequest->OnStatusCodeReceived(); }