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
- Reproduce with the real resolver to get the specific block reason — the generic default hides it when a non-Error was thrown
- If you control the resolver or test double, throw new Error('...') instead of a bare string so the message propagates
- 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
- Pre-validate the https URL shape client-side before registering
- When stubbing DNS in tests, throw Error objects so messages propagate instead of the generic default
- Read the wrapped description field: it distinguishes URL shape, scheme, metadata, private-address, and DNS causes
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
- callbackUrl is not a valid URL
- callbackUrl is required
- callbackUrl must use https
- callbackUrl hostname is a blocked metadata endpoint
- callbackUrl resolves to a private/reserved address: ${hostna
AI-assisted analysis of koala73/worldmonitor@eeab0a219f (2026-08-21).
Data as JSON: /api/errors/afa6f76206196d06.
Report an issue: GitHub.