{"record":{"id":"61fece12df7da825","repo":"decolua/9router","slug":"runway-status-r-status","errorCode":null,"errorMessage":"Runway status ${r.status}","messagePattern":"Runway status (.+?)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"open-sse/handlers/imageProviders/runwayml.js","lineNumber":37,"sourceCode":"    };\n  },\n  buildBody: (model, body) => {\n    const isVideo = !model.includes(\"image\");\n    const ratio = sizeToAspectRatio(body.size);\n    if (isVideo) {\n      return { promptText: body.prompt, model, ratio, duration: 5, ...(body.image ? { promptImage: body.image } : {}) };\n    }\n    return { promptText: body.prompt, model, ratio, ...(body.image ? { referenceImages: [{ uri: body.image }] } : {}) };\n  },\n  async parseResponse(response, { headers }) {\n    const { id } = await response.json();\n    if (!id) throw new Error(\"Runway: no task id returned\");\n    const taskUrl = `${BASE_URL}/tasks/${id}`;\n    const deadline = Date.now() + POLL_TIMEOUT_MS;\n    while (Date.now() < deadline) {\n      await sleep(POLL_INTERVAL_MS);\n      const r = await fetch(taskUrl, { headers });\n      if (!r.ok) throw new Error(`Runway status ${r.status}`);\n      const s = await r.json();\n      if (s.status === \"SUCCEEDED\") return s;\n      if (s.status === \"FAILED\" || s.status === \"CANCELLED\") throw new Error(s.failure || \"Runway task failed\");\n    }\n    throw new Error(\"Runway polling timeout\");\n  },\n  normalize: (responseBody) => {\n    const outputs = Array.isArray(responseBody.output) ? responseBody.output : [];\n    return { created: nowSec(), data: outputs.map((url) => ({ url })) };\n  },\n};\n","sourceCodeStart":19,"sourceCodeEnd":49,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/open-sse/handlers/imageProviders/runwayml.js#L19-L49","documentation":"Thrown by Runway's parseResponse when a GET to /tasks/{id} during polling returns a non-2xx status (runwayml.js:37). As with [50], the poll HTTP call itself failed — task creation succeeded but the status-check request did not, and the first bad response aborts generation with no retry.","triggerScenarios":"`fetch(taskUrl, { headers })` in the polling loop at runwayml.js:36-37 returns non-ok: 401/403 (key revoked or insufficient permissions mid-run), 404 (task id not found or deleted), 429 (Runway rate limits task-status polling), 5xx (Runway outage), or BASE_URL misconfigured so /tasks/{id} 404s on the wrong host.","commonSituations":"Long video generations (duration: 5s+) where the API key rotates/expires during the poll window; polling too aggressively for Runway's rate limits under parallel requests; Runway returning transient 5xx during incidents; X-Runway-Version header rejected for newer task endpoints; BASE_URL typo making every /tasks/{id} request 404.","solutions":["Check the reported status: 401/403 → refresh the Runway API key and ensure the X-Runway-Version header matches a supported version.","429 → lower polling frequency (increase POLL_INTERVAL_MS) or reduce concurrent Runway requests, then retry.","404 → confirm BASE_URL is the correct Runway API host and the task id path `${BASE_URL}/tasks/${id}` is well-formed.","5xx → wait and re-submit; check Runway's status page if it persists.","Harden parseResponse to retry transient 429/5xx poll responses a few times before giving up."],"exampleFix":"// before\nconst r = await fetch(taskUrl, { headers });\nif (!r.ok) throw new Error(`Runway status ${r.status}`);\n// after\nconst r = await fetch(taskUrl, { headers });\nif (!r.ok) {\n  if ((r.status === 429 || r.status >= 500) && ++pollErrors <= 3) continue;\n  throw new Error(`Runway status ${r.status}`);\n}\npollErrors = 0;","handlingStrategy":"retry","validationCode":"// Before submitting: check the task-status endpoint is reachable and auth works\nconst probe = await fetch(`${BASE_URL}/tasks`, { headers: { Authorization: `Bearer ${key}`, \"X-Runway-Version\": \"2024-11-06\" } });\nif (probe.status === 401 || probe.status === 403) throw new Error(\"Runway key invalid or lacking task-read permission\");\nif (!BASE_URL?.startsWith(\"https://\")) throw new Error(`Suspicious Runway BASE_URL: ${BASE_URL}`);","typeGuard":"function isTransientPollStatus(status) { return status === 429 || status >= 500; }","tryCatchPattern":"try {\n  const task = await imageProvider.parseResponse(response, { headers });\n} catch (e) {\n  const m = e.message.match(/^Runway status (\\d+)$/);\n  if (m) {\n    const status = Number(m[1]);\n    if (status === 401 || status === 403) await refreshRunwayKey();\n    if (isTransientPollStatus(status)) return retryWithBackoff(); // resubmit; poll errors are not persisted state\n  }\n  throw e;\n}","preventionTips":["Increase POLL_INTERVAL_MS and cap concurrent Runway tasks to stay under status-poll rate limits.","Refresh long-lived API keys before lengthy video generations.","Keep the X-Runway-Version header current; a rejected version surfaces as 4xx on task polls.","Alert on Runway status >= 500 patterns to catch provider incidents early.","Confirm BASE_URL correctness — a wrong host makes every /tasks/{id} poll 404."],"tags":["http-status","async-polling","image-generation","rate-limit","api-key"],"backgroundTag":"async-task-polling-http-error","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}