koala73/worldmonitor · warning · ValidationError

Unknown chokepoint ID: ${invalidCp}

Error message

Unknown chokepoint ID: ${invalidCp}

What it means

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.

Source

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

  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) {
    throw new ValidationError([
      { field: 'alertThreshold', description: 'alertThreshold must be between 0 and 100' },
    ]);
  }

  const ownerTag = await callerFingerprint(ctx.request, apiKeyResult.credential);

View on GitHub (pinned to eeab0a219f)

Solutions

  1. Take the offending id echoed in the error message and replace it with a current CHOKEPOINT_REGISTRY id from the registry/docs
  2. Source chokepointIds dynamically (fetch the current chokepoint list) instead of hardcoding
  3. Match exact casing and trim whitespace before sending

Example fix

// before
await registerWebhook({ callbackUrl, chokepointIds: ['panama', 'suez'] });
// after
await registerWebhook({ callbackUrl, chokepointIds: ['panama-canal', 'suez-canal'] }); // exact registry ids
Defensive patterns

Strategy: validation

Validate before calling

const VALID = new Set(['panama-canal', 'suez-canal' /* keep in sync with CHOKEPOINT_REGISTRY */]);
const bad = ids.filter(id => !VALID.has(id)); if (bad.length) throw new RangeError(`Unknown chokepoint ids: ${bad.join(', ')}`);

Try / catch

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; }

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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