koala73/worldmonitor · error · Error
DNS ${recordType} lookup failed: status ${data.Status}
Error message
DNS ${recordType} lookup failed: status ${data.Status} What it means
Thrown by the widget chat composer when POST widgetAgentUrl() returns a non-OK status. Before throwing, the modal tries to parse the body as a WidgetAgentHealth payload and calls reportWidgetEntitlementDesync() with the status, whether a tester key was used, the client's entitlement belief, and the captured user id. It is the generic client-side surface for auth (401), entitlement/tier (402/403), rate-limit (429), and server (5xx) rejections from the widget-agent edge endpoint; the localized 'widgets.serverError' template renders the numeric status into the chat transcript.
Source
Thrown at api/_notification-webhook-ssrf.ts:198
}
return null;
}
async function resolveDnsJson(hostname: string, recordType: 'A' | 'AAAA'): Promise<string[]> {
const url = new URL(DNS_JSON_ENDPOINT);
url.searchParams.set('name', hostname);
url.searchParams.set('type', recordType);
const response = await fetch(url, {
headers: {
Accept: 'application/dns-json',
'User-Agent': 'WorldMonitor-Notification-Webhooks/1.0',
},
signal: AbortSignal.timeout(DNS_RESOLUTION_TIMEOUT_MS),
});
if (!response.ok) throw new Error(`DNS ${recordType} lookup failed: HTTP ${response.status}`);
const data = await response.json() as { Status?: number; Answer?: Array<{ type?: number; data?: string }> };
if (data.Status !== 0) throw new Error(`DNS ${recordType} lookup failed: status ${data.Status}`);
const expectedType = recordType === 'A' ? 1 : 28;
return (data.Answer ?? [])
.filter(answer => answer.type === expectedType && typeof answer.data === 'string')
.map(answer => answer.data!);
}
async function defaultResolveHostname(hostname: string): Promise<string[]> {
const records = await Promise.all([
resolveDnsJson(hostname, 'A'),
resolveDnsJson(hostname, 'AAAA'),
]);
return records.flat();
}
/**
* Fail fast at registration when the webhook hostname currently resolves to a
* private or reserved address. Delivery repeats this check (and pins its
* connection) because DNS can change after registration.View on GitHub (pinned to eeab0a219f)
Solutions
- Branch on the numeric status in the message: 401 re-authenticate, 402/403 verify Pro entitlement and widget/pro keys, 429 wait and retry, 5xx check widget-agent health and logs
- Inspect the Sentry desync report emitted by reportWidgetEntitlementDesync to see whether client belief and server state disagreed
- Reopen the modal after re-auth so buildWidgetAuthHeaders() rebuilds headers from a fresh session
- If persistent, confirm the widgetAgentUrl() target and edge deployment are correct for the environment
Example fix
// before
const res = await fetch(widgetAgentUrl(), { method: 'POST', headers: reqHeaders, body });
if (!res.ok) throw new Error(t('widgets.serverError', { status: res.status }));
// after: act on the status before surfacing the generic error
const res = await fetch(widgetAgentUrl(), { method: 'POST', headers: reqHeaders, body });
if (!res.ok) {
if (res.status === 401) { await promptSignIn(); return; }
if (res.status === 429) { scheduleRetryAfter(res); return; }
throw new Error(t('widgets.serverError', { status: res.status }));
} Defensive patterns
Strategy: try-catch
Validate before calling
const authState = getAuthState();
if (isPro && !authState.user) { promptSignIn(); return; }
if (!navigator.onLine) { queueOffline(); return; } Try / catch
try {
const res = await fetch(widgetAgentUrl(), { method: 'POST', headers, body, signal });
if (!res.ok) {
if (res.status === 401 || res.status === 403) await promptReauth();
else if (res.status >= 500 || res.status === 429) scheduleRetry();
throw new Error(t('widgets.serverError', { status: res.status }));
}
} catch (e) {
setFooterStatus(footerEl, e instanceof Error ? e.message : String(e), 'error');
} Prevention
- Rebuild auth headers via buildWidgetAuthHeaders() immediately before each request
- Capture requestBelief/userId before the fetch so desync reports carry context, mirroring the modal
- Handle 401/403/429/5xx distinctly instead of flattening to one message
- Monitor reportWidgetEntitlementDesync output to catch client/server entitlement divergence early
When it happens
Trigger: buildWidgetAuthHeaders(isPro) produced stale or absent auth (expired Clerk session token, missing pro key) and the endpoint answered 401; the client believed it had Pro entitlement but the server disagreed (desync) returning 402/403; widget-agent upstream 5xx; tester key (X-WorldMonitor-Key) used where invalid.
Common situations: Session token expiring during a long chat; plan downgrade not yet reflected client-side (the desync Sentry report exists to catch exactly this); Vercel edge function misconfiguration or backend outage; account switch invalidating in-flight tokens.
Related errors
- HTTP ${res.status}
- HTTP ${resp.status}
- DNS ${recordType} lookup failed: HTTP ${response.status}
- Webhook URL is not a valid URL
- Webhook URL must use HTTPS
AI-assisted analysis of koala73/worldmonitor@eeab0a219f (2026-08-21).
Data as JSON: /api/errors/9bfd93eeb5ad96ed.
Report an issue: GitHub.