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:

  1. Activate() kicks off the work.
  2. Broadcast on game thread via AsyncTask if your callback is on a worker.
  3. SetReadyToDestroy() marks the action collectible so it doesn’t leak.

Common Mistakes

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.