{"record":{"id":"b81ba9b0e68adb33","repo":"nexu-io/open-design","slug":"leonardo-ai-poll-pollresp-status","errorCode":null,"errorMessage":"leonardo.ai poll ${pollResp.status}","messagePattern":"leonardo\\.ai poll (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/daemon/src/media/index.ts","lineNumber":2265,"sourceCode":"  }\n  \n  // Poll for completion\n  const maxPollMs = 120000; // 2 minutes\n  const pollIntervalMs = 2000; // 2 seconds\n  const startedAt = Date.now();\n  let imageUrl: string | null = null;\n  \n  while (Date.now() - startedAt < maxPollMs) {\n    await new Promise(resolve => setTimeout(resolve, pollIntervalMs));\n    \n    const pollResp = await fetch(`${baseUrl}/generations/${generationId}`, withMediaRequestInit(ctx, {\n      headers: {\n        'authorization': `Bearer ${credentials.apiKey}`,\n      },\n    }));\n    \n    if (!pollResp.ok) {\n      throw new Error(`leonardo.ai poll ${pollResp.status}`);\n    }\n    \n    const pollData = (await pollResp.json()) as Record<string, any>;\n    const generation = pollData?.generations_by_pk;\n    \n    if (generation?.status === 'COMPLETE') {\n      const images = generation?.generated_images;\n      if (Array.isArray(images) && images.length > 0) {\n        imageUrl = images[0]?.url;\n        break;\n      }\n    } else if (generation?.status === 'FAILED') {\n      throw new Error('leonardo.ai generation failed');\n    }\n  }\n  \n  if (!imageUrl) {\n    throw new Error('leonardo.ai generation timed out after 2 minutes');","sourceCodeStart":2247,"sourceCodeEnd":2283,"githubUrl":"https://github.com/nexu-io/open-design/blob/5be4028344c2eb4c667c5a97bda8f750c5597ef7/apps/daemon/src/media/index.ts#L2247-L2283","documentation":"Thrown inside the Leonardo polling loop when GET {baseUrl}/generations/{generationId} returns a non-2xx status. The message reports only the HTTP status code (no body). Distinct from [468] (FAILED status) and [469] (timeout): this fires on transport/auth-level rejection of the poll request itself.","triggerScenarios":"Polling loop calls /generations/{id} and pollResp.ok is false — 401/403 (key lost scope mid-job), 404 (generationId doesn't exist or was purged), 429 (poll rate-limit), or 5xx (Leonardo outage).","commonSituations":"Polling too aggressively hitting Leonardo rate limits; generationId expired from Leonardo's retention window before completion; API key rotated mid-job; transient Leonardo 5xx during a poll window.","solutions":["Retry the entire generation — transient 5xx and 429 on polls usually resolve on a fresh submit.","If 401/403: the key was likely invalidated mid-job; refresh LEONARDO_API_KEY and resubmit.","If 404: the generationId is gone — the job was purged or never durably created; resubmit and confirm the submit step returned a real id.","If 429 persists: increase pollIntervalMs (currently 2000ms) for your deployment to back off."],"exampleFix":"// before — single poll attempt throws on any non-2xx\nif (!pollResp.ok) {\n  throw new Error(`leonardo.ai poll ${pollResp.status}`);\n}\n\n// after — tolerate transient 5xx/429 inside the poll window\nif (!pollResp.ok) {\n  if ((pollResp.status === 429 || pollResp.status >= 500) && Date.now() - startedAt < maxPollMs) {\n    await new Promise(r => setTimeout(r, 5000));\n    continue;\n  }\n  throw new Error(`leonardo.ai poll ${pollResp.status}`);\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"// Poll failures are dominated by transient 5xx/429; tolerate them inside the\n// poll window, only throw on auth or definitive not-found.\nwhile (Date.now() - startedAt < maxPollMs) {\n  await new Promise(r => setTimeout(r, pollIntervalMs));\n  let pollResp: Response;\n  try {\n    pollResp = await fetch(`${baseUrl}/generations/${generationId}`, withMediaRequestInit(ctx, { headers: { authorization: `Bearer ${credentials.apiKey}` } }));\n  } catch (e) { throw new Error(`leonardo.ai poll network error: ${String(e)}`); }\n  if (!pollResp.ok) {\n    if ((pollResp.status === 429 || pollResp.status >= 500) && Date.now() - startedAt < maxPollMs) continue;\n    throw new Error(`leonardo.ai poll ${pollResp.status}`);\n  }\n  // ... handle pollData\n}","preventionTips":["Treat poll 5xx and 429 as retryable inside the ceiling rather than terminal; the current code throws on every non-2xx.","Use a jittered backoff on 429 to avoid thundering-herd against Leonardo's rate limiter.","Track per-generationId poll status codes in logs to detect chronic 4xx that signals key revocation mid-job."],"tags":["leonardo","polling","http-status","transient"],"backgroundTag":null,"analyzedSha":"5be4028344c2eb4c667c5a97bda8f750c5597ef7","analyzedAt":"2026-08-12T12:03:58.812Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}