fix: avoid stale stream writes after client disconnect - #5710
Conversation
WalkthroughAdds request-context guards to SSE writes, rewrites stream-scanner shutdown and ping cleanup to wait for goroutines, routes image streaming through shared helpers, and adds a cancellation test for streamed responses. ChangesStreaming goroutine lifecycle fix
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Warning Tools execution failed with the following error: Failed to run tools: 13 INTERNAL: Received RST_STREAM with code 2 (Internal server error) 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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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/helper/stream_scanner.go`:
- Around line 82-95: The stream scanner cleanup in stream_scanner.go is
happening too late because the deferred block still allows the final status log
to run while goroutines may be active. Move the cancel, response body close,
ticker stops, and wg.Wait logic into a cleanup function protected by sync.Once,
then invoke that cleanup at the done: path before the logging that reads c and
info.ReceivedResponseCount, while keeping defer cleanup() as a fallback safety
net. Use the existing stream-scanner flow around the main scan loop and final
status logging to place this change.
- Around line 124-126: The PingData(c) call inside stream_scanner.go is holding
writeMutex around an unbounded write/flush path, which can block cleanup if the
client is slow or half-closed. Update the ping write in the scanner loop to use
a bounded write path, such as setting a write deadline on the response writer
before calling PingData, and keep the lock held only as long as needed around
the actual write. Use the existing writeMutex and PingData(c) location to make
the change without altering the surrounding cleanup flow.
🪄 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: f87ca34f-26ff-4535-87bb-9e2e409f0fd4
📒 Files selected for processing (3)
relay/helper/common.gorelay/helper/stream_scanner.gorelay/helper/stream_scanner_test.go
There was a problem hiding this comment.
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/channel/api_request.go`:
- Around line 457-468: The synchronous helper.PingData(c) call in sendPingData
can block forever on c.Writer write/flush and prevent stopPinger() from being
observed, so update the ping path to be cancellation-aware or time-bounded
before waiting on the mutex/deferred pingerDone flow. Use the sendPingData
function and the helper.PingData invocation to locate the change, and ensure
stalled writes return or unblock on context cancellation so the handler cannot
hang indefinitely.
🪄 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: 48a520a5-72d2-42a6-8c33-421f2884987a
📒 Files selected for processing (2)
relay/channel/api_request.gorelay/helper/stream_scanner.go
🚧 Files skipped from review as they are similar to previous changes (1)
- relay/helper/stream_scanner.go
There was a problem hiding this comment.
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/channel/openai/relay_image.go`:
- Around line 272-279: The SSE write path in writeOpenaiImageStreamData and
writeOpenaiImageStreamDone can silently ignore client disconnects because
helper.ResponseChunkData has no error return and helper.StringData returns nil
on a canceled context. Update these helpers to detect when the request context
is already done and propagate that cancellation back to the caller instead of
treating it as a successful write, so the stream can be ended as client_gone
rather than StreamEndReasonDone.
🪄 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: bd239bf7-818c-4c6b-99cc-abbef10b2b64
📒 Files selected for processing (2)
relay/channel/openai/relay_image.gorelay/helper/stream_scanner.go
🚧 Files skipped from review as they are similar to previous changes (1)
- relay/helper/stream_scanner.go
| helper.ResponseChunkData(c, dto.ResponsesStreamResponse{Type: eventName}, string(data)) | ||
| return nil | ||
| } | ||
| return helper.FlushWriter(c) | ||
| return helper.StringData(c, string(data)) | ||
| } | ||
|
|
||
| func writeOpenaiImageStreamDone(c *gin.Context) error { | ||
| if _, err := fmt.Fprint(c.Writer, "data: [DONE]\n\n"); err != nil { | ||
| return err | ||
| } | ||
| return helper.FlushWriter(c) | ||
| return helper.StringData(c, "[DONE]") |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Return cancellation errors after guarded SSE writes.
ResponseChunkData has no error return, and StringData returns nil when the request context is already done. That means this path can silently skip payload or [DONE] writes after client disconnect, then the caller records StreamEndReasonDone instead of client_gone.
Proposed fix
if eventName != "" {
+ if err := openaiImageRequestContextErr(c); err != nil {
+ return err
+ }
helper.ResponseChunkData(c, dto.ResponsesStreamResponse{Type: eventName}, string(data))
- return nil
+ return openaiImageRequestContextErr(c)
}
- return helper.StringData(c, string(data))
+ if err := helper.StringData(c, string(data)); err != nil {
+ return err
+ }
+ return openaiImageRequestContextErr(c)
}
func writeOpenaiImageStreamDone(c *gin.Context) error {
- return helper.StringData(c, "[DONE]")
+ if err := helper.StringData(c, "[DONE]"); err != nil {
+ return err
+ }
+ return openaiImageRequestContextErr(c)
+}
+
+func openaiImageRequestContextErr(c *gin.Context) error {
+ if c == nil || c.Request == nil {
+ return nil
+ }
+ return c.Request.Context().Err()
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| helper.ResponseChunkData(c, dto.ResponsesStreamResponse{Type: eventName}, string(data)) | |
| return nil | |
| } | |
| return helper.FlushWriter(c) | |
| return helper.StringData(c, string(data)) | |
| } | |
| func writeOpenaiImageStreamDone(c *gin.Context) error { | |
| if _, err := fmt.Fprint(c.Writer, "data: [DONE]\n\n"); err != nil { | |
| return err | |
| } | |
| return helper.FlushWriter(c) | |
| return helper.StringData(c, "[DONE]") | |
| helper.ResponseChunkData(c, dto.ResponsesStreamResponse{Type: eventName}, string(data)) | |
| return openaiImageRequestContextErr(c) | |
| } | |
| if err := helper.StringData(c, string(data)); err != nil { | |
| return err | |
| } | |
| return openaiImageRequestContextErr(c) | |
| } | |
| func writeOpenaiImageStreamDone(c *gin.Context) error { | |
| if err := helper.StringData(c, "[DONE]"); err != nil { | |
| return err | |
| } | |
| return openaiImageRequestContextErr(c) | |
| } | |
| func openaiImageRequestContextErr(c *gin.Context) error { | |
| if c == nil || c.Request == nil { | |
| return nil | |
| } | |
| return c.Request.Context().Err() | |
| } |
🤖 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/channel/openai/relay_image.go` around lines 272 - 279, The SSE write
path in writeOpenaiImageStreamData and writeOpenaiImageStreamDone can silently
ignore client disconnects because helper.ResponseChunkData has no error return
and helper.StringData returns nil on a canceled context. Update these helpers to
detect when the request context is already done and propagate that cancellation
back to the caller instead of treating it as a successful write, so the stream
can be ended as client_gone rather than StreamEndReasonDone.
…racts Keep the goroutine-lifecycle fix (unconditional wg.Wait before returning the gin.Context, close resp.Body inside cleanup), but drop the drain-on-disconnect behavior: when the client goes away, cleanup now runs immediately so the upstream body is closed, the provider stops generating, and users are not billed for tokens produced after they disconnected. Also restore FlushWriter/StringData/PingData returning an error when the request context is done, so non-scanner relay loops (ollama, fake-stream, audio, image) keep their disconnect awareness instead of silently consuming the upstream to completion. ResponseChunkData now propagates write errors. Add a bounded per-write deadline (http.NewResponseController) before each locked stream write so a slow-but-connected client cannot block a write forever and hang the unconditional wg.Wait.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
relay/channel/openai/helper.go (1)
205-210: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winSwallowed write error diverges from the "propagate write errors" objective.
The PR aims to propagate
ResponseChunkDatawrite errors so streaming paths stop consuming upstream after client disconnect, yetsendResponsesStreamDatadiscards it via_. Since this helper isvoid, a write failure (e.g., request context done) won't stop the enclosing loop here. Consider returning the error to the caller so this path can abort likerelay_image.go(return helper.ResponseChunkData(...)).🤖 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/channel/openai/helper.go` around lines 205 - 210, The sendResponsesStreamData helper is swallowing ResponseChunkData write failures by assigning the result to _, which prevents streaming callers from stopping on client disconnect or other write errors. Update sendResponsesStreamData to return the error from helper.ResponseChunkData and have the caller in the streaming flow check and propagate it, matching the pattern used in relay_image.go so the loop can abort immediately on write failure.
🤖 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/channel/openai/helper.go`:
- Around line 205-210: The sendResponsesStreamData helper is swallowing
ResponseChunkData write failures by assigning the result to _, which prevents
streaming callers from stopping on client disconnect or other write errors.
Update sendResponsesStreamData to return the error from helper.ResponseChunkData
and have the caller in the streaming flow check and propagate it, matching the
pattern used in relay_image.go so the loop can abort immediately on write
failure.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: b30baa9c-681c-4c89-96e5-81d1c0a16eb4
📒 Files selected for processing (6)
relay/channel/api_request.gorelay/channel/openai/helper.gorelay/channel/openai/relay_image.gorelay/helper/common.gorelay/helper/stream_scanner.gorelay/helper/stream_scanner_test.go
🚧 Files skipped from review as they are similar to previous changes (3)
- relay/channel/openai/relay_image.go
- relay/helper/common.go
- relay/channel/api_request.go
冲突 3 处(全在 relay/,按同步策略:常规后端跟上游、保留 fork 有意特性): - relay/helper/stream_scanner.go: 保留 fork「流异常补发 SSE error 事件」+ 采纳上游 QuantumNous#5710 显式 cleanup()(stale-write 修复) - relay/helper/common.go(ResponseChunkData): 保留上游模型名脱敏 + 采纳上游 return FlushWriter(c) 错误透传 - relay/channel/openai/relay_image.go: 采纳上游改用 helper.StringData()(其内部已脱敏,fork 脱敏等价保留) 前端/i18n 自动合并无冲突。全量 build 通过。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Merges 94 upstream commits (rc.14 → rc.18): SSRF protection, secure session cookies, better admin permissions, billing/quota hardening, ClickHouse log LIKE-filter adaptation, graceful shutdown, system task runner + instance info panel, Responses↔Chat conversion, Playground/Markdown improvements, media model support (Wan2.7, doubao seedance-2.0), and a global `bun format` reflow. 12 files conflicted; resolved linearly (upstream base + surgical fork re-apply), all 9 fork customizations retained + build-verified: - relay/helper/stream_scanner.go (§8): upstream 153d7f0 (QuantumNous#5710) rewrote this (goroutine lifecycle, per-write timeout) and INDEPENDENTLY dropped drain-on- disconnect — matching the fork's own conclusion. Adopted upstream's single- select immediate-cleanup design; re-applied MarkClientGone telemetry to the main-loop client-disconnect case only. - web/classic/rsbuild.config.ts: DROPPED the fork's date-fns alias — upstream fixed the same classic-build issue itself (Dockerfile-level). Verified: real docker build of both frontend stages passes. - Frontend fork files (§1/§4/§7/§9): re-applied over upstream's bun-format reflow + new imports (useState/GitBranch); kept model-filter min-w-[20rem]. - i18n (6 default locales): union of fork keys + upstream's new keys. - Backend §1/§4/§5/§6/§7 (model/log.go, usedata, router, api_request, init) auto-merged cleanly; verified fork logic + upstream ClickHouse LIKE both present. APP_VERSION → v1.0.0-rc.18. FORK-CHANGES.md §8 churn note added. Verified: go build (all pkgs) ✓, frontend tsc 0 errors ✓, docker build default + classic ✓. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…codes * origin/main: (180 commits) fix(billing): extend quantity validation and saturating conversions to remaining paths fix(billing): validate quantity parameters and harden quota calculations Fix/build date dns error (QuantumNous#5945) fix: avoid stale stream writes after client disconnect (QuantumNous#5710) feat(group): enhance group ratio editor with improved visibility rules and JSON parsing feat: optimize legacy top-up warning banner copy (QuantumNous#5851) (QuantumNous#5855) fix(web): redirect authenticated users away from sign-up page (QuantumNous#5910) feat(ssrf): implement SSRF protection in HTTP clients and validation functions feat(user): better messages for redeem failures fix(html): 修复 Shadow DOM 隔离渲染下深浅色模式无法自动切换的问题 (QuantumNous#5890) fix(web): sync home iframe theme and language (QuantumNous#5917) fix: 任务差额结算后 quota 和阿里视频时长优化 (QuantumNous#5923) fix(web): refine mobile user cards test(user): cover self-service password update guard feat(session): support opt-in Secure session cookies fix(auth): allow read-only access for non-disabled tokens fix(user): harden account email and password handling fix: align dynamic pricing style with log details dialog sections fix(channels): show field passthrough controls for Codex (QuantumNous#5902) fix(user): trim whitespace from username and validate input ... # Conflicts: # controller/redemption.go # controller/user.go # model/option.go # model/redemption.go # model/user.go # web/default/src/features/auth/api.ts # web/default/src/features/redemption-codes/api.ts # web/default/src/features/redemption-codes/components/redemptions-provider.tsx # web/default/src/features/redemption-codes/components/redemptions-table.tsx # web/default/src/features/redemption-codes/constants.ts # web/default/src/hooks/use-sidebar-data.ts # web/default/src/i18n/static-keys.ts # web/default/src/routes/__root.tsx
) * fix: avoid stale stream writes after client disconnect * fix: wait for stream ping goroutines before returning * fix: log stream results after goroutine cleanup * fix: broadcast stream stop signals * fix: abort upstream on client disconnect and restore write error contracts Keep the goroutine-lifecycle fix (unconditional wg.Wait before returning the gin.Context, close resp.Body inside cleanup), but drop the drain-on-disconnect behavior: when the client goes away, cleanup now runs immediately so the upstream body is closed, the provider stops generating, and users are not billed for tokens produced after they disconnected. Also restore FlushWriter/StringData/PingData returning an error when the request context is done, so non-scanner relay loops (ollama, fake-stream, audio, image) keep their disconnect awareness instead of silently consuming the upstream to completion. ResponseChunkData now propagates write errors. Add a bounded per-write deadline (http.NewResponseController) before each locked stream write so a slow-but-connected client cannot block a write forever and hang the unconditional wg.Wait. --------- Co-authored-by: CaIon <i@caion.me>
) * fix: avoid stale stream writes after client disconnect * fix: wait for stream ping goroutines before returning * fix: log stream results after goroutine cleanup * fix: broadcast stream stop signals * fix: abort upstream on client disconnect and restore write error contracts Keep the goroutine-lifecycle fix (unconditional wg.Wait before returning the gin.Context, close resp.Body inside cleanup), but drop the drain-on-disconnect behavior: when the client goes away, cleanup now runs immediately so the upstream body is closed, the provider stops generating, and users are not billed for tokens produced after they disconnected. Also restore FlushWriter/StringData/PingData returning an error when the request context is done, so non-scanner relay loops (ollama, fake-stream, audio, image) keep their disconnect awareness instead of silently consuming the upstream to completion. ResponseChunkData now propagates write errors. Add a bounded per-write deadline (http.NewResponseController) before each locked stream write so a slow-but-connected client cannot block a write forever and hang the unconditional wg.Wait. --------- Co-authored-by: CaIon <i@caion.me>
) * fix: avoid stale stream writes after client disconnect * fix: wait for stream ping goroutines before returning * fix: log stream results after goroutine cleanup * fix: broadcast stream stop signals * fix: abort upstream on client disconnect and restore write error contracts Keep the goroutine-lifecycle fix (unconditional wg.Wait before returning the gin.Context, close resp.Body inside cleanup), but drop the drain-on-disconnect behavior: when the client goes away, cleanup now runs immediately so the upstream body is closed, the provider stops generating, and users are not billed for tokens produced after they disconnected. Also restore FlushWriter/StringData/PingData returning an error when the request context is done, so non-scanner relay loops (ollama, fake-stream, audio, image) keep their disconnect awareness instead of silently consuming the upstream to completion. ResponseChunkData now propagates write errors. Add a bounded per-write deadline (http.NewResponseController) before each locked stream write so a slow-but-connected client cannot block a write forever and hang the unconditional wg.Wait. --------- Co-authored-by: CaIon <i@caion.me>
) * fix: avoid stale stream writes after client disconnect * fix: wait for stream ping goroutines before returning * fix: log stream results after goroutine cleanup * fix: broadcast stream stop signals * fix: abort upstream on client disconnect and restore write error contracts Keep the goroutine-lifecycle fix (unconditional wg.Wait before returning the gin.Context, close resp.Body inside cleanup), but drop the drain-on-disconnect behavior: when the client goes away, cleanup now runs immediately so the upstream body is closed, the provider stops generating, and users are not billed for tokens produced after they disconnected. Also restore FlushWriter/StringData/PingData returning an error when the request context is done, so non-scanner relay loops (ollama, fake-stream, audio, image) keep their disconnect awareness instead of silently consuming the upstream to completion. ResponseChunkData now propagates write errors. Add a bounded per-write deadline (http.NewResponseController) before each locked stream write so a slow-but-connected client cannot block a write forever and hang the unconditional wg.Wait. --------- Co-authored-by: CaIon <i@caion.me>
) * fix: avoid stale stream writes after client disconnect * fix: wait for stream ping goroutines before returning * fix: log stream results after goroutine cleanup * fix: broadcast stream stop signals * fix: abort upstream on client disconnect and restore write error contracts Keep the goroutine-lifecycle fix (unconditional wg.Wait before returning the gin.Context, close resp.Body inside cleanup), but drop the drain-on-disconnect behavior: when the client goes away, cleanup now runs immediately so the upstream body is closed, the provider stops generating, and users are not billed for tokens produced after they disconnected. Also restore FlushWriter/StringData/PingData returning an error when the request context is done, so non-scanner relay loops (ollama, fake-stream, audio, image) keep their disconnect awareness instead of silently consuming the upstream to completion. ResponseChunkData now propagates write errors. Add a bounded per-write deadline (http.NewResponseController) before each locked stream write so a slow-but-connected client cannot block a write forever and hang the unconditional wg.Wait. --------- Co-authored-by: CaIon <i@caion.me>
) * fix: avoid stale stream writes after client disconnect * fix: wait for stream ping goroutines before returning * fix: log stream results after goroutine cleanup * fix: broadcast stream stop signals * fix: abort upstream on client disconnect and restore write error contracts Keep the goroutine-lifecycle fix (unconditional wg.Wait before returning the gin.Context, close resp.Body inside cleanup), but drop the drain-on-disconnect behavior: when the client goes away, cleanup now runs immediately so the upstream body is closed, the provider stops generating, and users are not billed for tokens produced after they disconnected. Also restore FlushWriter/StringData/PingData returning an error when the request context is done, so non-scanner relay loops (ollama, fake-stream, audio, image) keep their disconnect awareness instead of silently consuming the upstream to completion. ResponseChunkData now propagates write errors. Add a bounded per-write deadline (http.NewResponseController) before each locked stream write so a slow-but-connected client cannot block a write forever and hang the unconditional wg.Wait. --------- Co-authored-by: CaIon <i@caion.me> (cherry picked from commit 153d7f0)
) * fix: avoid stale stream writes after client disconnect * fix: wait for stream ping goroutines before returning * fix: log stream results after goroutine cleanup * fix: broadcast stream stop signals * fix: abort upstream on client disconnect and restore write error contracts Keep the goroutine-lifecycle fix (unconditional wg.Wait before returning the gin.Context, close resp.Body inside cleanup), but drop the drain-on-disconnect behavior: when the client goes away, cleanup now runs immediately so the upstream body is closed, the provider stops generating, and users are not billed for tokens produced after they disconnected. Also restore FlushWriter/StringData/PingData returning an error when the request context is done, so non-scanner relay loops (ollama, fake-stream, audio, image) keep their disconnect awareness instead of silently consuming the upstream to completion. ResponseChunkData now propagates write errors. Add a bounded per-write deadline (http.NewResponseController) before each locked stream write so a slow-but-connected client cannot block a write forever and hang the unconditional wg.Wait. --------- Co-authored-by: CaIon <i@caion.me>
) * fix: avoid stale stream writes after client disconnect * fix: wait for stream ping goroutines before returning * fix: log stream results after goroutine cleanup * fix: broadcast stream stop signals * fix: abort upstream on client disconnect and restore write error contracts Keep the goroutine-lifecycle fix (unconditional wg.Wait before returning the gin.Context, close resp.Body inside cleanup), but drop the drain-on-disconnect behavior: when the client goes away, cleanup now runs immediately so the upstream body is closed, the provider stops generating, and users are not billed for tokens produced after they disconnected. Also restore FlushWriter/StringData/PingData returning an error when the request context is done, so non-scanner relay loops (ollama, fake-stream, audio, image) keep their disconnect awareness instead of silently consuming the upstream to completion. ResponseChunkData now propagates write errors. Add a bounded per-write deadline (http.NewResponseController) before each locked stream write so a slow-but-connected client cannot block a write forever and hang the unconditional wg.Wait. --------- Co-authored-by: CaIon <i@caion.me>
) * fix: avoid stale stream writes after client disconnect * fix: wait for stream ping goroutines before returning * fix: log stream results after goroutine cleanup * fix: broadcast stream stop signals * fix: abort upstream on client disconnect and restore write error contracts Keep the goroutine-lifecycle fix (unconditional wg.Wait before returning the gin.Context, close resp.Body inside cleanup), but drop the drain-on-disconnect behavior: when the client goes away, cleanup now runs immediately so the upstream body is closed, the provider stops generating, and users are not billed for tokens produced after they disconnected. Also restore FlushWriter/StringData/PingData returning an error when the request context is done, so non-scanner relay loops (ollama, fake-stream, audio, image) keep their disconnect awareness instead of silently consuming the upstream to completion. ResponseChunkData now propagates write errors. Add a bounded per-write deadline (http.NewResponseController) before each locked stream write so a slow-but-connected client cannot block a write forever and hang the unconditional wg.Wait. --------- Co-authored-by: CaIon <i@caion.me>
) * fix: avoid stale stream writes after client disconnect * fix: wait for stream ping goroutines before returning * fix: log stream results after goroutine cleanup * fix: broadcast stream stop signals * fix: abort upstream on client disconnect and restore write error contracts Keep the goroutine-lifecycle fix (unconditional wg.Wait before returning the gin.Context, close resp.Body inside cleanup), but drop the drain-on-disconnect behavior: when the client goes away, cleanup now runs immediately so the upstream body is closed, the provider stops generating, and users are not billed for tokens produced after they disconnected. Also restore FlushWriter/StringData/PingData returning an error when the request context is done, so non-scanner relay loops (ollama, fake-stream, audio, image) keep their disconnect awareness instead of silently consuming the upstream to completion. ResponseChunkData now propagates write errors. Add a bounded per-write deadline (http.NewResponseController) before each locked stream write so a slow-but-connected client cannot block a write forever and hang the unconditional wg.Wait. --------- Co-authored-by: CaIon <i@caion.me>
Important
📝 变更描述 / Description
(简述:做了什么?为什么这样改能生效?请基于你对代码逻辑的理解来写,避免粘贴未经整理的内容)
一个请求发出后,等相关 goroutine 都退出后再让 Gin 回收这个 ctx。
用户断开后,把 EndReason 记成 client_gone。
🚀 变更类型 / Type of change
🔗 关联任务 / Related Issue
✅ 提交前检查项 / Checklist
Bug fix,我已提交或关联对应 Issue,且不会将设计取舍、预期不一致或理解偏差直接归类为 bug。📸 运行证明 / Proof of Work
(请在此粘贴截图、关键日志或测试报告,以证明变更生效)
Summary by CodeRabbit