{"record":{"id":"4353b7f527868e5d","repo":"koala73/worldmonitor","slug":"alertthreshold-must-be-between-0-and-100","errorCode":null,"errorMessage":"alertThreshold must be between 0 and 100","messagePattern":"alertThreshold must be between 0 and 100","errorType":"validation","errorClass":"ValidationError","httpStatus":400,"severity":"warning","filePath":"server/worldmonitor/shipping/v2/register-webhook.ts","lineNumber":78,"sourceCode":"  }\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);\n  const newSubscriberId = generateSubscriberId();\n  const secret = await generateSecret();\n\n  const record: WebhookRecord = {\n    subscriberId: newSubscriberId,\n    ownerTag,\n    callbackUrl,\n    chokepointIds: chokepointIds.length ? chokepointIds : [...VALID_CHOKEPOINT_IDS],\n    alertThreshold,\n    createdAt: new Date().toISOString(),\n    active: true,\n    secret,\n  };","sourceCodeStart":60,"sourceCodeEnd":96,"githubUrl":"https://github.com/koala73/worldmonitor/blob/eeab0a219fce0f02a00603b532dbae9041b934ac/server/worldmonitor/shipping/v2/register-webhook.ts#L60-L96","documentation":"registerWebhook re-enforces the inclusive 0..100 range on alertThreshold even though buf.validate normally enforces it at the wire layer, because direct handler calls (internal jobs, test harnesses, transports that bypass proto validation) would otherwise store out-of-range values. undefined means the partner omitted the field and defaults to 50; an explicit 0 is preserved and means deliver every alert.","triggerScenarios":"Direct (non-HTTP) invocation of the registerWebhook handler with alertThreshold below 0 or above 100; a transport wired without buf.validate; hand-built request objects in tests or internal jobs. Over-the-wire calls are normally stopped earlier by buf.validate with a different error shape.","commonSituations":"Unit tests constructing requests by hand; a UI slider allowing out-of-range values; internal automation jobs calling the handler directly; a new RPC transport added without the validation middleware.","solutions":["Clamp or validate alertThreshold to an integer in 0..100 before building the request","Keep explicit 0 if 'deliver every alert' was intended — only <0 or >100 throws","For direct handler calls, route requests through the same validation path as the wire layer or validate first"],"exampleFix":"// before\nregisterWebhook(ctx, { callbackUrl, chokepointIds, alertThreshold: 150 });\n// after\nregisterWebhook(ctx, {\n  callbackUrl,\n  chokepointIds,\n  alertThreshold: Math.min(100, Math.max(0, Math.round(150))),\n});","handlingStrategy":"validation","validationCode":"if (req.alertThreshold !== undefined && !(Number.isInteger(req.alertThreshold) && req.alertThreshold >= 0 && req.alertThreshold <= 100)) throw new RangeError('alertThreshold must be an integer in 0..100');","typeGuard":"const isAlertThreshold = (v: unknown): v is number => typeof v === 'number' && Number.isInteger(v) && v >= 0 && v <= 100;","tryCatchPattern":"catch (e) { if (e?.details?.[0]?.field === 'alertThreshold') { clamp to [0,100] and re-submit, or omit the field to take the default 50 } else throw e; }","preventionTips":["Remember undefined defaults to 50 and explicit 0 is legal — only out-of-range values throw","Constrain UI inputs (sliders/number fields) to 0..100 integers","Direct handler calls in tests bypass buf.validate: validate requests yourself"],"tags":["validation","range-check","webhooks","alert-threshold","http-400"],"backgroundTag":"value-out-of-range","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"}