Skip to content

fix(claude): close refused picker uploads and keep early replies intact - #6725

Merged
lidge-jun merged 4 commits into
devfrom
codex/carry-6659-picker-upload-cleanup
Oct 8, 2026
Merged

lidge-jun merged 4 commits into
devfrom
codex/carry-6659-picker-upload-cleanup

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Oct 8, 2026 •

Copy link
Copy Markdown
Owner

Summary

Carries #6659 by @luvs01 without its new upload limits. The maintainer review on #6659 held the 128-upload cap, the 30-second idle deadline and the five-minute absolute deadline because they can cut legitimate slow uploads. For example, a 30 MB attachment on a link below about 0.8 Mbps runs past five minutes, and upstream backpressure can pause input data events long enough to trip the idle timer. The cleanup from that PR lands here:

  • A refused (400/503) request that is still uploading has its input closed after its empty reply finishes. Before this change the refused HTTP/2 stream or HTTP/1.1 socket stayed open waiting for the rest of the body.
  • When upstream answers before the client finishes uploading, input relay stops only after the downstream reply's writable finishes or the response closes, or when upstream closes without having responded. Upstream request closure alone does not stop it, so a large reply still draining under backpressure arrives intact.
  • Closing after a complete reply is graceful. HTTP/1.1 stops relaying, discards remaining input, sends Connection: close and ends the socket with a FIN. An immediate destroy() truncated the last ~16 KiB of a large early reply in 3 of 50 runs. HTTP/2 sends RST_STREAM NO_ERROR after a complete response (RFC 9113 §8.1) and keeps CANCEL when no complete response was sent.
  • Upload framing (HTTP/2 headers without END_STREAM, or HTTP/1.1 chunked/positive Content-Length) decides whether cleanup applies. Bodyless requests and uploads that finish before the response are unaffected; cleanup listeners detach on body completion, so long-lived responses and SSE continue.

No timers or caps are added. The 256-request aggregate ceiling and its release paths are unchanged. structure/clients/claude-desktop.md documents the carried behavior; there is no user-visible limit to document in docs-site.

Supersedes #6659.

Co-authored-by: Epinephrine luvs01@hanmail.net

Verification

  • tests/claude-integration/claude-picker-upload.test.ts run 40 times: 40/40 files, 760 tests passed. Covers headers-only 400/503 refusals on both protocols, byte-exact 8 MiB early replies under H1/H2 backpressure (synchronized on observed buffering, no fixed sleeps), completed uploads with SSE, cancellation, upstream failure, shutdown and healthy sibling streams.
  • HTTP/1.1 attribution: with destroy() on finish the early-reply case passed 47/50 and 45/50; with the graceful end 50/50. HTTP/2 with NO_ERROR: 50/50.
  • Nearby picker suites: 194 pass. bun run test:changed: 7,310 pass, 31 skip, 0 fail. Test-layout and file-size ratchet: 27 pass. bun run typecheck, bun run structure:check, bun run privacy:scan exit 0.
  • Negative control: with picker-listener.ts reverted to dev, 12 of the upload regressions fail.
  • Full local suite not run; hosted CI on this head covers the remainder. Live Desktop validation not performed.

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.

Summary by CodeRabbit

  • Bug Fixes
    • Picker uploads are now stopped cleanly when a request is rejected or the relay ends early, helping prevent lingering connections across HTTP/1.1 and HTTP/2.
    • Uploads continue to be handled correctly when the upstream service responds before the upload finishes, and completed uploads remain unaffected by upstream socket failures.
    • Responses to incomplete uploads now close HTTP/1.1 connections when needed, while HTTP/2 streams are cleaned up independently.
  • Documentation
    • Updated guidance to describe picker upload handling, cancellation behavior, and related test coverage.

Carry #6659 (beea837) without its new
unfinished-upload cap or idle/absolute deadlines, which can reject legitimate
slow uploads. Preserve the existing 256-request aggregate ceiling and its
activeUpstreams release paths.

Close refused input after the empty downstream reply finishes. Preserve early
reply bytes under backpressure by cancelling unfinished input only after the
downstream writable finishes or closes, or upstream closes without responding.
Detach cleanup listeners when the body completes or the request closes.

Retain protocol, failure, cancellation, SSE and shutdown regressions, with
keep-alive HTTP/1.1 clients observing socket closure. Register the focused
suite in both layout inventories and describe only the carried behavior.

Co-authored-by: Epinephrine <luvs01@hanmail.net>
@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner October 8, 2026 00:54
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-08T00:58:07.562066Z 28aa101 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@coderabbitai

coderabbitai Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

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: d61c11db-d551-4f4a-8359-12db2d51125d
📥 Commits

Reviewing files that changed from the base of the PR and between 1a70e75 and 14aa401.

📒 Files selected for processing (1)
  • scripts/test-layout/layout.json

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


📝 Walkthrough

Walkthrough

The Claude picker relay identifies unfinished request bodies and manages their cleanup during refusals, upstream failures, and early responses over HTTP/1.1 and HTTP/2. Integration tests and documentation cover upload handling and response delivery.

Changes

Claude picker upload lifecycle

Layer / File(s) Summary
Framing-aware refusal handling
src/claude/intercept/picker-listener.ts, tests/claude-integration/claude-picker-upload.test.ts
The relay detects unfinished bodies from HTTP/2 framing or HTTP/1.1 transfer encoding and content length. It closes unfinished input after a refusal response finishes. HTTP/1.1 refusal responses include Connection: close. Tests cover 400 and 503 refusals over both protocols.
Relay upload lifecycle
src/claude/intercept/picker-listener.ts, tests/claude-integration/claude-picker-upload.test.ts
The relay tracks upstream responses and removes cleanup listeners when input completes. It closes unfinished input when the upstream closes before responding or the downstream response finishes or closes. HTTP/1.1 502 and relayed responses include Connection: close when the upload remains incomplete. Tests cover early responses, upstream failures, backpressure, admission limits, and shutdown.
Protocol integration coverage
structure/clients/claude-desktop.md, scripts/test-layout/layout.json, tests/fixtures/test-layout-expected.json, tests/claude-integration/claude-picker-upload.test.ts
The documentation describes upload framing and cleanup across both protocols. The test-layout mappings include the new integration test. The tests use shared protocol fixtures and cover upload cleanup and response handling.

Priority: ⬇️ Low

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

Change: Bug fix

Merge Risk: ⚪ Minimal · up to 14aa4

The reviewed changes preserve early replies while cleaning up unfinished picker uploads, and the new test is mapped to its existing integration directory. No material merge-blocking issue remains.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 8 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main changes: closing refused Claude picker uploads and preserving early upstream replies. It is specific, relevant, and suitable for the changeset.
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.
Full details: Docstring Coverage

Explanation

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

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

✅ Deterministic PR hygiene checks passed.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 28aa101f49

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/claude/intercept/picker-listener.ts Outdated
req.once("end", finishUpload);
req.once("close", finishUpload);
upReq.once("close", onUpstreamClose);
responseWritable.once("finish", stopUnfinishedUpload);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Advertise closure on early HTTP/1.1 responses

When an HTTP/1.1 upstream returns before the client finishes its framed upload, this finish handler now destroys the entire client socket, but the relayed response—unlike refuse()—does not include Connection: close. A keep-alive client can therefore treat a fully delimited early response (including the generated 502 path) as reusable and dispatch queued work onto a socket the proxy immediately destroys. Add the close header before sending any response that will trigger this HTTP/1.1 cleanup, or drain the request instead of destroying the connection.

Useful? React with 👍 / 👎.

@github-actions github-actions Bot added the bug Something isn't working label Oct 8, 2026
lidge-jun and others added 3 commits October 8, 2026 10:10
Unpipe unfinished uploads after the downstream writable finishes, discard
remaining HTTP/1.1 input and end the socket gracefully. Immediate request
destruction can truncate the last TLS response records despite finish.
Advertise Connection: close for refusals, early replies and generated 502s.

Use HTTP/2 NO_ERROR after complete replies and retain CANCEL for incomplete
responses. Keep upload cleanup free of new deadlines and byte caps.

Synchronize the large-reply regression on observed backpressure and readable
buffer state. Consume the fixture's known upload prefix before responding to
avoid an independent upstream reset, while leaving client input unfinished.
Cover buffered refusals and byte-exact replies; update the owning contract.

Verification: upload suite 40/40 repetitions (760 passing cases); nearby
picker 194/194; layout and ratchet 27/27; test:changed 7310 pass, 31 skip,
0 fail; typecheck, structure and privacy pass. Destroy-only negative control
fails 5/50; origin/dev negative control fails 12/19.

Co-authored-by: Epinephrine <luvs01@hanmail.net>
Co-authored-by: Epinephrine <luvs01@hanmail.net>
…tchet

Co-authored-by: Epinephrine <luvs01@hanmail.net>
@lidge-jun
lidge-jun merged commit c0f8165 into dev Oct 8, 2026
41 of 44 checks passed
@lidge-jun
lidge-jun deleted the codex/carry-6659-picker-upload-cleanup branch October 8, 2026 03:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant