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

  1. Check the Response in the Network tab: status, content-type, and whether a body streams at all
  2. Verify the widget-agent route always returns a streaming body for chat POSTs, never 204 or new Response(null)
  3. Retry in a clean browser profile with extensions/service workers disabled
  4. 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

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


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)