{"record":{"id":"afa6f76206196d06","repo":"koala73/worldmonitor","slug":"callbackurl-is-not-allowed","errorCode":null,"errorMessage":"callbackUrl is not allowed","messagePattern":"callbackUrl is not allowed","errorType":"validation","errorClass":"ValidationError","httpStatus":400,"severity":"warning","filePath":"server/worldmonitor/shipping/v2/register-webhook.ts","lineNumber":59,"sourceCode":"  const apiKeyResult = (await validateApiKey(ctx.request, { forceKey: true })) as {\n    valid: boolean; required: boolean; error?: string; credential?: string;\n  };\n  if (apiKeyResult.required && !apiKeyResult.valid) {\n    throw new ApiError(401, apiKeyResult.error ?? 'API key required', '');\n  }\n\n  await requirePremiumRpcAccess(ctx.request, ApiError, 'PRO subscription required');\n\n  const callbackUrl = (req.callbackUrl ?? '').trim();\n  if (!callbackUrl) {\n    throw new ValidationError([{ field: 'callbackUrl', description: 'callbackUrl is required' }]);\n  }\n\n  try {\n    await assertCallbackUrlRegistrationSafe(callbackUrl);\n  } catch (error) {\n    const message = error instanceof Error ? error.message : 'callbackUrl is not allowed';\n    throw new ValidationError([{ field: 'callbackUrl', description: message }]);\n  }\n\n  const chokepointIds = Array.isArray(req.chokepointIds) ? req.chokepointIds : [];\n  const invalidCp = chokepointIds.find(id => !VALID_CHOKEPOINT_IDS.has(id));\n  if (invalidCp) {\n    throw new ValidationError([\n      { field: 'chokepointIds', description: `Unknown chokepoint ID: ${invalidCp}` },\n    ]);\n  }\n\n  // alert_threshold is `optional int32` (#3242 followup #4) — undefined means\n  // the partner omitted the field, so apply the legacy default of 50. An\n  // explicit 0 is preserved (deliver every alert). The 0..100 range is\n  // normally enforced by buf.validate at the wire layer, but we re-enforce\n  // it here so direct handler calls (internal jobs, test harnesses, future\n  // transports that bypass buf.validate) can't store out-of-range values.\n  const alertThreshold = req.alertThreshold ?? 50;\n  if (alertThreshold < 0 || alertThreshold > 100) {","sourceCodeStart":41,"sourceCodeEnd":77,"githubUrl":"https://github.com/koala73/worldmonitor/blob/eeab0a219fce0f02a00603b532dbae9041b934ac/server/worldmonitor/shipping/v2/register-webhook.ts#L41-L77","documentation":"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).","triggerScenarios":"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.","commonSituations":"Test harnesses stubbing DNS or fetch with throw 'string'; custom resolveHostname implementations; older code paths that rethrow unknown values.","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"],"exampleFix":"// before\nthrow 'lookup blew up'; // non-Error -> generic 'callbackUrl is not allowed'\n// after\nthrow new Error('lookup blew up'); // real message surfaces in the 400 response","handlingStrategy":"validation","validationCode":"// client-side precheck mirroring the server policy\ntry { const u = new URL(url); if (u.protocol !== 'https:') throw new Error('https required'); } catch { fail early before the RPC }","typeGuard":null,"tryCatchPattern":"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; }","preventionTips":["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"],"tags":["validation","ssrf","webhooks","http-400","error-wrapping","callback-url"],"backgroundTag":"ssrf-protection","analyzedSha":"eeab0a219fce0f02a00603b532dbae9041b934ac","analyzedAt":"2026-08-21T16:51:25.751Z","contentChangedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-09-08T15:18:49.778Z"}