[C++] HTTP response returns nothing in a function call of a function

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.

1 Like