koala73/worldmonitor · error · Error

Webhook URL is not a valid URL

Error message

Webhook URL is not a valid URL

What it means

Thrown by blockedNotificationWebhookUrlReason when `new URL(rawUrl)` raises — i.e. the supplied webhook string is not a parseable absolute URL (missing protocol, unencoded spaces, stray characters). It is the first static gate inside assertNotificationWebhookRegistrationUrlSafe, so it fails fast at webhook registration before any DNS work.

Source

Thrown at api/_notification-webhook-ssrf.ts:223

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 ffec79ac33)

Solutions

  1. Send the URL with an explicit absolute https scheme, e.g. `https://hooks.example.com/webhook`.
  2. Validate client-side before submission: `new URL(value)` in a try/catch and require protocol === 'https:'.
  3. Trim and encode the input string before constructing the URL.
  4. Return a 400 to the caller with a clear message rather than letting the raw error bubble.

Example fix

// before
registerWebhook('hooks.example.com/x')
// after
registerWebhook('https://hooks.example.com/x')
Defensive patterns

Strategy: validation

Validate before calling

function isValidWebhookUrl(rawUrl: string): boolean {
  try {
    const u = new URL(rawUrl);
    return u.protocol === 'https:';
  } catch {
    return false;
  }
}

if (!isValidWebhookUrl(input)) return res.status(400).json({ error: 'A valid https:// webhook URL is required.' });

Type guard

function isWebhookUrl(value: unknown): value is string {
  if (typeof value !== 'string') return false;
  try { return new URL(value).protocol === 'https:'; } catch { return false; }
}

Try / catch

try {
  await assertNotificationWebhookRegistrationUrlSafe(rawUrl);
} catch (err) {
  if (err.message === 'Webhook URL is not a valid URL') {
    return res.status(400).json({ error: 'Please enter a valid absolute https:// URL.' });
  }
  throw err;
}

Prevention

When it happens

Trigger: A POST to the notification-webhook registration endpoint with a body whose url field is a bare hostname (e.g. "example.com/hook"), a string with spaces, a missing protocol, or any value the URL constructor rejects.

Common situations: Frontend form submitting the webhook without forcing an https:// prefix; copy-paste that drops the scheme; a misconfigured integrations panel; a test fixture passing a relative path.

Related errors


AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12). Data as JSON: /api/errors/d8454a4b017ba5aa. Report an issue: GitHub.