Hmm latent actions are a bit specific. It requires creating an additional LatentAction object and bind pointers as output pins.
I can straight up give you an old implementation of mine that uses latent action.
It’s a bit more complex than not using latent action, but does roughly the same thing I described above.
In short, I created a wrapper actor to contain a request. When I call my static request method, I spawn the request actor and execute the request. Upon completion, I set a flag to true and the latent action knows to trigger the output execution pin.
// .h
class ABlueprintHttpRequest : public AActor
{
GENERATED_UCLASS_BODY()
TSharedPtr<IHttpRequest> Request;
bool Done = false;
bool Success = false;
int32 Code = 0;
FString Body;
void ProcessGet(const FString& Url);
void OnResponseReceived(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful);
};
UCLASS()
class UBlueprintHttp : public UBlueprintFunctionLibrary
{
GENERATED_UCLASS_BODY()
UFUNCTION(BlueprintCallable, meta = (
Keywords = "http request get",
Latent,
WorldContext = "WorldContextObject",
DefaultToSelf = "WorldContextObject",
LatentInfo = "LatentInfo"
), Category = "BlueprintHttp")
static void HttpGet(UObject* WorldContextObject,
/* in */ const FString& Url,
/* out */ bool& Success,
/* out */ FString& Body,
/* out */ int32& Code,
FLatentActionInfo LatentInfo
);
};
//.cpp
class FBlueprintHttpAction : public FPendingLatentAction
{
private:
ABlueprintHttpRequest* Req;
// Output pins
bool* pSuccess;
int32* pCode;
FString* pBody;
public:
FName ExecutionFunction;
int32 OutputLink;
FWeakObjectPtr CallbackTarget;
FBlueprintHttpAction(ABlueprintHttpRequest* Req, bool* pSuccess, int32* pCode, FString* pBody, const FLatentActionInfo& LatentInfo)
: Req(Req)
, pSuccess(pSuccess)
, pCode(pCode)
, pBody(pBody)
, ExecutionFunction(LatentInfo.ExecutionFunction)
, OutputLink(LatentInfo.Linkage)
, CallbackTarget(LatentInfo.CallbackTarget)
{
}
virtual void UpdateOperation(FLatentResponse& Response) override
{
if (Req)
{
if (Req->Done)
{
(*pSuccess) = Req->Success;
(*pCode) = Req->Code;
(*pBody) = Req->Body;
Response.FinishAndTriggerIf(true, ExecutionFunction, OutputLink, CallbackTarget);
// Request is not needed anymore, clean it up
Req->Destroy();
Req = nullptr;
}
}
else
{
// Request wrapper got cleaned up (levelchange/exit) before request finished
// Note that the underlying request is not level-bound and will finish anyways (unless exit?)
Response.DoneIf(true);
}
}
// Object owning this latent node got destroyed - useless to wait for result
// Should we cancel the underlying request or let it finish ?
virtual void NotifyObjectDestroyed() override
{
if (Req)
{
//Req->Cancel();
Req->Destroy();
Req = nullptr;
}
}
};
// Blueprint static call
void UBlueprintHttp::HttpGet(UObject* WorldContextObject,
/* in */ const FString& Url,
/* out */ bool& Success,
/* out */ FString& Body,
/* out */ int32& Code,
FLatentActionInfo LatentInfo)
{
if (UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject))
{
FLatentActionManager& LatentActionManager = World->GetLatentActionManager();
if (LatentActionManager.FindExistingAction<FBlueprintHttpAction>(LatentInfo.CallbackTarget, LatentInfo.UUID) == NULL)
{
UE_LOG(LogTemp, Log, TEXT("[BlueprintHttp] Http GET %s"), *Url);
FActorSpawnParameters SpawnParams;
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
SpawnParams.bNoFail = true;
auto Req = World->SpawnActor<ABlueprintHttpRequest>(SpawnParams);
FBlueprintHttpAction* HttpAction = new FBlueprintHttpAction(Req, &Success, &Code, &Body, LatentInfo);
LatentActionManager.AddNewAction(LatentInfo.CallbackTarget, LatentInfo.UUID, HttpAction);
Req->ProcessGet(Url);
}
}
}
ABlueprintHttpRequest::ABlueprintHttpRequest(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
PrimaryActorTick.bCanEverTick = false;
Request = FHttpModule::Get().CreateRequest();
Request->SetHeader("User-Agent", "X-UnrealEngine-Agent");
Request->SetHeader("Content-Type", "application/json");
Request->SetHeader("Accept", "application/json");
}
void ABlueprintHttpRequest::ProcessGet(const FString& Url)
{
Request->SetURL(Url);
Request->SetVerb("GET");
Request->OnProcessRequestComplete().BindUObject(this, &ABlueprintHttpRequest::OnResponseReceived);
Request->ProcessRequest();
}
void ABlueprintHttpRequest::OnResponseReceived(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful)
{
// Happens when eg. hostname doesn't resolve
if (!Response.IsValid())
{
Done = true;
this->Request.Reset();
return;
}
Success = bWasSuccessful;
Code = Response->GetResponseCode();
Body = Response->GetContentAsString();
if (bWasSuccessful)
{
//...
}
else
{
UE_LOG(LogTemp, Warning, TEXT("[BlueprintHttpRequest] Request failed (%i)"), Code);
}
Done = true;
this->Request.Reset(); // our reference is no longer necessary
}
This is old 4.15 code but it shows the mechanisms.
I wouldn’t really recommend doing a latent action though, as those come with a slew of pitfalls. I had to use latent action for specific reasons, but if I had to do it in a “normal” project I would do it differently :
-
I would still wrap the request in an object, although probably subclass UObject instead of AActor. In the wrapper I would add a blueprint-bindable event dispatcher for request completion. Upon completion, simply broadcast the dispatcher with results.
-
Don’t use a latent action. Instead, create the request wrapper object and return it to the caller (blueprint). Then blueprint is responsible for binding the dispatcher to some custom event.