vercel/ai · warning · RelayRequestError

Invalid host tool relay request.

Error message

Invalid host tool relay request.

What it means

A valid /invoke body must be an object with string requestId, string toolName, a record-valued input, and a safe-integer catalogRevision. If any of these is missing or of the wrong type, handleInvocation throws this 400 RelayRequestError. This is the structural schema check, applied after the active-turn check and before revision/tool checks.

Source

Thrown at packages/harness-acp/src/v1/bridge/host-tool-relay.ts:284

}): Promise<{
  output: unknown;
  isError?: boolean;
  correlationToken: string;
}> {
  if (turn == null) {
    throw new RelayRequestError({
      status: 409,
      message: 'No ACP prompt turn is active.',
    });
  }
  if (
    !isRecord(body) ||
    typeof body.requestId !== 'string' ||
    typeof body.toolName !== 'string' ||
    !isRecord(body.input) ||
    !Number.isSafeInteger(body.catalogRevision)
  ) {
    throw new RelayRequestError({
      status: 400,
      message: 'Invalid host tool relay request.',
    });
  }
  if (body.catalogRevision !== state.revision) {
    throw new RelayRequestError({
      status: 409,
      message:
        `Host tool ${body.toolName} was invoked from stale catalog revision ` +
        `${body.catalogRevision}; the active revision is ${state.revision}.`,
    });
  }
  const tool = state.tools.find(item => item.name === body.toolName);
  if (tool == null) {
    throw new RelayRequestError({
      status: 404,
      message:
        `Host tool ${body.toolName} is not active in catalog revision ` +

View on GitHub (pinned to 69428b1f8b)

Solutions

  1. Send exactly { requestId: string, toolName: string, input: object, catalogRevision: safe integer }.
  2. Validate the payload client-side before POSTing (types + Number.isSafeInteger on catalogRevision).
  3. Ensure input is a JSON object (not an array or scalar) — serialize tool arguments as a record.
  4. If using a wrapper library, update it to match the current relay wire schema.

Example fix

// before
body: JSON.stringify({ id: 123, tool: name, args: input })
// after
body: JSON.stringify({
  requestId: String(id),
  toolName: name,
  input,
  catalogRevision: currentRevision,
});
Defensive patterns

Strategy: validation

Validate before calling

function isValidInvokeBody(body: unknown): body is {
  requestId: string; toolName: string;
  input: Record<string, unknown>; catalogRevision: number;
} {
  return body != null && typeof body === 'object' && !Array.isArray(body) &&
    typeof (body as any).requestId === 'string' &&
    typeof (body as any).toolName === 'string' &&
    (body as any).input != null && typeof (body as any).input === 'object' && !Array.isArray((body as any).input) &&
    Number.isSafeInteger((body as any).catalogRevision);
}

Type guard

function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
  return value != null && typeof value === 'object' && !Array.isArray(value);
}

Prevention

When it happens

Trigger: POSTing /invoke with a missing input object, numeric or non-string requestId/toolName, a missing or non-integer catalogRevision, malformed JSON that parses to a non-object, or fields renamed in a custom client.

Common situations: Hand-written MCP-to-relay adapters with the wrong payload shape; JSON.stringify dropping undefined fields; catalogRevision sent as a string; frameworks auto-serializing inputs that contain arrays at the top level (rejected by isRecord).

Related errors


AI-assisted analysis of vercel/ai@69428b1f8b (2026-08-30). Data as JSON: /api/errors/7fb25220db32bc8e. Report an issue: GitHub.