{"record":{"id":"2a5ea18e2392be71","repo":"koala73/worldmonitor","slug":"unknown-chokepoint-id-invalidcp","errorCode":null,"errorMessage":"Unknown chokepoint ID: ${invalidCp}","messagePattern":"Unknown chokepoint ID: (.+?)","errorType":"validation","errorClass":"ValidationError","httpStatus":400,"severity":"warning","filePath":"server/worldmonitor/shipping/v2/register-webhook.ts","lineNumber":65,"sourceCode":"\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) {\n    throw new ValidationError([\n      { field: 'alertThreshold', description: 'alertThreshold must be between 0 and 100' },\n    ]);\n  }\n\n  const ownerTag = await callerFingerprint(ctx.request, apiKeyResult.credential);","sourceCodeStart":47,"sourceCodeEnd":83,"githubUrl":"https://github.com/koala73/worldmonitor/blob/eeab0a219fce0f02a00603b532dbae9041b934ac/server/worldmonitor/shipping/v2/register-webhook.ts#L47-L83","documentation":"registerWebhook validates every entry of chokepointIds against VALID_CHOKEPOINT_IDS — the Set built from CHOKEPOINT_REGISTRY (server/_shared/chokepoint-registry via webhook-shared.ts:5) — and rejects with a 400 naming the first offending id. This keeps subscriptions bound to real maritime chokepoints so alert delivery never fans out to unknown ids.","triggerScenarios":"POST RegisterWebhook with a chokepointIds entry that is not a registry id: wrong slug form ('panama' vs the registered id), an id from an older registry version after renames, wrong casing, or trailing whitespace. The find() short-circuits on the first invalid id and echoes it in the message.","commonSituations":"Partner integration built against stale documentation; ids copied from a different API surface; registry renamed ids in a deploy; hand-built request objects in tests with placeholder ids.","solutions":["Take the offending id echoed in the error message and replace it with a current CHOKEPOINT_REGISTRY id from the registry/docs","Source chokepointIds dynamically (fetch the current chokepoint list) instead of hardcoding","Match exact casing and trim whitespace before sending"],"exampleFix":"// before\nawait registerWebhook({ callbackUrl, chokepointIds: ['panama', 'suez'] });\n// after\nawait registerWebhook({ callbackUrl, chokepointIds: ['panama-canal', 'suez-canal'] }); // exact registry ids","handlingStrategy":"validation","validationCode":"const VALID = new Set(['panama-canal', 'suez-canal' /* keep in sync with CHOKEPOINT_REGISTRY */]);\nconst bad = ids.filter(id => !VALID.has(id)); if (bad.length) throw new RangeError(`Unknown chokepoint ids: ${bad.join(', ')}`);","typeGuard":null,"tryCatchPattern":"catch (e) { const m = /Unknown chokepoint ID: (.+)/.exec(e?.details?.[0]?.description ?? ''); if (m) { replace m[1] with a registry id and re-submit } else throw e; }","preventionTips":["Fetch chokepoint ids from the current registry instead of hardcoding","Keep partner docs in sync when registry ids change","Trim and case-match ids exactly before sending"],"tags":["validation","chokepoint","webhooks","http-400","identifier","shipping"],"backgroundTag":"unknown-identifier-validation","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"}