koala73/worldmonitor · error · Error
Brief service unavailable (${res.status})
Error message
Brief service unavailable (${res.status}) What it means
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.
Source
Thrown at src/components/LatestBriefPanel.ts:383
// returns 403 for BOTH a free plan (`pro_required`) and a rejected
// origin (`Origin not allowed`), and a `pro_required` the client's own
// entitlement state contradicts is a server-side desync — rendering
// any of those as "Upgrade to Pro" tells a paying user to buy the
// 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…'),
),
);
}
/**View on GitHub (pinned to eeab0a219f)
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
Example fix
// before
if (!res.ok) throw new Error(`Brief service unavailable (${res.status})`); // caller shows generic error
// after (caller-side retry policy keyed on status):
catch (e) {
if (e instanceof Error && e.message.startsWith('Brief service unavailable (')) {
const status = Number(e.message.slice(-4, -1));
if (status === 429 || status >= 500) await retryWithBackoff(() => panel.refresh());
else showError(e);
}
} Defensive patterns
Strategy: retry
Validate before calling
// nothing client-side can pre-validate a 5xx; gate the retry on prior knowledge: if (briefServiceHealthy === false) return; // skip refresh while known unhealthy
Try / catch
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; } Prevention
- 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
When it happens
Trigger: 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.
Common situations: Brief service deployment in progress; upstream LLM/composition dependency failing; a user hammering refresh hitting rate limits; Vercel function timeout surfacing as 504.
Understand the failure class
- HTTP status errors: handling 4xx and 5xx responses — how to handle 4xx and 5xx responses properly.
Related errors
- HTTP ${resp.status}
- HTTP ${resp.status}
- HTTP ${resp.status}
- HTTP ${res.status}
- DNS ${recordType} lookup failed: status ${data.Status}
AI-assisted analysis of koala73/worldmonitor@eeab0a219f (2026-08-21).
Data as JSON: /api/errors/c24708e92156d9d2.
Report an issue: GitHub.