{"record":{"id":"6c69bb4982cbf244","repo":"vercel/ai","slug":"invalid-host-tool-catalog-poll-request","errorCode":null,"errorMessage":"Invalid host tool catalog poll request.","messagePattern":"Invalid host tool catalog poll request\\.","errorType":"http","errorClass":"RelayRequestError","httpStatus":400,"severity":"warning","filePath":"packages/harness-acp/src/v1/bridge/host-tool-relay.ts","lineNumber":211,"sourceCode":"  throw new RelayRequestError({\n    status: 404,\n    message: 'Unknown host tool relay endpoint.',\n  });\n}\n\nasync function handleCatalogNext({\n  body,\n  state,\n}: {\n  body: unknown;\n  state: CatalogState;\n}): Promise<unknown> {\n  if (\n    !isRecord(body) ||\n    !Number.isSafeInteger(body.afterRevision) ||\n    (body.afterRevision as number) < 0\n  ) {\n    throw new RelayRequestError({\n      status: 400,\n      message: 'Invalid host tool catalog poll request.',\n    });\n  }\n  const afterRevision = body.afterRevision as number;\n  if (!state.closed && afterRevision >= state.revision) {\n    await waitForCatalogChange({ state });\n  }\n  if (state.closed) {\n    return { closed: true, revision: state.revision };\n  }\n  return afterRevision < state.revision\n    ? { revision: state.revision, tools: state.tools }\n    : { revision: state.revision };\n}\n\nfunction handleCatalogSeen({\n  body,","sourceCodeStart":193,"sourceCodeEnd":229,"githubUrl":"https://github.com/vercel/ai/blob/69428b1f8b037e4d118fb4853428d5c4e620493c/packages/harness-acp/src/v1/bridge/host-tool-relay.ts#L193-L229","documentation":"The /catalog/next long-poll endpoint requires a JSON body that is an object with an afterRevision field that is a safe non-negative integer. Anything else — missing field, non-integer, negative, or a non-object body — yields this 400 RelayRequestError before the poll is evaluated.","triggerScenarios":"Polling /catalog/next with an empty body, with { revision: 0 } instead of { afterRevision: 0 }, with afterRevision as a string ('0'), a float, null, or a negative number, or sending non-JSON content.","commonSituations":"Hand-rolled polling clients using the wrong field name; JSON that lost integer precision (afterRevision sent as 1.0 string); test scripts hitting the endpoint directly without the expected payload shape; serializing undefined so the field is dropped.","solutions":["Send { afterRevision: <non-negative safe integer> } — use 0 for the initial poll.","Ensure the body is JSON with content-type application/json and no extra top-level type mismatch (must be an object).","Validate the value client-side with Number.isSafeInteger and >= 0 before sending.","If revisions grow beyond safe integers in your client, stop and re-sync rather than casting."],"exampleFix":"// before\nbody: JSON.stringify({ revision: lastRev })\n// after\nbody: JSON.stringify({ afterRevision: Number.isSafeInteger(lastRev) && lastRev >= 0 ? lastRev : 0 })","handlingStrategy":"validation","validationCode":"function catalogPollBody(afterRevision: unknown) {\n  if (!Number.isSafeInteger(afterRevision) || (afterRevision as number) < 0) {\n    throw new Error('afterRevision must be a non-negative safe integer');\n  }\n  return JSON.stringify({ afterRevision });\n}","typeGuard":"function isValidPollBody(body: unknown): body is { afterRevision: number } {\n  return body != null && typeof body === 'object' && !Array.isArray(body) &&\n    Number.isSafeInteger((body as any).afterRevision) &&\n    (body as any).afterRevision >= 0;\n}","tryCatchPattern":null,"preventionTips":["Initial poll should use { afterRevision: 0 }.","Never send the revision as a string; JSON numbers only.","Use the field name afterRevision (not revision) for polls.","Validate with Number.isSafeInteger before serializing."],"tags":["validation","relay","catalog","harness-acp"],"backgroundTag":"request-body-validation-failed","analyzedSha":"69428b1f8b037e4d118fb4853428d5c4e620493c","analyzedAt":"2026-08-30T12:32:21.016Z","schemaVersion":2},"datasetVersion":"2026-08-30T13:17:10.514Z"}