From 33b5fc3b0e5f8dd8cf9980441cb3d86aa7e018a1 Mon Sep 17 00:00:00 2001 From: chiyi Date: Thu, 9 Jul 2026 17:06:43 +0800 Subject: [PATCH] fix(runway): retry the /v1/tasks submit on transient errors A dropped proxy connection (EOF) or 5xx on the task-create failed the whole generation. submitTask now retries transient (ErrTemporaryUpstream) submits up to 3x with backoff, honoring ctx. Applies to both Nano Banana image and Gen-4 video. Validated: 10/10 4K images (19-24MB PNG each) generated across 10 accounts. --- backend/internal/provider/runway/image.go | 3 +-- backend/internal/provider/runway/video.go | 20 +++++++++++++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/backend/internal/provider/runway/image.go b/backend/internal/provider/runway/image.go index dde1c56..32e27db 100644 --- a/backend/internal/provider/runway/image.go +++ b/backend/internal/provider/runway/image.go @@ -9,7 +9,6 @@ import ( "strings" "time" - http "github.com/bogdanfinn/fhttp" tlsclient "github.com/bogdanfinn/tls-client" "github.com/google/uuid" ) @@ -127,7 +126,7 @@ func (c *Client) createImageTask(ctx context.Context, client tlsclient.HttpClien if len(refImages) > 0 { opts["reference_images"] = refImages } - res, err := c.apiJSON(ctx, client, token, teamID, http.MethodPost, "/v1/tasks", map[string]any{ + res, err := c.submitTask(ctx, client, token, teamID, map[string]any{ "taskType": "gemini_3_1_flash_image", "options": opts, "asTeamId": jsonNumberOrString(teamID), diff --git a/backend/internal/provider/runway/video.go b/backend/internal/provider/runway/video.go index 463ceb7..53f33d7 100644 --- a/backend/internal/provider/runway/video.go +++ b/backend/internal/provider/runway/video.go @@ -213,7 +213,7 @@ func (c *Client) createTask(ctx context.Context, client tlsclient.HttpClient, to if assetGroupID != "" { opts["assetGroupId"] = assetGroupID } - res, err := c.apiJSON(ctx, client, token, teamID, http.MethodPost, "/v1/tasks", map[string]any{ + res, err := c.submitTask(ctx, client, token, teamID, map[string]any{ "taskType": "gen4_turbo", "options": opts, "asTeamId": jsonNumberOrString(teamID), @@ -271,6 +271,24 @@ func (c *Client) pollTask(ctx context.Context, client tlsclient.HttpClient, toke } } +// submitTask POSTs a /v1/tasks create, retrying a few times on transient +// (network / 5xx) failures. A dropped proxy connection ("EOF") on the submit +// would otherwise fail the whole generation even though a quick retry succeeds. +func (c *Client) submitTask(ctx context.Context, client tlsclient.HttpClient, token, teamID string, body map[string]any) (map[string]any, error) { + var res map[string]any + var err error + for attempt := 0; attempt < 3; attempt++ { + res, err = c.apiJSON(ctx, client, token, teamID, http.MethodPost, "/v1/tasks", body) + if err == nil || !errors.Is(err, ErrTemporaryUpstream) { + return res, err + } + if sleepCtx(ctx, time.Duration(attempt+1)*time.Second) != nil { + return nil, ctx.Err() + } + } + return res, err +} + // apiJSON performs an authed JSON request against the Runway API and returns the // parsed body, mapping status codes to the shared provider error sentinels. func (c *Client) apiJSON(ctx context.Context, client tlsclient.HttpClient, token, teamID, method, path string, body any) (map[string]any, error) {