Skip to content

fix(relay): set Request.GetBody so the HTTP/2 transport can transparently retry after an upstream stream reset - #6249

Merged
Calcium-Ion merged 6 commits into
QuantumNous:mainfrom
BlockchainHe:fix/upstream-request-getbody
Aug 6, 2026
Merged

fix(relay): set Request.GetBody so the HTTP/2 transport can transparently retry after an upstream stream reset#6249
Calcium-Ion merged 6 commits into
QuantumNous:mainfrom
BlockchainHe:fix/upstream-request-getbody

Conversation

@BlockchainHe

@BlockchainHe BlockchainHe commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

📝 Description

Note

Maintainer follow-up (2026-08-06): The original BodyStorage.NewReader / Request.GetBody implementation and its two commits remain authored by @BlockchainHe. The branch is now rebased onto the latest main, with separate maintainer commits that:

  • migrate the new Alpha Search path and bind replay metadata for all converted bodies;
  • bind both ContentLength and GetBody on pass-through bodies, the Sora opaque fallback, and the Jimeng direct request path;
  • clear and rebind body metadata at every channel attempt so cross-channel retries cannot retain a callback to closed storage;
  • return upstream 301/302/303/307/308 responses without following them, using a shallow copy of the cached HTTP client so shared client state and HTTP/2 transport retries remain unchanged;
  • add pass-through REFUSED_STREAM, graceful GOAWAY, cross-channel lifecycle, Sora, and redirect regression coverage.

Explicit Content-Length on type-erased pass-through bodies is a wire-visible improvement; those HTTP/1.1 requests previously used chunked transfer encoding. Current maintainer verification passed go test ./relay/... ./common/... -count=1, focused -race, HTTP/2 tests with -count=20, scoped build/vet, and independent relaykit test/build. A root go build ./... in this checkout is blocked only because generated web/dist assets are absent.

The detailed original analysis and proof below are preserved as submitted; the current scope and validation are recorded above.

While relaying to an HTTP/2 upstream, requests started failing with:

http2: Transport: cannot retry err [...] after Request.Body was written; define Request.GetBody to avoid this error

Go's transport can transparently retry these resets (REFUSED_STREAM / graceful GOAWAY), but only when Request.GetBody is set. The relay never sets it — the outbound body is a type-erased reader over BodyStorage — so every such reset surfaces to the caller even though a retry would have succeeded. While fixing this I also noticed DoTaskApiRequest installs a GetBody that re-reads an already-consumed reader: a retry there would silently send an empty body, which is worse than failing.

The fix hands out independent replay readers from BodyStorage (a new NewReader() on both the memory and disk implementations — zero-copy in both modes), carries the replay hook on RelayInfo next to UpstreamRequestBodySize, and injects it only when net/http didn't already derive a correct GetBody. The broken override is removed. Unit tests plus a raw-frame HTTP/2 end-to-end test cover both the transparent-retry path and the empty-body regression.

Full analysis: problem, fix design, and test coverage

Problem

1. Requests on the main relay paths can never be transparently retried by Go's HTTP/2 transport

Per net/http semantics, the HTTP/2 transport can transparently retry a request that fails with a retry-safe error — a stream-level RST_STREAM(REFUSED_STREAM) (RFC 9113 §8.7: the upstream guarantees the stream was not processed; common with proxy/CDN-fronted upstreams under load or during graceful restarts) or a graceful connection-level GOAWAY. However, once the request body has been written, retrying requires Request.GetBody to be non-nil; otherwise the request fails outright:

http2: Transport: cannot retry err [stream error: stream ID 1; REFUSED_STREAM; received from peer] after Request.Body was written; define Request.GetBody to avoid this error

The outbound body built by DoApiRequest is a type-erased io.Reader (common.ReaderOnly(BodyStorage)), and net/http only auto-derives GetBody for *bytes.Reader / *bytes.Buffer / *strings.Reader. So GetBody stays nil on every path going through DoApiRequest — chat / claude / gemini / responses / embedding / image / rerank. This is the other half of the same type-erasure issue that applyUpstreamContentLength already handles for ContentLength.

2. The hand-rolled GetBody in DoTaskApiRequest silently replays an empty body (worse than failing)

req.GetBody = func() (io.ReadCloser, error) {
    return io.NopCloser(requestBody), nil
}

This returns the same, already-consumed reader: if the transport ever retries (or a redirect needs to replay the body), the second attempt sends an empty body. Moreover, most task adaptors pass *bytes.Reader bodies, for which net/http had already derived a correct snapshot-based GetBody — this code overwrote a correct implementation with a broken one.

Fix

BodyStorage retains the full payload for the lifetime of the request (in memory, or in a disk-backed temp file above the configured threshold), so replay support only needed wiring:

  • BodyStorage gains a NewReader() (io.ReadCloser, error) method returning an independent reader per call, as required by the Request.GetBody contract ("a new copy of Body"): the memory implementation returns a fresh bytes.Reader over the same immutable backing array (zero-copy), and the disk implementation opens a fresh file descriptor (zero-copy, kernel page-cache backed). Independent cursors mean replays can never race an in-flight read.
  • NewOutboundJSONBody now also returns getBody func() (io.ReadCloser, error) backed by NewReader(); closing a replayed body never affects the underlying storage (its lifecycle stays owned by the handler's defer closer.Close()), and once the storage is released, GetBody fails with ErrStorageClosed instead of replaying stale data.
  • RelayInfo gains an UpstreamRequestGetBody field, set at the same call sites (8 handlers, 9 call sites) and with the same lifecycle as the existing UpstreamRequestBodySize.
  • A new applyUpstreamGetBody helper (symmetric with applyUpstreamContentLength) wires it into DoApiRequest / DoFormRequest / DoTaskApiRequest, and only injects when req.GetBody == nil — it never overrides a correct auto-derived implementation.
  • The broken override in DoTaskApiRequest is removed: *bytes.Reader / *bytes.Buffer-style bodies fall back to net/http's snapshot-based GetBody, and non-replayable type-erased bodies keep GetBody == nil, so a retry fails loudly instead of silently sending a corrupted (empty) request.

Tests

  • End-to-end HTTP/2 retry test (relay/channel/api_request_getbody_test.go), against a raw-frame HTTP/2 test server that reads the full request body, answers the first attempt with RST_STREAM(REFUSED_STREAM), and serves the retried stream normally with 200:
    • TestUpstreamGetBody_HTTP2RetryAfterUpstreamStreamReset: builds the request the same way DoApiRequest does (type-erased body + both applyUpstream* helpers) and asserts the transport retries transparently and the server receives a byte-identical body on both attempts;
    • TestUpstreamGetBody_HTTP2CannotRetryWithoutGetBody: negative case reproducing the pre-fix cannot retry err ... after Request.Body was written failure.
  • TestNewOutboundJSONBody_GetBodyReadersAreIndependent (+ _DiskStorage): two readers obtained from GetBody are read interleaved and each still yields the complete, correct payload — independent cursors verified for both the memory- and disk-backed storage modes.
  • TestApplyUpstreamGetBody_*: injected GetBody is non-nil and two consecutive calls both return the full body; an existing GetBody is never overridden; without a replay source it stays nil.
  • TestNewOutboundJSONBody_* (relay/common/outbound_body_test.go): after the main body is consumed (or partially read), getBody replays the full body repeatedly; closing a replayed body does not affect the storage lifecycle.
  • TestDoTaskApiRequest_KeepsReplayableGetBody: regression test for problem 2 — after the request completes, req.GetBody still returns the full body repeatedly (it returned an empty body before the fix).
  • go build ./... passes; go vet reports no new findings on the touched packages (identical to the main baseline); go test ./relay/... ./common/... all pass; the two HTTP/2 end-to-end tests run flake-free with -count=20.

🚀 Type of change

  • 🐛 Bug fix - please link the corresponding issue; do not classify design trade-offs, expectation mismatches, or misunderstandings as bugs
  • ✨ New feature - major features should be discussed in an issue first
  • ⚡ Performance / refactor
  • 📝 Documentation

🔗 Related Issue

  • No corresponding issue (I searched existing issues and PRs and found no duplicate report). This is a deterministic defect rather than a design trade-off: the Go transport's own error message says "define Request.GetBody to avoid this error", and this PR ships a reproducible negative test capturing the pre-fix behavior. Happy to open a tracking issue and link it here if the maintainers prefer.

✅ 提交前检查项 / Checklist

  • Human confirmation: I have personally reviewed, organized, and written this description; it is not unprocessed AI output.
  • Not a duplicate: I have searched existing issues and PRs and confirmed this is not a duplicate submission.
  • Bug fix note: if this PR is labeled Bug fix, I have filed or linked a corresponding issue, and I am not classifying design trade-offs, expectation mismatches, or misunderstandings as bugs.
  • Understanding: I understand how these changes work and their potential impact.
  • Focused scope: this PR contains no code changes unrelated to the task at hand.
  • Local verification: I have run and passed the tests (or verified manually) locally, so maintainers can use the results for review.
  • Security & compliance: the code contains no sensitive credentials and follows the project's coding conventions.

📸 Proof of Work

$ go build ./...
(ok, no output)

$ gofmt -l <all files touched by this PR>
(no output — clean)

$ go test ./relay/channel/ -run 'ApplyUpstreamGetBody|DoTaskApiRequest|UpstreamGetBody_HTTP2' -v -count=1
--- PASS: TestDoTaskApiRequest_KeepsReplayableGetBody (0.00s)
--- PASS: TestUpstreamGetBody_HTTP2RetryAfterUpstreamStreamReset (0.00s)
--- PASS: TestUpstreamGetBody_HTTP2CannotRetryWithoutGetBody (0.00s)
--- PASS: TestApplyUpstreamGetBody_SetsReplayableGetBody (0.00s)
--- PASS: TestApplyUpstreamGetBody_NoopWithoutReplaySource (0.00s)
--- PASS: TestApplyUpstreamGetBody_KeepsExistingGetBody (0.00s)
PASS
ok      github.com/QuantumNous/new-api/relay/channel    0.813s

$ go test ./relay/common/ -run 'OutboundJSONBody' -v -count=1
--- PASS: TestNewOutboundJSONBody_GetBodyAfterPartialRead (0.00s)
--- PASS: TestNewOutboundJSONBody_GetBodyReplaysFullBody (0.00s)
PASS
ok      github.com/QuantumNous/new-api/relay/common     1.215s

$ go test ./relay/channel/ -run 'UpstreamGetBody_HTTP2' -count=20   # flake check
ok      github.com/QuantumNous/new-api/relay/channel    0.412s

$ go test ./relay/... ./common/... -count=1
(all 14 packages with tests: ok, no FAIL)

Genuine pre-fix reproduction, captured by running the same e2e scenario without the applyUpstreamGetBody wiring:

Error: Post "http://upstream.test/v1/chat/completions": http2: Transport: cannot retry err
[stream error: stream ID 1; REFUSED_STREAM; received from peer] after Request.Body was written;
define Request.GetBody to avoid this error

Summary by CodeRabbit

  • Bug Fixes

    • Improved upstream request retries by safely replaying request bodies when supported.
    • Prevented retries from sending empty or corrupted payloads after a request body has been consumed.
    • Ensured HTTP/2 stream-reset retries resend complete request bodies.
    • Preserved upstream redirect responses without following redirect targets unexpectedly.
    • Improved request-body handling across API, task, and pass-through requests.
  • Tests

    • Added coverage for replay behavior, partial reads, redirects, and HTTP/2 retry scenarios.

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f13afe51-cd63-40d9-89cd-1d70e0858c7d

📥 Commits

Reviewing files that changed from the base of the PR and between 0ab0202 and dabee60.

📒 Files selected for processing (20)
  • common/body_storage.go
  • relay/alpha_search_handler.go
  • relay/channel/api_request.go
  • relay/channel/api_request_getbody_test.go
  • relay/channel/api_request_redirect_test.go
  • relay/channel/jimeng/adaptor.go
  • relay/channel/task/sora/adaptor.go
  • relay/channel/task/sora/adaptor_test.go
  • relay/chat_completions_via_responses.go
  • relay/claude_handler.go
  • relay/common/outbound_body.go
  • relay/common/outbound_body_test.go
  • relay/common/relay_info.go
  • relay/common/relay_info_test.go
  • relay/compatible_handler.go
  • relay/embedding_handler.go
  • relay/gemini_handler.go
  • relay/image_handler.go
  • relay/rerank_handler.go
  • relay/responses_handler.go
🚧 Files skipped from review as they are similar to previous changes (7)
  • relay/rerank_handler.go
  • relay/compatible_handler.go
  • relay/gemini_handler.go
  • relay/chat_completions_via_responses.go
  • relay/common/outbound_body.go
  • common/body_storage.go
  • relay/embedding_handler.go

Walkthrough

Changes

Upstream body replay

Layer / File(s) Summary
Replayable body contract
relay/common/outbound_body.go, common/body_storage.go, relay/common/relay_info.go, relay/common/outbound_body_test.go
NewOutboundJSONBody returns a callback that creates independent readers. RelayInfo stores the callback and body size. Memory- and disk-backed replay behavior is tested.
Handler replay metadata propagation
relay/*_handler.go, relay/chat_completions_via_responses.go
Pass-through and converted request handlers store UpstreamRequestGetBody with the upstream body size.
Channel request retry integration
relay/channel/api_request.go, relay/channel/jimeng/adaptor.go, relay/channel/task/sora/adaptor.go, relay/channel/api_request_getbody_test.go, relay/common/relay_info_test.go
Channel requests apply body metadata. Task requests no longer replay consumed readers. HTTP/2 tests cover reset, GOAWAY, and missing GetBody cases.
Redirect response handling
relay/channel/api_request.go, relay/channel/api_request_redirect_test.go
Requests use a shallow client copy with redirects disabled. Tests verify that upstream redirect responses and request bodies remain unchanged.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested reviewers: calcium-ion

Poem

A rabbit stored each body with care,
So every retry had bytes to spare.
A reset stream could not undo
The complete payload sent anew.
Redirects stayed where the upstream route drew.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.13% which is insufficient. The required threshold is 80.00%. 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 summarizes the main change: adding Request.GetBody support for HTTP/2 retries after upstream stream resets.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@relay/common/outbound_body.go`:
- Around line 26-43: Update NewOutboundJSONBody’s getBody closure to create and
return a fresh reader backed by an independent copy of the original body data,
rather than rewinding and reusing storage. Preserve the existing error
propagation and io.ReadCloser return contract so retries and redirects each
receive an independently positioned reader.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6be0eeda-9a45-45a6-8380-8b75e651d239

📥 Commits

Reviewing files that changed from the base of the PR and between a63364d and 04488b0.

📒 Files selected for processing (13)
  • relay/channel/api_request.go
  • relay/channel/api_request_getbody_test.go
  • relay/chat_completions_via_responses.go
  • relay/claude_handler.go
  • relay/common/outbound_body.go
  • relay/common/outbound_body_test.go
  • relay/common/relay_info.go
  • relay/compatible_handler.go
  • relay/embedding_handler.go
  • relay/gemini_handler.go
  • relay/image_handler.go
  • relay/rerank_handler.go
  • relay/responses_handler.go

Comment thread relay/common/outbound_body.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
relay/common/outbound_body_test.go (1)

20-26: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use testify/assert for non-fatal value checks.

As per coding guidelines, Go backend tests must use testify/assert for non-fatal value checks, reserving testify/require strictly for setup and fatal assertions. The require.Equal and require.EqualValues calls throughout this test file act as non-fatal checks for the replay payloads and should be updated. Ensure "github.com/stretchr/testify/assert" is imported if not already present.

  • relay/common/outbound_body_test.go#L20-L26: Change require.EqualValues(t, len(payload), size) and require.Equal(t, payload, first) to use assert.
  • relay/common/outbound_body_test.go#L36-L36: Change require.Equal to assert.Equal.
  • relay/common/outbound_body_test.go#L58-L68: Change require.Equal to assert.Equal on lines 58 and 67.
  • relay/common/outbound_body_test.go#L84-L84: Change require.Equal to assert.Equal.
  • relay/common/outbound_body_test.go#L96-L111: Change require.Equal to assert.Equal on lines 96, 101, 106, and 111.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@relay/common/outbound_body_test.go` around lines 20 - 26, In
relay/common/outbound_body_test.go, replace the non-fatal require.EqualValues
and require.Equal checks throughout the outbound body tests with
assert.EqualValues and assert.Equal, including the checks in lines 20-26, 36,
58, 67, 84, 96, 101, 106, and 111; add the testify/assert import if needed,
while retaining require for setup and fatal assertions.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@relay/common/outbound_body_test.go`:
- Around line 20-26: In relay/common/outbound_body_test.go, replace the
non-fatal require.EqualValues and require.Equal checks throughout the outbound
body tests with assert.EqualValues and assert.Equal, including the checks in
lines 20-26, 36, 58, 67, 84, 96, 101, 106, and 111; add the testify/assert
import if needed, while retaining require for setup and fatal assertions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8e0a4352-193b-41c0-b2c7-e1b8eacf3796

📥 Commits

Reviewing files that changed from the base of the PR and between 04488b0 and c5952ee.

📒 Files selected for processing (3)
  • common/body_storage.go
  • relay/common/outbound_body.go
  • relay/common/outbound_body_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • relay/common/outbound_body.go

@BlockchainHe

Copy link
Copy Markdown
Contributor Author

@Calcium-Ion Could you please review this fix?

Recommended for merge: it restores safe HTTP/2 transparent retries by providing independent replayable request bodies through Request.GetBody. This prevents retry-safe REFUSED_STREAM / GOAWAY failures from surfacing to callers, while avoiding the prior risk of replaying an empty body. The shared-cursor concern has been addressed with independent memory- and disk-backed readers, covered by targeted regression and end-to-end retry tests. Thank you!

BlockchainHe and others added 6 commits August 6, 2026 15:35
…ntly retry after an upstream stream reset

The outbound request body is a type-erased io.Reader over BodyStorage, so
net/http cannot derive Request.GetBody (it only does so for *bytes.Reader,
*bytes.Buffer and *strings.Reader). With GetBody nil, the HTTP/2 transport
cannot transparently retry a request once the body has been written and the
upstream resets the stream with a retryable error (REFUSED_STREAM, or a
connection-level GOAWAY); the relay request then fails with:

    http2: Transport: cannot retry err [...] after Request.Body was written;
    define Request.GetBody to avoid this error

This affects every relay path that goes through DoApiRequest (chat, claude,
gemini, responses, embedding, image, rerank).

BodyStorage (memory and disk) already implements io.Seeker, so replay support
only needed wiring:

- NewOutboundJSONBody additionally returns a getBody that rewinds the storage
  and hands out a fresh non-closing reader. The transport only calls GetBody
  after the previous attempt's body has been abandoned, so the rewind cannot
  race an in-flight read.
- RelayInfo carries it in the new UpstreamRequestGetBody field, set alongside
  UpstreamRequestBodySize by the handlers that build storage-backed bodies.
- applyUpstreamGetBody (symmetric with applyUpstreamContentLength) wires it
  into DoApiRequest/DoFormRequest/DoTaskApiRequest, only when req.GetBody is
  still nil.

Also remove the hand-rolled GetBody override in DoTaskApiRequest: it returned
the same already-consumed reader, so any transport-level replay would have
silently sent an empty body, and it clobbered the correct snapshot-based
GetBody that net/http derives from the *bytes.Reader bodies the task adaptors
pass in. For non-replayable bodies GetBody now stays nil, so a retry fails
loudly instead of corrupting the request.

Covered by unit tests plus an end-to-end raw-frame HTTP/2 test that resets
the first stream with REFUSED_STREAM after the body is written and asserts
the transport transparently retries with the complete body.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per the http.Request.GetBody contract ("returns a new copy of Body"),
each call must yield a reader with its own cursor. The previous
implementation rewound and reused the shared BodyStorage, so two
consecutive GetBody readers would interfere with each other, and a
replay could disturb the primary body's offset under extreme transport
timing (e.g. attempt N's body write not yet fully abandoned when the
transport builds attempt N+1).

Instead of snapshotting the payload (an extra copy), add
BodyStorage.NewReader, which returns an independent zero-copy reader:

- memory mode: a fresh bytes.Reader over the same immutable backing
  array;
- disk mode: a separate file descriptor over the cache file, so the
  transport closing a replayed body only closes that descriptor.

NewOutboundJSONBody's getBody now simply hands out storage.NewReader,
and once the handler releases the storage, GetBody fails with
ErrStorageClosed instead of replaying stale data.

Tests: interleaved reads across two replay readers and the primary
body each observe exactly their own byte stream, for both the memory
and the disk-backed storage; the existing GetBody and HTTP/2 retry
suites still pass (h2 e2e tests flake-free with -count=20).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Calcium-Ion
Calcium-Ion force-pushed the fix/upstream-request-getbody branch from c5952ee to dabee60 Compare August 6, 2026 07:40
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@Calcium-Ion

Calcium-Ion commented Aug 6, 2026

Copy link
Copy Markdown
Member

感谢 @BlockchainHe 提供核心的 BodyStorage.NewReader / Request.GetBody 实现;

本次补全包括:rebase 到最新 main(含 Alpha Search 迁移)、所有 pass-through/Sora/Jimeng 路径的 replay metadata、跨渠道尝试的字段清理与重新绑定,以及禁止 relay 自动跟随上游 301/302/303/307/308(3xx 原样交回错误处理链,HTTP/2 transport retry 不受影响)。

@Calcium-Ion Calcium-Ion self-assigned this Aug 6, 2026
@Calcium-Ion
Calcium-Ion merged commit d6b5ce9 into QuantumNous:main Aug 6, 2026
2 of 3 checks passed
youngshunf added a commit to youngshunf/new-api that referenced this pull request Aug 6, 2026
…s / 84 files)

上游本次内容:
- Feat/auto group (QuantumNous#6590):令牌自动分组(含前端 auto-group 编排 UI 与后端组选择)
- fix(aws): Bedrock 请求在客户端断连时取消 (QuantumNous#6589)
- fix(relay): 设置 Request.GetBody,让 HTTP/2 传输在上游 stream reset 后可透明重试 (QuantumNous#6249)
- refactor(relay): 把 replay 元数据移到请求体上(RelayInfo.UpstreamRequestBodySize 移除)

冲突 1 处,纯新增函数相邻,两侧保留:
- relay/channel/api_request.go:我方 upstreamRequestIDFromHeaders 与上游 keepUpstreamRedirectResponse
yiranxiaohui added a commit to yiranxiaohui/new-api that referenced this pull request Aug 7, 2026
Brings in from QuantumNous/new-api:
- fix: 修复兑换码额度精度损失 (QuantumNous#6685)
- feat(rate-limit): user critical rate limit for access token and aff transfer routes
- fix: test Claude/Gemini endpoints with native request format (QuantumNous#6698)
- feat(channels): refine fetched model categorization (QuantumNous#6632)
- security: atomic access-token rotation and aff updates (merge commit from fork)
- refactor(relay): move replay metadata onto request bodies
- fix(relay): set Request.GetBody for transparent HTTP/2 retry (QuantumNous#6249)
- Feat/auto group (QuantumNous#6590)
- fix(aws): cancel Bedrock requests on client disconnect (QuantumNous#6589)

Conflicts resolved:
- controller/model.go: adopt upstream's unified group-model loop with
  FormatMatchingModelName token-limit matching; keep fork's
  hiddenMappedModels filter inside the loop
- model/user.go: keep fork's BatchDeleteUsers; adopt upstream's atomic
  gorm.Expr-based inviteUser
- relay/channel/task/sora/adaptor_test.go: keep both fork's New API
  Video channel tests and upstream's replayable-body test

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ZteapnbTWdP52n7wZ6NsN
0401lucky pushed a commit to 0401lucky/new-api that referenced this pull request Aug 7, 2026
…ntly retry after an upstream stream reset (QuantumNous#6249)

* fix(relay): set Request.GetBody so the HTTP/2 transport can transparently retry after an upstream stream reset

The outbound request body is a type-erased io.Reader over BodyStorage, so
net/http cannot derive Request.GetBody (it only does so for *bytes.Reader,
*bytes.Buffer and *strings.Reader). With GetBody nil, the HTTP/2 transport
cannot transparently retry a request once the body has been written and the
upstream resets the stream with a retryable error (REFUSED_STREAM, or a
connection-level GOAWAY); the relay request then fails with:

    http2: Transport: cannot retry err [...] after Request.Body was written;
    define Request.GetBody to avoid this error

This affects every relay path that goes through DoApiRequest (chat, claude,
gemini, responses, embedding, image, rerank).

BodyStorage (memory and disk) already implements io.Seeker, so replay support
only needed wiring:

- NewOutboundJSONBody additionally returns a getBody that rewinds the storage
  and hands out a fresh non-closing reader. The transport only calls GetBody
  after the previous attempt's body has been abandoned, so the rewind cannot
  race an in-flight read.
- RelayInfo carries it in the new UpstreamRequestGetBody field, set alongside
  UpstreamRequestBodySize by the handlers that build storage-backed bodies.
- applyUpstreamGetBody (symmetric with applyUpstreamContentLength) wires it
  into DoApiRequest/DoFormRequest/DoTaskApiRequest, only when req.GetBody is
  still nil.

Also remove the hand-rolled GetBody override in DoTaskApiRequest: it returned
the same already-consumed reader, so any transport-level replay would have
silently sent an empty body, and it clobbered the correct snapshot-based
GetBody that net/http derives from the *bytes.Reader bodies the task adaptors
pass in. For non-replayable bodies GetBody now stays nil, so a retry fails
loudly instead of corrupting the request.

Covered by unit tests plus an end-to-end raw-frame HTTP/2 test that resets
the first stream with REFUSED_STREAM after the body is written and asserts
the transport transparently retries with the complete body.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(relay): hand out independent readers from GetBody (address review)

Per the http.Request.GetBody contract ("returns a new copy of Body"),
each call must yield a reader with its own cursor. The previous
implementation rewound and reused the shared BodyStorage, so two
consecutive GetBody readers would interfere with each other, and a
replay could disturb the primary body's offset under extreme transport
timing (e.g. attempt N's body write not yet fully abandoned when the
transport builds attempt N+1).

Instead of snapshotting the payload (an extra copy), add
BodyStorage.NewReader, which returns an independent zero-copy reader:

- memory mode: a fresh bytes.Reader over the same immutable backing
  array;
- disk mode: a separate file descriptor over the cache file, so the
  transport closing a replayed body only closes that descriptor.

NewOutboundJSONBody's getBody now simply hands out storage.NewReader,
and once the handler releases the storage, GetBody fails with
ErrStorageClosed instead of replaying stale data.

Tests: interleaved reads across two replay readers and the primary
body each observe exactly their own byte stream, for both the memory
and the disk-backed storage; the existing GetBody and HTTP/2 retry
suites still pass (h2 e2e tests flake-free with -count=20).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(relay): bind replayable metadata on pass-through requests

* fix(relay): reset upstream body metadata between channels

* test(relay): cover replay across retries and channel attempts

* fix(relay): stop following upstream redirects

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
0401lucky added a commit to 0401lucky/new-api that referenced this pull request Aug 7, 2026
- fix(relay): set Request.GetBody so HTTP/2 can transparently retry after upstream stream reset (QuantumNous#6249)
- refactor(relay): move replay metadata onto request bodies (适配 fork: 保留 types.PriceData)
- feat(channels): refine fetched model categorization (QuantumNous#6632)
- fix: test Claude/Gemini endpoints with native request format (QuantumNous#6698)
- feat(rate-limit): add user critical rate limit middleware (access-token / aff-transfer 路由)
- fix: 修复兑换码额度精度损失 (QuantumNous#6685) (保留 fork 随机额度/下载/100000 批量特性)

跳过: relaykit 模块重构系列(86ac0f7 等, 按 fork 策略), GitCode 发布 CI 工作流, 已同步的 24 个 cherry-pick 副本
sixwaaaay added a commit to sixwaaaay/new-api that referenced this pull request Aug 8, 2026
- fix(relay): set Request.GetBody for HTTP/2 transparent retry (QuantumNous#6249)
- refactor(relay): move replay metadata onto request bodies
- fix: user model concurrency safety (access token rotation, invite counters)
- feat(rate-limit): user critical rate limit middleware
- fix(ali): stop injecting top_p into requests that omit it
- fix: test Claude/Gemini endpoints with native request format
- feat(channels): refine fetched model categorization
- fix: redemption code quota precision loss
- drop sync-release-to-gitcode.yml (kept our deletion)
latioswang added a commit to trycortexai/new-api that referenced this pull request Aug 10, 2026
* fix(relay): set Request.GetBody so the HTTP/2 transport can transparently retry after an upstream stream reset (QuantumNous#6249)

* fix(relay): set Request.GetBody so the HTTP/2 transport can transparently retry after an upstream stream reset

The outbound request body is a type-erased io.Reader over BodyStorage, so
net/http cannot derive Request.GetBody (it only does so for *bytes.Reader,
*bytes.Buffer and *strings.Reader). With GetBody nil, the HTTP/2 transport
cannot transparently retry a request once the body has been written and the
upstream resets the stream with a retryable error (REFUSED_STREAM, or a
connection-level GOAWAY); the relay request then fails with:

    http2: Transport: cannot retry err [...] after Request.Body was written;
    define Request.GetBody to avoid this error

This affects every relay path that goes through DoApiRequest (chat, claude,
gemini, responses, embedding, image, rerank).

BodyStorage (memory and disk) already implements io.Seeker, so replay support
only needed wiring:

- NewOutboundJSONBody additionally returns a getBody that rewinds the storage
  and hands out a fresh non-closing reader. The transport only calls GetBody
  after the previous attempt's body has been abandoned, so the rewind cannot
  race an in-flight read.
- RelayInfo carries it in the new UpstreamRequestGetBody field, set alongside
  UpstreamRequestBodySize by the handlers that build storage-backed bodies.
- applyUpstreamGetBody (symmetric with applyUpstreamContentLength) wires it
  into DoApiRequest/DoFormRequest/DoTaskApiRequest, only when req.GetBody is
  still nil.

Also remove the hand-rolled GetBody override in DoTaskApiRequest: it returned
the same already-consumed reader, so any transport-level replay would have
silently sent an empty body, and it clobbered the correct snapshot-based
GetBody that net/http derives from the *bytes.Reader bodies the task adaptors
pass in. For non-replayable bodies GetBody now stays nil, so a retry fails
loudly instead of corrupting the request.

Covered by unit tests plus an end-to-end raw-frame HTTP/2 test that resets
the first stream with REFUSED_STREAM after the body is written and asserts
the transport transparently retries with the complete body.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(relay): hand out independent readers from GetBody (address review)

Per the http.Request.GetBody contract ("returns a new copy of Body"),
each call must yield a reader with its own cursor. The previous
implementation rewound and reused the shared BodyStorage, so two
consecutive GetBody readers would interfere with each other, and a
replay could disturb the primary body's offset under extreme transport
timing (e.g. attempt N's body write not yet fully abandoned when the
transport builds attempt N+1).

Instead of snapshotting the payload (an extra copy), add
BodyStorage.NewReader, which returns an independent zero-copy reader:

- memory mode: a fresh bytes.Reader over the same immutable backing
  array;
- disk mode: a separate file descriptor over the cache file, so the
  transport closing a replayed body only closes that descriptor.

NewOutboundJSONBody's getBody now simply hands out storage.NewReader,
and once the handler releases the storage, GetBody fails with
ErrStorageClosed instead of replaying stale data.

Tests: interleaved reads across two replay readers and the primary
body each observe exactly their own byte stream, for both the memory
and the disk-backed storage; the existing GetBody and HTTP/2 retry
suites still pass (h2 e2e tests flake-free with -count=20).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(relay): bind replayable metadata on pass-through requests

* fix(relay): reset upstream body metadata between channels

* test(relay): cover replay across retries and channel attempts

* fix(relay): stop following upstream redirects

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* refactor(relay): move replay metadata onto request bodies

* Merge commit from fork

* feat(channels): refine fetched model categorization (QuantumNous#6632)

* feat(channels): refine fetched model categorization

* fix: channel category

* fix: hy3 category

* fix: test Claude/Gemini endpoints with native request format (QuantumNous#6698)

* feat(rate-limit): add user critical rate limit middleware for access token and aff transfer routes

* fix: 修复兑换码额度精度损失 (QuantumNous#6685)

* fix: 修复兑换码额度精度损失(QuantumNous#6680)

* fix(redemption): guard update data integrity

* CI: enhance release synchronization workflow with optional file syncing

* fix(ali): stop injecting top_p into requests that omit it (QuantumNous#6674)

* fix(channels): classify Qwen TTS models correctly (QuantumNous#6711)

* feat(channels): add auto-disable-only channel test mode (QuantumNous#6728)

* perf(web): debounce server and large-list searches (QuantumNous#6727)

* fix: record reasoning effort consistently in usage logs (QuantumNous#6641)

* feat(relay): expose user and group context to parameter overrides (QuantumNous#6534)

* fix(ollama): preserve reasoning and tool-call context (QuantumNous#6605)

* fix: backend length validation (QuantumNous#5548)

* feat(billing): highlight matched conditional multipliers in logs (QuantumNous#6561)

* feat(billing): highlight matched conditional multipliers in usage logs

* fix(billing): make request rule tracing stable and type-safe

* fix(web): require confirmation before rotating access token (QuantumNous#6749)

---------

Co-authored-by: Lucas <hepo.lucas@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: CaIon <i@caion.me>
Co-authored-by: RedwindA <128586631+RedwindA@users.noreply.github.com>
Co-authored-by: Seefs <40468931+seefs001@users.noreply.github.com>
Co-authored-by: lihu-001 <lihu9048@gmail.com>
Co-authored-by: ENCHIGO <38551565+ENCHIGO@users.noreply.github.com>
2hot4you pushed a commit to 2hot4you/new-api that referenced this pull request Aug 13, 2026
…ntly retry after an upstream stream reset (QuantumNous#6249)

* fix(relay): set Request.GetBody so the HTTP/2 transport can transparently retry after an upstream stream reset

The outbound request body is a type-erased io.Reader over BodyStorage, so
net/http cannot derive Request.GetBody (it only does so for *bytes.Reader,
*bytes.Buffer and *strings.Reader). With GetBody nil, the HTTP/2 transport
cannot transparently retry a request once the body has been written and the
upstream resets the stream with a retryable error (REFUSED_STREAM, or a
connection-level GOAWAY); the relay request then fails with:

    http2: Transport: cannot retry err [...] after Request.Body was written;
    define Request.GetBody to avoid this error

This affects every relay path that goes through DoApiRequest (chat, claude,
gemini, responses, embedding, image, rerank).

BodyStorage (memory and disk) already implements io.Seeker, so replay support
only needed wiring:

- NewOutboundJSONBody additionally returns a getBody that rewinds the storage
  and hands out a fresh non-closing reader. The transport only calls GetBody
  after the previous attempt's body has been abandoned, so the rewind cannot
  race an in-flight read.
- RelayInfo carries it in the new UpstreamRequestGetBody field, set alongside
  UpstreamRequestBodySize by the handlers that build storage-backed bodies.
- applyUpstreamGetBody (symmetric with applyUpstreamContentLength) wires it
  into DoApiRequest/DoFormRequest/DoTaskApiRequest, only when req.GetBody is
  still nil.

Also remove the hand-rolled GetBody override in DoTaskApiRequest: it returned
the same already-consumed reader, so any transport-level replay would have
silently sent an empty body, and it clobbered the correct snapshot-based
GetBody that net/http derives from the *bytes.Reader bodies the task adaptors
pass in. For non-replayable bodies GetBody now stays nil, so a retry fails
loudly instead of corrupting the request.

Covered by unit tests plus an end-to-end raw-frame HTTP/2 test that resets
the first stream with REFUSED_STREAM after the body is written and asserts
the transport transparently retries with the complete body.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(relay): hand out independent readers from GetBody (address review)

Per the http.Request.GetBody contract ("returns a new copy of Body"),
each call must yield a reader with its own cursor. The previous
implementation rewound and reused the shared BodyStorage, so two
consecutive GetBody readers would interfere with each other, and a
replay could disturb the primary body's offset under extreme transport
timing (e.g. attempt N's body write not yet fully abandoned when the
transport builds attempt N+1).

Instead of snapshotting the payload (an extra copy), add
BodyStorage.NewReader, which returns an independent zero-copy reader:

- memory mode: a fresh bytes.Reader over the same immutable backing
  array;
- disk mode: a separate file descriptor over the cache file, so the
  transport closing a replayed body only closes that descriptor.

NewOutboundJSONBody's getBody now simply hands out storage.NewReader,
and once the handler releases the storage, GetBody fails with
ErrStorageClosed instead of replaying stale data.

Tests: interleaved reads across two replay readers and the primary
body each observe exactly their own byte stream, for both the memory
and the disk-backed storage; the existing GetBody and HTTP/2 retry
suites still pass (h2 e2e tests flake-free with -count=20).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(relay): bind replayable metadata on pass-through requests

* fix(relay): reset upstream body metadata between channels

* test(relay): cover replay across retries and channel attempts

* fix(relay): stop following upstream redirects

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit d6b5ce9)
yuqiyi pushed a commit to yuqiyi/new-api that referenced this pull request Aug 16, 2026
* v1.0.0-rc.24: (117 commits)
  CI: enhance release synchronization workflow with optional file syncing
  fix: 修复兑换码额度精度损失 (QuantumNous#6685)
  feat(rate-limit): add user critical rate limit middleware for access token and aff transfer routes
  fix: test Claude/Gemini endpoints with native request format (QuantumNous#6698)
  feat(channels): refine fetched model categorization (QuantumNous#6632)
  Merge commit from fork
  refactor(relay): move replay metadata onto request bodies
  fix(relay): set Request.GetBody so the HTTP/2 transport can transparently retry after an upstream stream reset (QuantumNous#6249)
  Feat/auto group (QuantumNous#6590)
  fix(aws): cancel Bedrock requests on client disconnect (QuantumNous#6589)
  fix(billing): harden tiered retry group-switch billing (QuantumNous#6570)
  fix(billing): settle tiered retries with final group (QuantumNous#6518)
  feat: deepseek responses api (QuantumNous#6562)
  style: use text-sm for public header nav links to match other nav components (QuantumNous#6557)
  fix(oauth): stop treating a foreign window.opener as a bind flow (QuantumNous#6425)
  fix(relay): preserve multipart image edits for New API channels (QuantumNous#6559)
  feat(logs): expose stream status to log owners (QuantumNous#6558)
  feat: support zstd request decompression (QuantumNous#6545)
  fix: preserve Qwen thinking_budget passthrough (QuantumNous#5836)
  feat(oidc): 支持自定义 OIDC 登录显示名称 (QuantumNous#6012)
  ...

# Conflicts:
#	service/text_quota.go
#	web/src/features/models/components/drawers/model-mutate-drawer.tsx
#	web/src/features/pricing/components/model-details.tsx
#	web/src/features/pricing/lib/price.ts
DayFliggy pushed a commit to DayFliggy/Ren2Hub that referenced this pull request Aug 17, 2026
…ntly retry after an upstream stream reset (QuantumNous#6249)

* fix(relay): set Request.GetBody so the HTTP/2 transport can transparently retry after an upstream stream reset

The outbound request body is a type-erased io.Reader over BodyStorage, so
net/http cannot derive Request.GetBody (it only does so for *bytes.Reader,
*bytes.Buffer and *strings.Reader). With GetBody nil, the HTTP/2 transport
cannot transparently retry a request once the body has been written and the
upstream resets the stream with a retryable error (REFUSED_STREAM, or a
connection-level GOAWAY); the relay request then fails with:

    http2: Transport: cannot retry err [...] after Request.Body was written;
    define Request.GetBody to avoid this error

This affects every relay path that goes through DoApiRequest (chat, claude,
gemini, responses, embedding, image, rerank).

BodyStorage (memory and disk) already implements io.Seeker, so replay support
only needed wiring:

- NewOutboundJSONBody additionally returns a getBody that rewinds the storage
  and hands out a fresh non-closing reader. The transport only calls GetBody
  after the previous attempt's body has been abandoned, so the rewind cannot
  race an in-flight read.
- RelayInfo carries it in the new UpstreamRequestGetBody field, set alongside
  UpstreamRequestBodySize by the handlers that build storage-backed bodies.
- applyUpstreamGetBody (symmetric with applyUpstreamContentLength) wires it
  into DoApiRequest/DoFormRequest/DoTaskApiRequest, only when req.GetBody is
  still nil.

Also remove the hand-rolled GetBody override in DoTaskApiRequest: it returned
the same already-consumed reader, so any transport-level replay would have
silently sent an empty body, and it clobbered the correct snapshot-based
GetBody that net/http derives from the *bytes.Reader bodies the task adaptors
pass in. For non-replayable bodies GetBody now stays nil, so a retry fails
loudly instead of corrupting the request.

Covered by unit tests plus an end-to-end raw-frame HTTP/2 test that resets
the first stream with REFUSED_STREAM after the body is written and asserts
the transport transparently retries with the complete body.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(relay): hand out independent readers from GetBody (address review)

Per the http.Request.GetBody contract ("returns a new copy of Body"),
each call must yield a reader with its own cursor. The previous
implementation rewound and reused the shared BodyStorage, so two
consecutive GetBody readers would interfere with each other, and a
replay could disturb the primary body's offset under extreme transport
timing (e.g. attempt N's body write not yet fully abandoned when the
transport builds attempt N+1).

Instead of snapshotting the payload (an extra copy), add
BodyStorage.NewReader, which returns an independent zero-copy reader:

- memory mode: a fresh bytes.Reader over the same immutable backing
  array;
- disk mode: a separate file descriptor over the cache file, so the
  transport closing a replayed body only closes that descriptor.

NewOutboundJSONBody's getBody now simply hands out storage.NewReader,
and once the handler releases the storage, GetBody fails with
ErrStorageClosed instead of replaying stale data.

Tests: interleaved reads across two replay readers and the primary
body each observe exactly their own byte stream, for both the memory
and the disk-backed storage; the existing GetBody and HTTP/2 retry
suites still pass (h2 e2e tests flake-free with -count=20).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(relay): bind replayable metadata on pass-through requests

* fix(relay): reset upstream body metadata between channels

* test(relay): cover replay across retries and channel attempts

* fix(relay): stop following upstream redirects

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
330079598 pushed a commit to 330079598/new-api that referenced this pull request Aug 19, 2026
…ntly retry after an upstream stream reset (QuantumNous#6249)

* fix(relay): set Request.GetBody so the HTTP/2 transport can transparently retry after an upstream stream reset

The outbound request body is a type-erased io.Reader over BodyStorage, so
net/http cannot derive Request.GetBody (it only does so for *bytes.Reader,
*bytes.Buffer and *strings.Reader). With GetBody nil, the HTTP/2 transport
cannot transparently retry a request once the body has been written and the
upstream resets the stream with a retryable error (REFUSED_STREAM, or a
connection-level GOAWAY); the relay request then fails with:

    http2: Transport: cannot retry err [...] after Request.Body was written;
    define Request.GetBody to avoid this error

This affects every relay path that goes through DoApiRequest (chat, claude,
gemini, responses, embedding, image, rerank).

BodyStorage (memory and disk) already implements io.Seeker, so replay support
only needed wiring:

- NewOutboundJSONBody additionally returns a getBody that rewinds the storage
  and hands out a fresh non-closing reader. The transport only calls GetBody
  after the previous attempt's body has been abandoned, so the rewind cannot
  race an in-flight read.
- RelayInfo carries it in the new UpstreamRequestGetBody field, set alongside
  UpstreamRequestBodySize by the handlers that build storage-backed bodies.
- applyUpstreamGetBody (symmetric with applyUpstreamContentLength) wires it
  into DoApiRequest/DoFormRequest/DoTaskApiRequest, only when req.GetBody is
  still nil.

Also remove the hand-rolled GetBody override in DoTaskApiRequest: it returned
the same already-consumed reader, so any transport-level replay would have
silently sent an empty body, and it clobbered the correct snapshot-based
GetBody that net/http derives from the *bytes.Reader bodies the task adaptors
pass in. For non-replayable bodies GetBody now stays nil, so a retry fails
loudly instead of corrupting the request.

Covered by unit tests plus an end-to-end raw-frame HTTP/2 test that resets
the first stream with REFUSED_STREAM after the body is written and asserts
the transport transparently retries with the complete body.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(relay): hand out independent readers from GetBody (address review)

Per the http.Request.GetBody contract ("returns a new copy of Body"),
each call must yield a reader with its own cursor. The previous
implementation rewound and reused the shared BodyStorage, so two
consecutive GetBody readers would interfere with each other, and a
replay could disturb the primary body's offset under extreme transport
timing (e.g. attempt N's body write not yet fully abandoned when the
transport builds attempt N+1).

Instead of snapshotting the payload (an extra copy), add
BodyStorage.NewReader, which returns an independent zero-copy reader:

- memory mode: a fresh bytes.Reader over the same immutable backing
  array;
- disk mode: a separate file descriptor over the cache file, so the
  transport closing a replayed body only closes that descriptor.

NewOutboundJSONBody's getBody now simply hands out storage.NewReader,
and once the handler releases the storage, GetBody fails with
ErrStorageClosed instead of replaying stale data.

Tests: interleaved reads across two replay readers and the primary
body each observe exactly their own byte stream, for both the memory
and the disk-backed storage; the existing GetBody and HTTP/2 retry
suites still pass (h2 e2e tests flake-free with -count=20).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(relay): bind replayable metadata on pass-through requests

* fix(relay): reset upstream body metadata between channels

* test(relay): cover replay across retries and channel attempts

* fix(relay): stop following upstream redirects

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants