koala73/worldmonitor · error · Error
Unexpected response from brief service
Error message
Unexpected response from brief service
What it means
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.
Source
Thrown at src/components/LatestBriefPanel.ts:387
// plan they already bought (#5608).
// classifyDenialResponse reads the body ONLY on a denial status, so
// res.json() below still has an unconsumed stream on the success path.
const verdict = await classifyDenialResponse(res, readClientEntitlementBelief(getAuthState()));
if (verdict !== null) {
// Reading the body is awaited, so a gate-lock or account-switch abort
// can land mid-parse — where readDenialErrorCode swallows it. Without
// this, that abort would surface as a denial render instead of the
// no-op the abort was asking for.
if (signal.aborted) throw new DOMException('aborted while reading denial body', 'AbortError');
if (verdict === 'entitlement_desync') reportEntitlementDesync('latest-brief');
throw new BriefAccessError(verdict);
}
if (!res.ok) {
throw new Error(`Brief service unavailable (${res.status})`);
}
const body = (await res.json()) as LatestBriefResponse;
if (!body || (body.status !== 'ready' && body.status !== 'composing')) {
throw new Error('Unexpected response from brief service');
}
return body;
}
private renderLoading(): void {
clearChildren(this.content);
this.content.appendChild(
h('div', { className: 'latest-brief-empty' },
h('div', { className: 'latest-brief-empty-title' }, 'Loading your brief…'),
),
);
}
/**
* Desktop / tester-key auth can satisfy hasPremiumAccess without a
* Clerk userId. /api/latest-brief is user-scoped, so there's
* nothing to fetch. Render a specific CTA rather than pretending
* this is an error state.View on GitHub (pinned to eeab0a219f)
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
Example fix
// before
const body = (await res.json()) as LatestBriefResponse;
if (!body || (body.status !== 'ready' && body.status !== 'composing')) {
throw new Error('Unexpected response from brief service');
}
// after (include diagnostics):
const body = (await res.json()) as LatestBriefResponse;
if (!body || (body.status !== 'ready' && body.status !== 'composing')) {
throw new Error(`Unexpected response from brief service (status=${JSON.stringify((body as {status?: string})?.status)})`);
} Defensive patterns
Strategy: type-guard
Validate before calling
function isLatestBriefResponse(body: unknown): body is LatestBriefResponse {
const b = body as { status?: unknown } | null | undefined;
return b != null && (b.status === 'ready' || b.status === 'composing');
} Type guard
function isLatestBriefResponse(body: unknown): body is LatestBriefResponse {
const b = body as { status?: unknown } | null | undefined;
return b != null && (b.status === 'ready' || b.status === 'composing');
} Try / catch
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; } Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
AI-assisted analysis of koala73/worldmonitor@eeab0a219f (2026-08-21).
Data as JSON: /api/errors/4af746801c251d23.
Report an issue: GitHub.