{"record":{"id":"4af746801c251d23","repo":"koala73/worldmonitor","slug":"unexpected-response-from-brief-service","errorCode":null,"errorMessage":"Unexpected response from brief service","messagePattern":"Unexpected response from brief service","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/components/LatestBriefPanel.ts","lineNumber":387,"sourceCode":"    // 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  /**\n   * Desktop / tester-key auth can satisfy hasPremiumAccess without a\n   * Clerk userId. /api/latest-brief is user-scoped, so there's\n   * nothing to fetch. Render a specific CTA rather than pretending\n   * this is an error state.","sourceCodeStart":369,"sourceCodeEnd":405,"githubUrl":"https://github.com/koala73/worldmonitor/blob/eeab0a219fce0f02a00603b532dbae9041b934ac/src/components/LatestBriefPanel.ts#L369-L405","documentation":"Thrown by LatestBriefPanel.fetchLatest() when the response IS ok (2xx) but the body fails the shape check: body is falsy, or body.status is neither 'ready' nor 'composing'. The transport succeeded but the contract broke — the endpoint returned 200 with something other than a valid LatestBriefResponse.","triggerScenarios":"An intermediary (proxy, CDN, service worker, redirect-to-HTML) returns 200 with an HTML page instead of JSON, so body.status is undefined; a deployed API version whose status enum gained/renamed values the client does not know; a truncated or empty body from a gateway.","commonSituations":"Client and server skew after a partial deploy (new server states, old client check); a captive portal or WAF serving an HTML interstitial with 200; a service worker returning a cached shell document for /api/latest-brief; a manual endpoint change without regenerating the shared types.","solutions":["Log res.url and the raw body text when this throws — a 200-with-HTML almost always means an intermediary or wrong URL, not a bad server","Align client and server versions: redeploy both together and keep the LatestBriefResponse status union in shared contract code","If a service worker handles /api/*, exclude authenticated API paths from its cache/shell strategy"],"exampleFix":"// before\nconst body = (await res.json()) as LatestBriefResponse;\nif (!body || (body.status !== 'ready' && body.status !== 'composing')) {\n  throw new Error('Unexpected response from brief service');\n}\n\n// after (include diagnostics):\nconst body = (await res.json()) as LatestBriefResponse;\nif (!body || (body.status !== 'ready' && body.status !== 'composing')) {\n  throw new Error(`Unexpected response from brief service (status=${JSON.stringify((body as {status?: string})?.status)})`);\n}","handlingStrategy":"type-guard","validationCode":"function isLatestBriefResponse(body: unknown): body is LatestBriefResponse {\n  const b = body as { status?: unknown } | null | undefined;\n  return b != null && (b.status === 'ready' || b.status === 'composing');\n}","typeGuard":"function isLatestBriefResponse(body: unknown): body is LatestBriefResponse {\n  const b = body as { status?: unknown } | null | undefined;\n  return b != null && (b.status === 'ready' || b.status === 'composing');\n}","tryCatchPattern":"catch (e) { if (e instanceof Error && e.message === 'Unexpected response from brief service') { logResponseBodyShape(); bustIntermediaryCaches(); showError('Brief format changed — reload the app'); } else throw e; }","preventionTips":["Deploy client and API together so the status-union check cannot skew","Exclude /api/* authenticated paths from service-worker shells and CDN HTML fallbacks","Log the raw body on this failure — a 200-with-HTML points at an intermediary, not the server"],"tags":["api-contract","response-shape","latest-brief"],"backgroundTag":"unexpected-response-shape","analyzedSha":"eeab0a219fce0f02a00603b532dbae9041b934ac","analyzedAt":"2026-08-21T16:51:25.751Z","contentChangedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}