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

  1. 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
  2. Inspect the Sentry desync report emitted by reportWidgetEntitlementDesync to see whether client belief and server state disagreed
  3. Reopen the modal after re-auth so buildWidgetAuthHeaders() rebuilds headers from a fresh session
  4. 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

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


AI-assisted analysis of koala73/worldmonitor@eeab0a219f (2026-08-21). Data as JSON: /api/errors/9bfd93eeb5ad96ed. Report an issue: GitHub.