fix(client): classify 401 as terminal auth failure, never retryable (RBR-1036) - #9
Conversation
…RBR-1036) Rebase of eefe907 onto latest master to resolve the PR #9 merge conflict (unrelated UI files diverged after the fix branch was cut). Content is byte-identical to the original commit for the 6 files this fix touches; no unrelated UI changes carried forward. - cli/src/client/http.ts: ApiAuthError extends ApiRequestError with a fixed status of 401 and a distinct name/instanceof identity, so callers writing retry/backoff wrappers can special-case it and fail fast on the first occurrence instead of folding it into the timeout/5xx/network-error retry path. The existing single-shot interactive recoverAuth board-login exchange (CLI-only, human in the loop) is unaffected. - packages/mcp-server/src/client.ts: PaperclipApiAuthError extends PaperclipApiError the same way, and format.ts tags its MCP tool error response with errorClass: "auth_failed" so a future incident doesn't require decoding the JWT by hand. - Regression tests (both packages) simulate a 401 response mid a bounded retry loop and assert the client/wrapper stops after one attempt with a distinguishable error, not the generic timeout/5xx classification. - skills/paperclip/SKILL.md: documents the never-retry-a-401 rule next to the existing never-retry-a-409 rule. AC1: 401 classified as a distinct auth-failure error class immediately. AC2: retrying a 401 is a no-op. AC3: distinct error class name + errorClass: "auth_failed" in the MCP error payload surfaces the real cause without JWT decoding. AC4: regression tests for both clients.
eefe907 to
398028a
Compare
…RBR-1036) Rebase of eefe907 onto latest master to resolve the PR #9 merge conflict (unrelated UI files diverged after the fix branch was cut). Content is byte-identical to the original commit for the 6 files this fix touches; no unrelated UI changes carried forward. - cli/src/client/http.ts: ApiAuthError extends ApiRequestError with a fixed status of 401 and a distinct name/instanceof identity, so callers writing retry/backoff wrappers can special-case it and fail fast on the first occurrence instead of folding it into the timeout/5xx/network-error retry path. The existing single-shot interactive recoverAuth board-login exchange (CLI-only, human in the loop) is unaffected. - packages/mcp-server/src/client.ts: PaperclipApiAuthError extends PaperclipApiError the same way, and format.ts tags its MCP tool error response with errorClass: "auth_failed" so a future incident doesn't require decoding the JWT by hand. - Regression tests (both packages) simulate a 401 response mid a bounded retry loop and assert the client/wrapper stops after one attempt with a distinguishable error, not the generic timeout/5xx classification. - skills/paperclip/SKILL.md: documents the never-retry-a-401 rule next to the existing never-retry-a-409 rule. AC1: 401 classified as a distinct auth-failure error class immediately. AC2: retrying a 401 is a no-op. AC3: distinct error class name + errorClass: "auth_failed" in the MCP error payload surfaces the real cause without JWT decoding. AC4: regression tests for both clients.
398028a to
73da26c
Compare
…RBR-1036)
Both first-party API clients now throw a distinct, unmistakable error
class for a 401 response instead of the generic request-failure error:
- cli/src/client/http.ts: ApiAuthError extends ApiRequestError with a
fixed status of 401 and a distinct name/instanceof identity, so
callers writing retry/backoff wrappers can special-case it and fail
fast on the first occurrence instead of folding it into the
timeout/5xx/network-error retry path. The existing single-shot
interactive `recoverAuth` board-login exchange (CLI-only, human in
the loop) is unaffected — it still gets exactly one bounded recovery
attempt before giving up.
- packages/mcp-server/src/client.ts: PaperclipApiAuthError extends
PaperclipApiError the same way, and format.ts tags its MCP tool
error response with `errorClass: "auth_failed"` so a future incident
doesn't require decoding the JWT by hand to find the real cause.
Regression tests (both packages) simulate a 401 response mid a bounded
retry loop and assert the client/wrapper stops after one attempt with
a distinguishable error, not the generic timeout/5xx classification.
Also documents the "never retry a 401" rule in skills/paperclip/SKILL.md
next to the existing "never retry a 409" rule, so agents writing their
own retry wrappers around Paperclip API calls know to special-case it.
AC1: 401 classified as a distinct auth-failure error class immediately.
AC2: retrying a 401 is a no-op — the client throws before any retry
loop sees a generic/timeout-shaped error.
AC3: the distinct error class name and `errorClass: "auth_failed"` in
the MCP error payload surface the real cause without JWT decoding.
AC4: regression tests for both clients.
73da26c to
80bbbef
Compare
|
Rebased onto latest Re-verified after rebase:
|
Summary
Split off from RBR-1015. The API client's own retry/timeout handling must treat
401as terminal, not retryable.cli/src/client/http.ts: newApiAuthError(extendsApiRequestError, fixedstatus: 401, distinctname/instanceofidentity) thrown immediately on any 401 response, before the generic error path. The existing single-shot interactiverecoverAuthboard-login exchange is unaffected — it still gets exactly one bounded recovery attempt.packages/mcp-server/src/client.ts: matchingPaperclipApiAuthErrorextendsPaperclipApiError.packages/mcp-server/src/format.ts: tags the MCP tool error payload witherrorClass: "auth_failed"so a future incident doesn't require decoding the JWT by hand.skills/paperclip/SKILL.md: documents "never retry a 401" next to the existing "never retry a 409" rule.Acceptance criteria
ApiAuthError/PaperclipApiAuthError), separate from timeout/5xx/network-error paths.instanceof ApiAuthErrorand fail fast.ApiAuthError/PaperclipApiAuthError) anderrorClass: "auth_failed"in the MCP tool error response.cli/src/__tests__/http.test.tsandpackages/mcp-server/src/client.test.tssimulate a 401 mid-retry-loop and assert one-attempt fail-fast with a distinguishable error.Verification
cli/src/__tests__/http.test.ts— 8/8 passing (via./node_modules/.bin/vitest run cli/src/__tests__/http.test.ts).packages/mcp-server/src/client.test.ts(new) — 3/3 passing.packages/mcp-server/src/tools.test.ts— 12/13 passing; the 1 failure (allowDuplicatefield mismatch) is pre-existing on this branch before this change (confirmed viagit stash), unrelated to this fix.tsc --noEmitinpackages/mcp-server— clean.tsc --noEmitincli— pre-existing unrelated noise only (@paperclipai/plugin-sdkmodule resolution inserver/src/services/*, not built in this sandbox); no errors inclient/http.tsor its test.clivitest project run — 9 pre-existing failures (embedded-postgres timeouts, a board-profile-persona test needing different env config), confirmed identical ongit stash(unmodified branch); none touchclient/http.ts.Coordination
credential_expiredterminal reason server-side).