koala73/worldmonitor · warning · ValidationError

callbackUrl is not allowed

Error message

callbackUrl is not allowed

What it means

registerWebhook wraps every failure of assertCallbackUrlRegistrationSafe into a 400 ValidationError on callbackUrl, rethrowing error.message and defaulting to 'callbackUrl is not allowed' only when the thrown value is not an Error instance. So this exact default text means a non-Error was thrown inside the safety check; the Error case surfaces the specific reason (not a valid URL, must use https, blocked metadata endpoint, private/reserved address, DNS resolution failed, or no addresses).

Source

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

  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
  // the partner omitted the field, so apply the legacy default of 50. An
  // explicit 0 is preserved (deliver every alert). The 0..100 range is
  // normally enforced by buf.validate at the wire layer, but we re-enforce
  // it here so direct handler calls (internal jobs, test harnesses, future
  // transports that bypass buf.validate) can't store out-of-range values.
  const alertThreshold = req.alertThreshold ?? 50;
  if (alertThreshold < 0 || alertThreshold > 100) {

View on GitHub (pinned to eeab0a219f)

Solutions

  1. Reproduce with the real resolver to get the specific block reason — the generic default hides it when a non-Error was thrown
  2. If you control the resolver or test double, throw new Error('...') instead of a bare string so the message propagates
  3. Fix the callbackUrl per the underlying reason: https scheme, public DNS-resolvable host, no private/metadata addresses

Example fix

// before
throw 'lookup blew up'; // non-Error -> generic 'callbackUrl is not allowed'
// after
throw new Error('lookup blew up'); // real message surfaces in the 400 response
Defensive patterns

Strategy: validation

Validate before calling

// client-side precheck mirroring the server policy
try { const u = new URL(url); if (u.protocol !== 'https:') throw new Error('https required'); } catch { fail early before the RPC }

Try / catch

catch (e) { const desc = e?.details?.find(d => d.field === 'callbackUrl')?.description; if (desc) show desc to the operator — it carries the real SSRF-policy reason; }

Prevention

When it happens

Trigger: Something inside the registration safety path throws a plain string or object instead of an Error — e.g. a resolveHostname test double (Symbol.for('worldmonitor.shippingV2.resolveWebhookHostnameForTest')) throwing a bare string, or a monkeypatched fetch rejecting with a non-Error. Any genuine SSRF-policy rejection normally produces one of the more specific messages instead.

Common situations: Test harnesses stubbing DNS or fetch with throw 'string'; custom resolveHostname implementations; older code paths that rethrow unknown values.

Related errors


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