koala73/worldmonitor · warning · ValidationError

callbackUrl is required

Error message

callbackUrl is required

What it means

registerWebhook throws a 400 ValidationError on callbackUrl when (req.callbackUrl ?? '').trim() is empty — the field is missing, an empty string, or whitespace only. It fires after the API-key and PRO gates and before assertCallbackUrlRegistrationSafe, because there is nothing else to validate about an absent URL.

Source

Thrown at server/worldmonitor/shipping/v2/register-webhook.ts:52

  // API key. Without forceKey, a Clerk-authenticated pro caller reaches this
  // handler with no API key, callerFingerprint() falls back to 'anon', and
  // every such caller collapses into a shared 'anon' owner bucket — letting
  // one Clerk-session holder enumerate/overwrite other tenants' webhooks.
  // Matches the legacy `api/v2/shipping/webhooks/[subscriberId]{,/[action]}.ts`
  // gate and the documented "X-WorldMonitor-Key required" contract in
  // docs/api-shipping-v2.mdx.
  const apiKeyResult = (await validateApiKey(ctx.request, { forceKey: true })) as {
    valid: boolean; required: boolean; error?: string; credential?: string;
  };
  if (apiKeyResult.required && !apiKeyResult.valid) {
    throw new ApiError(401, apiKeyResult.error ?? 'API key required', '');
  }

  await requirePremiumRpcAccess(ctx.request, ApiError, 'PRO subscription required');

  const callbackUrl = (req.callbackUrl ?? '').trim();
  if (!callbackUrl) {
    throw new ValidationError([{ field: 'callbackUrl', description: 'callbackUrl is required' }]);
  }

  try {
    await assertCallbackUrlRegistrationSafe(callbackUrl);
  } catch (error) {
    const message = error instanceof Error ? error.message : 'callbackUrl is not allowed';
    throw new ValidationError([{ field: 'callbackUrl', description: message }]);
  }

  const chokepointIds = Array.isArray(req.chokepointIds) ? req.chokepointIds : [];
  const invalidCp = chokepointIds.find(id => !VALID_CHOKEPOINT_IDS.has(id));
  if (invalidCp) {
    throw new ValidationError([
      { field: 'chokepointIds', description: `Unknown chokepoint ID: ${invalidCp}` },
    ]);
  }

  // alert_threshold is `optional int32` (#3242 followup #4) — undefined means

View on GitHub (pinned to eeab0a219f)

Solutions

  1. Set callbackUrl to a fully-formed https URL before calling RegisterWebhook
  2. Add client-side required-field validation so the request is never sent with an empty value
  3. Check exact field name and casing (callbackUrl) against the proto/request type

Example fix

// before
await registerWebhook({ callbackUrl: form.url ?? '', chokepointIds }); // '' -> 400
// after
const url = form.url?.trim();
if (!url) throw new RangeError('callbackUrl is required');
await registerWebhook({ callbackUrl: url, chokepointIds });
Defensive patterns

Strategy: validation

Validate before calling

const url = req.callbackUrl?.trim(); if (!url) throw new RangeError('callbackUrl is required');

Type guard

const isNonEmptyCallbackUrl = (v: unknown): v is string => typeof v === 'string' && v.trim().length > 0;

Try / catch

catch (e) { if (e?.details?.some?.(d => d.field === 'callbackUrl')) { fix the request field and re-submit } else throw e; }

Prevention

When it happens

Trigger: POST RegisterWebhook with callbackUrl omitted (undefined), '' or ' '; a JSON body with a misnamed field (callbackURL, callback_url) so the expected property is undefined; form submitted with an empty URL input.

Common situations: UI form validation gap letting an empty field through; env var interpolation producing '' in CI; field renamed during a client refactor; optional chaining defaulting to empty string before the call.

Understand the failure class

Background: "missing required argument" and "the following required arguments were not provided": what required-argument errors mean and how to fix them — this error's family across 20 libraries.

Related errors


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