koala73/worldmonitor · error · Error
Webhook URL is not a valid URL
Error message
Webhook URL is not a valid URL
What it means
Thrown when the widget-agent responded HTTP OK but res.body is null, so res.body.getReader() (line 332) could never run. A bodyless response means the endpoint returned something like 204, or the runtime stripped the ReadableStream: old browsers, proxies/CDNs that buffer responses, or service-worker fetch interception returning a synthetic Response. The chat modal needs a streaming body to read newline-delimited widget events, so it aborts with the same localized serverError template (the interpolated status will be the OK status, e.g. 200).
Solutions
- Check the Response in the Network tab: status, content-type, and whether a body streams at all
- Verify the widget-agent route always returns a streaming body for chat POSTs, never 204 or new Response(null)
- Retry in a clean browser profile with extensions/service workers disabled
- If behind a proxy/CDN, disable response buffering for the widget-agent path (e.g., X-Accel-Buffering: no)
Example fix
// before
if (!res.body) {
throw new Error(t('widgets.serverError', { status: res.status }));
}
const reader = res.body.getReader();
// after: distinguish 'no body' from 'server error' and fail with a precise message
if (!res.body) {
throw new Error(`Widget agent returned ${res.status} with no stream body (proxy or 204?)`);
}
const reader = res.body.getReader(); Defensive patterns
Strategy: type-guard
Validate before calling
const streamSupportOk = typeof ReadableStream !== 'undefined' && typeof ReadableStream.prototype.getReader === 'function';
if (!streamSupportOk) { fallbackToNonStreaming(); } Type guard
function hasStreamBody(res: Response): res is Response & { body: ReadableStream<Uint8Array> } {
return res.body instanceof ReadableStream;
} Try / catch
const res = await fetch(widgetAgentUrl(), opts);
if (!hasStreamBody(res)) {
throw new Error(`Widget agent returned ${res.status} without a stream body`);
} Prevention
- Never return 204 or new Response(null) from streaming endpoints
- Disable response buffering on proxies/CDNs in front of the widget agent
- Test widget chat in a clean profile to catch extension service-worker interception
- Assert res.body instanceof ReadableStream before calling getReader()
When it happens
Trigger: Widget agent returned 204 No Content on some code path; a corporate proxy or CDN disabled streaming for the route; a polyfilled fetch whose Response lacks a ReadableStream body; an extension service worker intercepting fetch and returning new Response(null).
Common situations: Deploying the widget agent behind a proxy that does not support SSE; browser extensions intercepting fetch; jsdom/Node test environments where fetch semantics differ; an edge function branch returning new Response(null) instead of a stream.
Related errors
- Cloud fallback blocked for
- Convex embed key validation unavailable: fetch-error
- DNS lookup failed: HTTP
- DNS lookup failed: status
- ${err.message}
AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-08-21).
Data as JSON: /api/errors/d8454a4b017ba5aa.
Report an issue: GitHub.
Appendix: source
Thrown at api/_notification-webhook-ssrf.ts:239
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.
*/
export async function assertNotificationWebhookRegistrationUrlSafe(
rawUrl: string,
resolveHostname: ResolveHostname = defaultResolveHostname,
): Promise<void> {
const staticError = blockedNotificationWebhookUrlReason(rawUrl);
if (staticError) throw new Error(staticError);
const hostname = new URL(rawUrl).hostname.toLowerCase();
if (isIpLiteral(hostname)) return;
let resolvedAddresses: string[];
try {
resolvedAddresses = await resolveHostname(hostname);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(`Webhook URL DNS resolution failed: ${message}`);
}
if (!resolvedAddresses.length) throw new Error('Webhook URL DNS resolution returned no addresses');
if (resolvedAddresses.some(isBlockedNotificationResolvedAddress)) {
throw new Error('Webhook URL must not point to a private/local address');
}
}
View on GitHub (pinned to 7d06c8633d)