{"record":{"id":"c24708e92156d9d2","repo":"koala73/worldmonitor","slug":"brief-service-unavailable-res-status","errorCode":null,"errorMessage":"Brief service unavailable (${res.status})","messagePattern":"Brief service unavailable \\((.+?)\\)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/components/LatestBriefPanel.ts","lineNumber":383,"sourceCode":"    // returns 403 for BOTH a free plan (`pro_required`) and a rejected\n    // origin (`Origin not allowed`), and a `pro_required` the client's own\n    // entitlement state contradicts is a server-side desync — rendering\n    // any of those as \"Upgrade to Pro\" tells a paying user to buy the\n    // plan they already bought (#5608).\n    // classifyDenialResponse reads the body ONLY on a denial status, so\n    // res.json() below still has an unconsumed stream on the success path.\n    const verdict = await classifyDenialResponse(res, readClientEntitlementBelief(getAuthState()));\n    if (verdict !== null) {\n      // Reading the body is awaited, so a gate-lock or account-switch abort\n      // can land mid-parse — where readDenialErrorCode swallows it. Without\n      // this, that abort would surface as a denial render instead of the\n      // no-op the abort was asking for.\n      if (signal.aborted) throw new DOMException('aborted while reading denial body', 'AbortError');\n      if (verdict === 'entitlement_desync') reportEntitlementDesync('latest-brief');\n      throw new BriefAccessError(verdict);\n    }\n    if (!res.ok) {\n      throw new Error(`Brief service unavailable (${res.status})`);\n    }\n    const body = (await res.json()) as LatestBriefResponse;\n    if (!body || (body.status !== 'ready' && body.status !== 'composing')) {\n      throw new Error('Unexpected response from brief service');\n    }\n    return body;\n  }\n\n  private renderLoading(): void {\n    clearChildren(this.content);\n    this.content.appendChild(\n      h('div', { className: 'latest-brief-empty' },\n        h('div', { className: 'latest-brief-empty-title' }, 'Loading your brief…'),\n      ),\n    );\n  }\n\n  /**","sourceCodeStart":365,"sourceCodeEnd":401,"githubUrl":"https://github.com/koala73/worldmonitor/blob/eeab0a219fce0f02a00603b532dbae9041b934ac/src/components/LatestBriefPanel.ts#L365-L401","documentation":"Thrown by LatestBriefPanel.fetchLatest() when the response to /api/latest-brief is non-OK and was NOT classified as a denial. The classification step (classifyDenialResponse on 401/403, covering free-plan 'pro_required' and rejected origins) runs first and throws BriefAccessError instead — so reaching this message means a status outside that contract: typically 5xx from the brief composer, 429 rate limiting, or 502/504 from a gateway.","triggerScenarios":"GET /api/latest-brief returns 500 (brief generation crashed), 502/503 (Railway service down or redeploying), 429 (user or shared quota throttled), or any other non-OK status that is not a 401/403 denial.","commonSituations":"Brief service deployment in progress; upstream LLM/composition dependency failing; a user hammering refresh hitting rate limits; Vercel function timeout surfacing as 504.","solutions":["Retry with backoff for 5xx/429 — the brief endpoint is transient-failure prone; abort any in-flight fetch first via the existing signal","Check the brief service health endpoint and recent deploys if the status persists","Capture res.status in telemetry: distinguishing 429 (back off longer) from 503 (redeploy) changes the retry policy"],"exampleFix":"// before\nif (!res.ok) throw new Error(`Brief service unavailable (${res.status})`); // caller shows generic error\n\n// after (caller-side retry policy keyed on status):\ncatch (e) {\n  if (e instanceof Error && e.message.startsWith('Brief service unavailable (')) {\n    const status = Number(e.message.slice(-4, -1));\n    if (status === 429 || status >= 500) await retryWithBackoff(() => panel.refresh());\n    else showError(e);\n  }\n}","handlingStrategy":"retry","validationCode":"// nothing client-side can pre-validate a 5xx; gate the retry on prior knowledge:\nif (briefServiceHealthy === false) return; // skip refresh while known unhealthy","typeGuard":null,"tryCatchPattern":"catch (e) { const m = /^Brief service unavailable \\((\\d+)\\)$/.exec(e instanceof Error ? e.message : ''); if (m) { const status = Number(m[1]); if (status === 429 || status >= 500) await retryWithBackoff(refresh, { signal }); else showError(e); } else throw e; }","preventionTips":["Only retry 429/5xx with backoff — other statuses mean something is genuinely wrong","Emit res.status to telemetry so 429 vs 503 policies diverge","Abort in-flight fetches (the panel's signal) before retrying to avoid double renders"],"tags":["network","http-status","latest-brief","retry"],"backgroundTag":"http-error-status","analyzedSha":"eeab0a219fce0f02a00603b532dbae9041b934ac","analyzedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-08-23T13:39:53.451Z"}