Quick answer: In your UBlueprintAsyncActionBase, override Activate() to start the work, Broadcast on the output delegate when complete (from the game thread), then call SetReadyToDestroy(). Missing any of those leaves the BP node hanging.
You wrote a custom “DownloadJSON” BP node. It runs the HTTP request fine; the output pin never fires. The async wrapper class needs three things in the right order to satisfy the BP execution model.
The Symptom
Custom BP async node visibly executes its input but never reaches its OnSuccess / OnFailure pins. Or it fires once and crashes on the second invocation. Or works in editor and not in shipping.
The Required Pattern
// Header
UCLASS()
class UDownloadJSONAction : public UBlueprintAsyncActionBase
{
GENERATED_BODY()
public:
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnComplete, FString, Json);
UPROPERTY(BlueprintAssignable)
FOnComplete OnSuccess;
UPROPERTY(BlueprintAssignable)
FOnComplete OnFailure;
UFUNCTION(BlueprintCallable, meta=(BlueprintInternalUseOnly="true"))
static UDownloadJSONAction* DownloadJSON(UObject* WorldContextObject, FString Url);
virtual void Activate() override;
private:
FString Url;
};
// Cpp
UDownloadJSONAction* UDownloadJSONAction::DownloadJSON(UObject* WorldContextObject, FString Url)
{
UDownloadJSONAction* Action = NewObject<UDownloadJSONAction>();
Action->Url = Url;
Action->RegisterWithGameInstance(WorldContextObject);
return Action;
}
void UDownloadJSONAction::Activate()
{
FHttpRequestRef Req = FHttpModule::Get().CreateRequest();
Req->SetURL(Url);
Req->OnProcessRequestComplete().BindLambda([this](FHttpRequestPtr R, FHttpResponsePtr Resp, bool bOk)
{
AsyncTask(ENamedThreads::GameThread, [this, Resp, bOk]()
{
if (bOk && Resp.IsValid())
OnSuccess.Broadcast(Resp->GetContentAsString());
else
OnFailure.Broadcast(TEXT(""));
SetReadyToDestroy();
});
});
Req->ProcessRequest();
}
Three pieces:
- Activate() kicks off the work.
- Broadcast on game thread via AsyncTask if your callback is on a worker.
- SetReadyToDestroy() marks the action collectible so it doesn’t leak.
Common Mistakes
- Doing the work in the static factory instead of Activate. Factory runs before BP wires up; output pins aren’t bound yet.
- Forgetting AsyncTask. Broadcasting from a worker thread that touches UObjects can crash.
- Forgetting SetReadyToDestroy. Action stays alive holding the lambda capture.
Verifying
Trigger the BP node. Watch the output pin fire on completion. Place a Print String on each output pin to confirm. Check logs for any “async action leaked” warnings on PIE end.
“Activate. Broadcast on game thread. ReadyToDestroy. The pin fires.”
Related Issues
For input modifier stuck, see input modifier. For Niagara CPU/GPU readback, see CPU/GPU readback.
Three steps. Pin fires. Memory frees.