koala73/worldmonitor · warning · ValidationError

Unknown chokepoint ID

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.

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@7d06c8633d (2026-08-21). Data as JSON: /api/errors/2a5ea18e2392be71. Report an issue: GitHub.

Appendix: source

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

  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 7d06c8633d)