Skip to content

fix(claude): record why a native passthrough failed in its request-log row - #6295

Closed
sh940701 wants to merge 3 commits into
lidge-jun:devfrom
sh940701:fix/claude-passthrough-failure-reason
Closed

sh940701 wants to merge 3 commits into
lidge-jun:devfrom
sh940701:fix/claude-passthrough-failure-reason

Conversation

@sh940701

@sh940701 sh940701 commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Summary

A failed native Anthropic passthrough request is logged with a status and no reason. anthropicNativePassthrough (src/server/claude-messages.ts) never sets logCtx.upstreamError. As a result, /api/logs shows no cause for any failure it answers without a stream, and usage.jsonl persists no failure diagnostics for those rows (diagnostics are written for rows at status 400 and above). The affected failures are:

  • upstream error responses (400 / 401 / 403 / 429 / 5xx)
  • a header timeout
  • a fetch failure
  • a body stall or byte-cap overflow
  • a client cancel

The managed native Messages lane and the adapter lanes record a reason for the same upstream.

On my own install, all 378 of the 378 anthropic-native rows at status 400 or above since 2026-09-20 carry no upstreamError: 179 × 400, 173 × 502, 12 × 429, 11 × 499, 2 × 504, 1 × 401. Over the same period, 867 of 871 failure rows from other providers have one. Nothing in the log says why those 179 Claude Code requests were rejected.

Follow-up to #6235, which covered the streamed side of the same function.

Change

  • Upstream error responses: the body is still relayed verbatim (status, body, Content-Type and Retry-After unchanged). The row now gets Provider error <status>: <type>: <message> from formatAnthropicErrorBody in src/adapters/anthropic.ts. That is the same helper the managed lane's nativeMessagesErrorResponse falls back to; it redacts secrets and bounds the detail to 400 characters.
    • A body without an Anthropic error logs Provider error <status>.
    • A body over 64 Ki characters also logs Provider error <status>. That is the order of the managed lane's 64 KiB error read bound (MAX_NATIVE_MESSAGES_ERROR_BYTES); it counts characters because parse cost scales with string length. The passthrough body cap is 64 MB by default, or unbounded with bodyMaxBytes: 0, and parsing plus redacting a large body blocks the event loop: about 1.4 s for a 1 MB message in a local measurement.
    • Only 4xx and 5xx get a reason; a relayed 3xx is not an error row.
  • Local answers: header timeout 504, fetch failure 502, body stall 504, byte-cap overflow 502 and client cancel 499 now go through one local fail(). It redacts the message once and sends it to both the log and the client, as the managed lane's fail() does. It then finalizes with the same status and closeReason as before. Five duplicated return blocks become one helper. The client message only changes if it contains a secret-shaped substring (maintainer review follow-up, f9953e17c).
  • structure/clients/claude-desktop.md: documents the reasons, the 64 KiB bound, and the errorCode effect below.

Side effect, intended and pinned. With a reason present, requestLogErrorCode classifies passthrough rows the way it already classifies the other lanes. An authentication-shaped 403 ("OAuth authentication is currently not supported.") now gets errorCode: "invalid_api_key" instead of the blanket permission_denied. Nothing that drives account or key health reads upstreamError, and failure attribution ignores it.

Privacy. Every stored reason goes through redactSecretString, and one test pins an sk-ant-api03-… key echoed in an upstream message. Bun's fetch errors do not include the URL, query string or proxy userinfo in err.message; checked locally for refused, userinfo-URL, DNS, bad-scheme and credentialed-proxy cases on Bun 1.4.0.

Verification

Head f9953e17c on dev d4480b7a4 (7 behind), Bun 1.4.0 (package.json). f9953e17c adds client-side redaction in fail() and its test, and rewords the bound as characters; the checks below were rerun on it unless marked 9c1277236.

  • RED first. With src/server/claude-messages.ts reverted to dev, 10 of the first 11 new tests fail because upstreamError is undefined: upstream 400 relayed + usage.jsonl, non-JSON 502, redacted credential, unreachable upstream, header timeout, byte-cap overflow, oversized error body, 403 errorCode, body stall, client cancel. The 11th, a 3xx answer is not logged with an error reason, guards the 4xx/5xx condition; it passes on dev and failed against the first version of this change.
  • Determinism.
    • The unreachable-upstream test stubs the refused origin the same way the existing reject-path activation test in claude-messages-endpoint.test.ts does, so it does not rely on a real refusal inside cfg()'s 250 ms header deadline.
    • The stall, cancel and header-timeout tests use raw-socket upstreams. Bun.serve holds a streamed body's headers until the first chunk, which would trip the header deadline instead.
  • Client redaction. The 12th new test, a credential in a local failure message is redacted for the client as well as the log, failed at 9c1277236: the key reached the client.
  • Targeted. bun test tests/lab/core-lab-boundary.test.ts tests/claude-integration/claude-native-passthrough.test.ts tests/claude-integration/claude-messages-endpoint.test.ts tests/claude-integration/messages-native.test.ts tests/claude-integration/messages-native-oauth.test.ts tests/claude-integration/messages-native-opaque-state.test.ts → 163 pass, 0 fail.
  • Directory. bun test tests/claude-integration → 1232 pass, 1 fail.
    • The failure is POST /v1/messages?beta=true streams an Anthropic-shaped turn end to end, a translated route this change does not touch.
    • It fails the same way in a directory run of untouched dev and passes when its file runs alone (see fix(claude): end a passthrough stream on an upstream reset with an Anthropic error event #6235).
    • An earlier directory run with a 5-minute load average of 176 failed 69 tests; the same tree passed after the load dropped, so I discarded that run.
  • Gates. bun x tsc --noEmit → exit 0. bun run structure:check → passed. bun run privacy:scan → passed.
  • Full suite (bun run test on a Mac mini with the Codex app not running, at 9c1277236): exit 1 after 560 s with 7 failures, none in Claude or Messages code.
    • 6 fail the same way in a full run of untouched dev d4480b7a4 on the same machine: deferToNewerServiceRuntime (2), ocx launcher graceful shutdown (3) and turning Codex off. That dev run had 23 failures in all, because the machine was under load.
    • The 7th, ocx export --json … points at the live proxy port, passes when rerun at f9953e17c together with the Claude files: bun test tests/cli/cli-export-command.test.ts tests/config/client-config-export.test.ts tests/claude-integration/claude-native-passthrough.test.ts tests/claude-integration/messages-native.test.ts tests/claude-integration/claude-messages-endpoint.test.ts → 221 pass, 0 fail.

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults.

Review readiness checklist

This PR stays in draft until every box below is ticked. Tick all four boxes once the requirements are met:

  • Required local validation passed; commands, results, and any full-suite exception are documented.

  • I pushed my PR to a recent dev commit (at most 10 behind; a maintainer may still ask for the exact tip before merge).

  • I resolved all correct Codex and CodeRabbit findings.

  • My PR is ready for review.

Summary by CodeRabbit

  • Bug Fixes
    • Request history now provides clearer, redacted details for provider errors, timeouts, stalled or oversized responses, and client cancellations.
    • Provider error responses continue to be relayed unchanged, while their details are included in logs when they are within the supported size limit.
    • Recorded failure reasons are retained in request history and help ensure errors are classified accurately.

…g row

anthropicNativePassthrough never set logCtx.upstreamError. Every failure it
answered without a stream (upstream error responses, a header timeout, a
fetch failure, a body stall or byte-cap overflow, a client cancel) was
logged with only a status, so /api/logs showed no reason and usage.jsonl
persisted no failure diagnostics. The managed native Messages lane and the
adapter lanes record a reason for the same upstream.

- An upstream error response is still relayed verbatim; the row now gets
  "Provider error <status>: <type>: <message>" from formatAnthropicErrorBody
  (redacted, bounded, the same helper the managed lane uses), or
  "Provider error <status>" when the body is not an Anthropic error.
- Local answers go through one fail() that logs the message the client
  receives, redacted, before finalizing.
…ields

Review follow-up:
- Parse an upstream error body for the log reason only within 64 KiB, the
  managed lane's error read bound. The passthrough body cap is 64 MB by
  default (unbounded at 0), and JSON.parse plus redaction over a large
  body would block the event loop; a larger body logs "Provider error
  <status>".
- Only 4xx/5xx answers get a reason; a relayed 3xx is not an error row.
- The unreachable-upstream test stubs the refused origin, as the existing
  reject-path activation test does, instead of relying on a real refusal
  inside cfg()'s 250 ms header deadline.
- New tests pin the body-stall and client-cancel reasons, the oversized
  body, the 3xx case, and the 403 errorCode that a reason now drives
  (invalid_api_key for an authentication-shaped 403, as on other lanes).
- The structure doc and comments say "the same formatAnthropicErrorBody
  the managed lane falls back to" instead of claiming the same format.
@coderabbitai

coderabbitai Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: lidge-jun/opencodex/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: a08ddc55-0bf2-4cdd-813d-cb5d88d8ec11

📥 Commits

Reviewing files that changed from the base of the PR and between 9c12772 and f9953e1.

📒 Files selected for processing (3)
  • src/server/claude-messages.ts
  • structure/clients/claude-desktop.md
  • tests/claude-integration/claude-native-passthrough.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 8 remain after this review.


📝 Walkthrough

Walkthrough

Native Anthropic passthrough records bounded details for upstream error responses and local failures. It continues to relay upstream response bodies unchanged. Integration tests cover request logs, usage history, error-code classification, and failure conditions.

Changes

Native Anthropic passthrough diagnostics

Layer / File(s) Summary
Record passthrough error details
src/server/claude-messages.ts, structure/clients/claude-desktop.md
A shared helper logs redacted local failure messages capped at 500 characters. For upstream error responses with bodies up to 64 Ki characters, logs include a formatted provider error when available. Larger bodies produce a status-only log reason. The response body remains unchanged. The documentation describes the diagnostics and error-code classification.
Verify logged diagnostics
tests/claude-integration/claude-native-passthrough.test.ts
Integration tests cover provider errors, fetch failures, header timeouts, body overflow and stalls, client cancellation, oversized error bodies, 3xx responses, persisted usage history, and error-code classification.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix

Suggested reviewers: lidge-jun

Merge Risk: ⚪ Minimal · up to f9953

This change adds redacted, bounded failure reasons to native passthrough request logs without altering what clients receive. No actionable merge-blocking risk was identified; the author's full-suite run was still pending at the time of review.

Architecture Summary

Architecture risk: 🔵 Low · up to f9953

The change affects 3 systems.

Changed systems: src, structure, tests

Architecture concerns
No architecture-level concerns identified.

Review details

Systems and components

  • observed — src (service) was modified; 1 changed file maps to changed impact.
  • observed — structure (service) was modified; 1 changed file maps to changed impact.
  • observed — tests (service) was modified; 1 changed file maps to changed impact.

Before / after behavior

  • observed — Modified behavior in src/server/claude-messages.ts: Adds the formatAnthropicErrorBody import used to format provider-error details in passthrough logs.
  • observed — Modified behavior in src/server/claude-messages.ts: Adds a shared local-failure helper that redacts the message, records it in upstreamError capped at 500 characters, finalizes with the supplied status and close reason, and returns an Anthropic error response containing the redacted message.
  • observed — Modified behavior in src/server/claude-messages.ts: Header timeout and fetch-error branches now use the shared helper instead of finalizing and constructing responses separately. Their 504 and 502 statuses, close reason, client message, and error type are retained, while the redacted error is added to the request log.
  • observed — Modified behavior in src/server/claude-messages.ts: Client cancellation, body stall, and body overflow now use the shared helper, retaining their statuses and close reasons while logging error details. For non-stream upstream responses with status 400 or higher, the log records a formatted provider error when the body is at most 64 Ki characters, or only the provider status when it is larger; the response body remains relayed verbatim.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 3 functions across 2 files. (1 skipped: 1… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: recording the failure reason for native Claude passthrough requests in the request-log row. It matches the changes in src/server/claude-messa…
Full details: Docstring Coverage

Explanation

Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 3 functions across 2 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sh940701

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Deterministic PR hygiene checks passed.

@github-actions github-actions Bot added the bug Something isn't working label Sep 30, 2026
@coderabbitai

coderabbitai Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@github-actions

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

✅ READY

  • all PR quality gates passed; the review readiness checklist is complete.

Review readiness checklist

  • ✅ Required local validation passed; commands, results, and any full-suite exception are documented.
  • ✅ I pushed my PR to a recent dev commit (at most 10 behind; a maintainer may still ask for the exact tip before merge).
  • ✅ I resolved all correct Codex and CodeRabbit findings.
  • ✅ My PR is ready for review.

✅ 4/4 boxes ticked.

This pull request has been marked Ready for Review.
The review-ready label marks this PR as ready; review automation runs independently.
Maintainers notified: @lidge-jun @Ingwannu

@lidge-jun

Copy link
Copy Markdown
Owner

리뷰 · 우선순위 63 / 80

Claude Code가 Anthropic으로 그대로 나가는 native passthrough에서, 스트림이 아닌 실패가 로그에 이유 없이 남던 구멍을 막습니다. 예전에는 상태 숫자만 있어서 /api/logs와 usage.jsonl에 왜 깨졌는지가 없었습니다. 작성자가 자기 설치에서 본 숫자도 그 구멍입니다. 2026-09-20 이후 anthropic-native 실패 378건에 upstreamError가 하나도 없었습니다. 지금은 업스트림 4xx/5xx 본문을 클라이언트에게 그대로 넘기고, 로그에는 managed 레인이 쓰는 formatAnthropicErrorBody로 짧은 이유를 넣습니다. 본문이 Anthropic 에러가 아니거나 64KiB를 넘으면 Provider error <status>만 남깁니다. 헤더 시간 초과, 연결 실패, 본문 멈춤, 용량 초과, 클라이언트가 끊은 경우는 fail() 하나로 모아서, 클라이언트가 받는 문장을 로그에도 남깁니다. 테스트가 실패 종류를 하나씩 고정하고, 에러 메시지에 섞인 키는 지우고, 큰 본문은 파싱하지 않는지도 확인합니다. base는 dev이고, #6235가 스트림 쪽을 고친 뒤의 후속입니다.

라인 - src/server/claude-messages.ts (PASSTHROUGH_ERROR_DETAIL_MAX_CHARS, text.length): 주석은 managed 레인의 64 KiB 바이트 읽기 한도와 같다고 하는데, 비교는 JS 문자열 길이입니다. 영어 에러는 거의 같고, 한글·이모지가 많으면 바이트와 어긋납니다. JSON.parse를 막는 목적에는 문자 길이가 더 맞을 수 있습니다. 이름과 주석만 맞춰 두면 됩니다.
라인 - fail(): 로그는 지우고 클라이언트에는 예전처럼 원문 메시지를 줍니다. managed 레인의 fail은 둘 다 지웁니다. 본문에 Bun fetch 메시지에 URL이 없다고 적혀 있습니다. 런타임이 바뀌면 fetch 실패 문구가 새어 나올 여지는 남습니다.
라인 - PR이 아직 draft이고 리뷰 준비 체크리스트가 비어 있습니다. 본문에 적은 로컬 테스트는 보이지만, ready 전에 그 칸을 채워야 합니다.

메인테이너의 판단이 필요한 지점

이유를 넣으면 requestLogErrorCode가 다른 레인과 같이 돌아갑니다. 그래서 "OAuth authentication is currently not supported." 같은 403이 permission_denied가 아니라 invalid_api_key가 됩니다. classifyError가 문구 안의 "authentication"을 키 실패로 보기 때문입니다. 작성자는 계정·키 건강 검사가 upstreamError를 안 본다고 했고, 테스트로 이 코드 변화를 고정했습니다. 로그 화면에서 이 코드를 키 문제로 읽을지, 다른 레인과 맞출지가 판단입니다.

너의 추천

구멍은 실측과 맞고, 클라이언트 응답은 그대로 두고 로그에만 이유를 넣는 쪽이 맞습니다. 바이트/문자 주석을 고치고 403 코드 변화를 한 번 확인한 뒤 draft를 풀면 됩니다. types.ts/config.ts 분할이나 미리보기 배포는 이 변경과 무관합니다.

이 댓글은 grok-bot이 작성했습니다

Maintainer review follow-up:
- fail() now sends the redacted message to the client as well as the log,
  as the managed lane's fail() does, so a fetch error that ever carries a
  URL or key cannot reach the client. New test: a key in a stubbed fetch
  error is absent from both the response and the row.
- The error-detail bound is 64 Ki characters, and the comment and doc now
  say so: parse cost scales with characters, and the bound is only of the
  order of the managed lane's 64 KiB byte read limit.
@sh940701

Copy link
Copy Markdown
Contributor Author

Thanks for the review. Both line comments are addressed in f9953e17c.

fail() redaction. The client now gets the redacted message as well as the log, the same as the managed lane's fail(). I agree the current Bun wording is not a guarantee. New test: a credential in a local failure message is redacted for the client as well as the log stubs a fetch error that carries an sk-ant-api03-… key and checks that the key is absent from both the response body and the row. It failed before the change: the key reached the client. The ordinary messages are unchanged, because redaction only rewrites secret-shaped substrings. The existing reject-path test in claude-messages-endpoint.test.ts still passes.

Bytes vs. characters. The bound is characters, since JSON.parse and redaction cost scale with string length. The comment, the structure doc and the test comment now say "64 Ki characters, the order of the managed lane's 64 KiB byte read bound" instead of claiming the same limit.

403 errorCode. I left the classification as the other lanes already do it and pinned it in a test, so the change is visible either way. I have no strong view here. If you would rather keep permission_denied for passthrough 403s, I can special-case it.

At f9953e17c: running the six related files (bun test tests/lab/core-lab-boundary.test.ts tests/claude-integration/claude-native-passthrough.test.ts tests/claude-integration/claude-messages-endpoint.test.ts tests/claude-integration/messages-native.test.ts tests/claude-integration/messages-native-oauth.test.ts tests/claude-integration/messages-native-opaque-state.test.ts) gives 163 pass, 0 fail. tsc, structure:check and privacy:scan pass. The full-suite comparison is still running; I'll add it to the description before ticking the checklist.

@sh940701

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@github-actions
github-actions Bot marked this pull request as ready for review September 30, 2026 04:59
lidge-jun added a commit that referenced this pull request Sep 30, 2026
…g row (#6330)

Carries #6295 with a maintainer privacy rework: the stored reason is limited to the HTTP status plus an allowlisted Anthropic error type, so upstream message text (which can echo account identifiers or request content) never reaches the request log or usage history. Client responses are unchanged.

Co-authored-by: sh940701 <visioner2168@gmail.com>
@lidge-jun

Copy link
Copy Markdown
Owner

Landed on through maintainer carry #6330, with a Co-authored-by trailer for you. One change from this PR: review found that the upstream error message can echo account identifiers or request content, which secret redaction does not remove, so the stored reason is now limited to the HTTP status plus an allowlisted Anthropic error type. Thank you for the fix!

@lidge-jun lidge-jun closed this Sep 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working review-ready

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants