{"record":{"id":"9993ce3e0af7163e","repo":"decolua/9router","slug":"nanobanana-status-r-status","errorCode":null,"errorMessage":"NanoBanana status ${r.status}","messagePattern":"NanoBanana status (.+?)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"open-sse/handlers/imageProviders/nanobanana.js","lineNumber":47,"sourceCode":"    if (isEdit) {\n      const urls = Array.isArray(body.images) ? body.images.filter(Boolean) : [];\n      if (body.image) urls.push(body.image);\n      req.imageUrls = urls;\n    }\n    return req;\n  },\n  // Async: parse submit → poll until SUCCESS, return raw poll data\n  async parseResponse(response, { headers }) {\n    const submitData = await response.json();\n    if (submitData.code !== 200) throw new Error(submitData.msg || \"NanoBanana submit failed\");\n    const taskId = submitData.data?.taskId;\n    if (!taskId) throw new Error(\"NanoBanana: no taskId returned\");\n    const pollUrl = `${POLL_BASE}?taskId=${encodeURIComponent(taskId)}`;\n    const deadline = Date.now() + POLL_TIMEOUT_MS;\n    while (Date.now() < deadline) {\n      await sleep(POLL_INTERVAL_MS);\n      const r = await fetch(pollUrl, { headers });\n      if (!r.ok) throw new Error(`NanoBanana status ${r.status}`);\n      const s = await r.json();\n      const flag = s.data?.successFlag;\n      if (flag === 1) return s.data;\n      if (flag === 2 || flag === 3) throw new Error(s.data?.errorMessage || \"NanoBanana generation failed\");\n    }\n    throw new Error(\"NanoBanana polling timeout\");\n  },\n  normalize: (responseBody, prompt) => {\n    const url = responseBody.response?.resultImageUrl || responseBody.response?.originImageUrl;\n    if (url) return { created: nowSec(), data: [{ url, revised_prompt: prompt }] };\n    return { created: nowSec(), data: [] };\n  },\n};\n","sourceCodeStart":29,"sourceCodeEnd":61,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/open-sse/handlers/imageProviders/nanobanana.js#L29-L61","documentation":"Thrown by NanoBanana's parseResponse while polling the async generation task endpoint. Every poll fetch is checked with response.ok, and any non-2xx poll response (4xx/5xx) aborts the whole image generation immediately — there is no retry on transient poll failures. It means the poll HTTP call itself failed, not that the generation failed.","triggerScenarios":"A `fetch(pollUrl, { headers })` inside the polling loop at open-sse/handlers/imageProviders/nanobanana.js:46-47 returns a non-ok status (e.g. 401 because the Bearer token expired mid-poll, 404 because the taskId expired upstream, 429 rate limit, or 5xx upstream outage).","commonSituations":"Long-running generations outliving a short-lived API key/accessToken; the provider expiring or garbage-collecting the taskId; provider-side rate limiting under concurrent image requests; temporary upstream 5xx during the polling window (POLL_TIMEOUT_MS); misconfigured pollUrl in PROVIDER_MEDIA['nanobanana'].imageConfig hitting a wrong host.","solutions":["Check the reported HTTP status: 401/403 → refresh or replace the NanoBanana API key/accessToken in the provider credentials, then retry.","429 → reduce concurrency or add client-side backoff before re-submitting the image request.","404 → the taskId expired or the poll URL is wrong; verify imageConfig.pollUrl in PROVIDER_MEDIA['nanobanana'] and re-submit a fresh generation.","5xx → retry after a short wait; if persistent, check the provider's status page or network/proxy connectivity.","If transient statuses are frequent for you, patch/wrap parseResponse to tolerate (retry a few times) 429/5xx poll responses instead of throwing on the first non-ok."],"exampleFix":"// before\nconst r = await fetch(pollUrl, { headers });\nif (!r.ok) throw new Error(`NanoBanana status ${r.status}`);\n// after\nconst r = await fetch(pollUrl, { headers });\nif (!r.ok) {\n  if ((r.status === 429 || r.status >= 500) && ++pollErrors <= 3) continue; // tolerate transient poll errors\n  throw new Error(`NanoBanana status ${r.status}`);\n}\npollErrors = 0;","handlingStrategy":"try-catch","validationCode":"// Before calling: ensure a NanoBanana credential exists and the poll config is present\nconst cfg = PROVIDER_MEDIA[\"nanobanana\"]?.imageConfig;\nif (!cfg?.baseUrl || !cfg?.pollUrl) throw new Error(\"NanoBanana imageConfig (baseUrl/pollUrl) missing\");\nif (!(creds?.apiKey || creds?.accessToken)) throw new Error(\"NanoBanana API key/accessToken missing\");","typeGuard":"function isOkResponse(r) { return typeof r === \"object\" && r !== null && typeof r.ok === \"boolean\" && r.ok; }","tryCatchPattern":"try {\n  const result = await imageProvider.parseResponse(response, { headers });\n} catch (e) {\n  if (/^NanoBanana status \\d+$/.test(e.message)) {\n    const status = Number(e.message.match(/\\d+/)?.[0]);\n    if (status === 401 || status === 403) await refreshCredentials();\n    // retry submission for transient 429/5xx\n    if (status === 429 || status >= 500) return retryWithBackoff();\n  }\n  throw e;\n}","preventionTips":["Rotate/refresh NanoBanana credentials before long async generations, not just at submit time.","Keep imageConfig.pollUrl in PROVIDER_MEDIA in sync with the provider's current API.","Throttle concurrent image generations to avoid poll-endpoint 429s.","Monitor for this error pattern and alert on repeated non-ok poll statuses per provider."],"tags":["http-status","async-polling","image-generation","api-key","rate-limit"],"backgroundTag":"async-task-polling-http-error","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}