fix(cli): guard numeric .code to HTTP range; bound cause-chain depth

Address @claude review of #1583:

- statusOf() now only treats a numeric `.code` as an HTTP status when it
  is in the 100-599 range, so an MCP SDK McpError JSON-RPC code (e.g.
  -32601 MethodNotFound) is no longer leaked into the envelope `status`
  or misclassified as AUTH_REQUIRED (it falls back to USAGE).
- causeOf() now carries a depth cap (MAX_CAUSE_DEPTH) so a cyclic/
  self-referential `error.cause` chain terminates instead of recursing
  infinitely.
- Add tests for both: a -32601 McpError-shaped error (status undefined,
  exit USAGE) and a self-referential cause chain (finite string).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
This commit is contained in:
cliffhall 2026-07-01 19:10:25 -04:00
commit fa5514a380

View file

@ -49,13 +49,18 @@ export class CliExitCodeError extends Error {
}
}
/** Depth cap for {@link causeOf} — bounds a pathological/self-referential
* `error.cause` chain so a cycle can never recurse infinitely. */
const MAX_CAUSE_DEPTH = 16;
/** Read an `Error.cause` chain into a single readable string, if present. */
function causeOf(error: unknown): string | undefined {
function causeOf(error: unknown, depth = 0): string | undefined {
if (depth >= MAX_CAUSE_DEPTH) return undefined;
if (!(error instanceof Error)) return undefined;
const cause = (error as { cause?: unknown }).cause;
if (cause === undefined || cause === null) return undefined;
if (cause instanceof Error) {
const nested = causeOf(cause);
const nested = causeOf(cause, depth + 1);
return nested ? `${cause.message}: ${nested}` : cause.message;
}
return String(cause);
@ -71,9 +76,14 @@ function statusOf(error: unknown): number | undefined {
};
if (typeof e.status === "number") return e.status;
if (typeof e.response?.status === "number") return e.response.status;
// SDK SSEError exposes `.code` as the HTTP status; numeric only (avoid
// mistaking node error codes like "ENOTFOUND" for a status).
if (typeof e.code === "number") return e.code;
// SDK SseError / StreamableHTTPError expose `.code` as the HTTP status. Guard
// to the HTTP range (100-599) so a JSON-RPC McpError code (e.g. -32601
// MethodNotFound) is not mistaken for an HTTP status and leaked into the
// envelope or misclassified as AUTH_REQUIRED. String node codes like
// "ENOTFOUND" are already excluded by the numeric check.
if (typeof e.code === "number" && e.code >= 100 && e.code <= 599) {
return e.code;
}
return undefined;
}