Skip to content

fix: propagate upstream stream errors to prevent truncated responses marked as success#917

Merged
ding113 merged 1 commit intoding113:devfrom
KevinShiCN:fix/stream-socket-error-916
Mar 15, 2026
Merged

fix: propagate upstream stream errors to prevent truncated responses marked as success#917
ding113 merged 1 commit intoding113:devfrom
KevinShiCN:fix/stream-socket-error-916

Conversation

@KevinShiCN
Copy link
Copy Markdown
Contributor

@KevinShiCN KevinShiCN commented Mar 14, 2026

Problem

When a SocketError (other side closed / UND_ERR_SOCKET) occurs during an active streaming response, the stream is silently truncated and marked as success. The client (e.g. Claude Code) receives a partial response with no error signal, causing it to stop mid-operation.

Log evidence:

12:24:03.409Z WARN  "Upstream stream error (gracefully closed)" error: "other side closed"
12:24:03.411Z INFO  "Streaming request finalized as success"  ← WRONG

Root Cause

In forwarder.ts, nodeStreamToWebStreamSafe() handles upstream stream errors by calling controller.close() — which signals a normal end-of-stream. The downstream ResponseHandler reader gets { done: true }, sets streamEndedNormally = true, and finalizes as success.

Fix

1. forwarder.ts — Signal error instead of normal close

- controller.close();
+ controller.error(err);

This causes reader.read() in ResponseHandler to reject with the original error.

2. errors.ts — Export isTransportError()

The function was already implemented but not exported. Now available for response-handler.ts.

3. response-handler.ts — Route transport errors correctly

Added isTransportError() check in the catch block so SocketError/ECONNRESET errors are routed to the STREAM_UPSTREAM_ABORTED path with proper:

  • Circuit breaker failure recording
  • 502 status code in internal bookkeeping
  • Error logging with error name/code/message

Relationship to #911

Aspect PR #911 This PR
Phase Connection/request phase Mid-stream (after data flowing)
Code path Error categorizer else-branch Stream error propagation → finalization
Symptom No retry/failover on SocketError Truncated output marked as success

These are independent bugs in different code paths.

Changes

  • src/app/v1/_lib/proxy/forwarder.ts: controller.close()controller.error(err) in nodeStream error handler (+4/-4)
  • src/app/v1/_lib/proxy/errors.ts: Export isTransportError() (+1/-1)
  • src/app/v1/_lib/proxy/response-handler.ts: Add isTransportError import and catch branch (+34/-1)

Testing

  • tsc --noEmit: ✅ Pass
  • biome check: ✅ Pass
  • The fix is defensive — only changes error propagation path, existing success paths are unchanged
  • Transport errors now correctly trigger STREAM_UPSTREAM_ABORTED instead of being silently swallowed

Fixes #916

Greptile Summary

This PR fixes a bug where upstream transport errors (SocketError, ECONNRESET) during active streaming were silently swallowed, causing truncated responses to be marked as successful. The fix has three parts: (1) forwarder.ts now propagates stream errors via controller.error(err) instead of controller.close(), (2) errors.ts exports the existing isTransportError() utility, and (3) response-handler.ts adds a new catch branch that routes transport errors to STREAM_UPSTREAM_ABORTED finalization with proper circuit breaker failure recording and 502 status codes.

  • The change is well-scoped and defensive — only the error propagation path is affected; normal success paths remain unchanged
  • The new isTransportError branch correctly mirrors the existing upstream abort handling logic, including fallback to persistRequestFailure if finalization itself fails
  • The JSDoc on nodeStreamToWebStreamSafe is now outdated and should be updated to reflect the new error-propagation behavior

Confidence Score: 4/5

  • This PR is safe to merge — it only changes the error propagation path and does not affect normal success flows.
  • The fix is well-motivated by a clear root cause analysis, the code changes are minimal and defensive, and the new transport error branch mirrors existing patterns in the codebase. The only concern is a stale JSDoc comment, which is non-blocking.
  • Pay attention to src/app/v1/_lib/proxy/forwarder.ts — the close event fires after error in Node.js, and while the try/catch handles this safely, the outdated JSDoc may confuse future maintainers.

Important Files Changed

Filename Overview
src/app/v1/_lib/proxy/forwarder.ts Changes controller.close() to controller.error(err) in the Node stream error handler, correctly propagating upstream errors to the downstream reader instead of silently swallowing them. The close event handler's try/catch safely handles the subsequent call. Outdated JSDoc on the method.
src/app/v1/_lib/proxy/errors.ts Adds export keyword to the existing isTransportError() function to make it available for response-handler.ts. No logic changes.
src/app/v1/_lib/proxy/response-handler.ts Adds an isTransportError catch branch that routes transport errors (SocketError, ECONNRESET) to STREAM_UPSTREAM_ABORTED finalization with proper circuit breaker accounting, mirroring the existing upstream abort handling path.

Sequence Diagram

sequenceDiagram
    participant US as Upstream Provider
    participant NS as Node Stream
    participant WS as Web ReadableStream (forwarder.ts)
    participant RH as ResponseHandler (reader)

    US->>NS: data chunks...
    NS->>WS: controller.enqueue(chunk)
    WS->>RH: reader.read() → {value, done:false}

    Note over US,NS: SocketError / ECONNRESET occurs

    rect rgb(255, 230, 230)
        Note right of NS: Before this PR
        NS->>WS: error event → controller.close()
        WS->>RH: reader.read() → {done:true}
        RH->>RH: streamEndedNormally = true
        Note over RH: Finalized as SUCCESS ❌
    end

    rect rgb(230, 255, 230)
        Note right of NS: After this PR
        NS->>WS: error event → controller.error(err)
        WS->>RH: reader.read() rejects with err
        RH->>RH: catch → isTransportError(err) = true
        RH->>RH: finalizeStream(STREAM_UPSTREAM_ABORTED)
        Note over RH: Finalized as FAILURE ✅
    end
Loading

Comments Outside Diff (1)

  1. src/app/v1/_lib/proxy/forwarder.ts, line 3966 (link)

    Outdated JSDoc after behavior change

    The comment says "吞掉上游流的错误事件,避免 'terminated' 错误冒泡到调用者" (swallow upstream stream errors), but this PR intentionally changes the behavior to propagate errors via controller.error(err). The JSDoc should be updated to reflect the new semantics.

Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/app/v1/_lib/proxy/forwarder.ts
Line: 3966

Comment:
**Outdated JSDoc after behavior change**

The comment says "吞掉上游流的错误事件,避免 'terminated' 错误冒泡到调用者" (swallow upstream stream errors), but this PR intentionally changes the behavior to **propagate** errors via `controller.error(err)`. The JSDoc should be updated to reflect the new semantics.

```suggestion
   * 关键特性:将上游流错误传播到下游 reader,确保 ResponseHandler 能正确检测到截断
```

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: b968b1d

(3/5) Reply to the agent's comments like "Can you suggest a fix for this @greptileai?" or ask follow-up questions!

…marked as success

When a SocketError occurs during active streaming, the nodeStream error
handler in forwarder.ts was calling controller.close() which signals a
normal end-of-stream. This caused ResponseHandler to set
streamEndedNormally=true, marking truncated responses as successful.

Changes:
- forwarder.ts: Use controller.error(err) instead of controller.close()
  in the nodeStream error handler, so downstream readers detect the error
- errors.ts: Export isTransportError() for use by response-handler
- response-handler.ts: Add isTransportError() check in the catch block
  to route SocketError/ECONNRESET to STREAM_UPSTREAM_ABORTED path with
  proper circuit breaker accounting and 502 status recording

Fixes ding113#916
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses a critical issue where upstream streaming errors, such as socket closures, were being silently swallowed and responses were incorrectly marked as successful, leading to truncated data for clients. The changes ensure that these transport errors are properly propagated and handled throughout the proxy, enabling accurate error reporting, logging, and system behavior, preventing misleading success states for partial responses.

Highlights

  • Error Propagation: Modified forwarder.ts to propagate upstream stream errors using controller.error(err) instead of controller.close(), ensuring downstream consumers receive error signals.
  • Error Utility Export: Exported the isTransportError() function from errors.ts to allow its use in other modules.
  • Robust Error Handling: Enhanced response-handler.ts to specifically detect and handle transport errors using the newly exported isTransportError(), leading to correct circuit breaker failure recording, 502 status codes, and detailed error logging.
Changelog
  • src/app/v1/_lib/proxy/errors.ts
    • Modified the isTransportError function to be exported.
  • src/app/v1/_lib/proxy/forwarder.ts
    • Changed the nodeStream.on("error") handler to call controller.error(err) instead of controller.close().
    • Updated the associated comment to reflect error propagation rather than graceful closure.
  • src/app/v1/_lib/proxy/response-handler.ts
    • Added isTransportError to the import statement from ./errors.
    • Introduced a new else if (isTransportError(err)) block within the catch logic to specifically handle transport errors.
    • Implemented logging, stream finalization, and request failure persistence with a 502 status code for transport errors.
Activity
  • No human activity has occurred on this pull request yet.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 14, 2026

📝 Walkthrough

Walkthrough

该PR修复了流式响应期间发生传输层错误(如SocketError)时的处理问题。通过导出传输错误判断函数、修改上游错误信号机制(从关闭改为错误传播),以及在响应处理器中添加对应的错误检测和清理逻辑,确保截断的流不会被误标记为成功。

Changes

Cohort / File(s) Summary
导出传输错误判断
src/app/v1/_lib/proxy/errors.ts
isTransportError函数从内部函数改为公共导出函数,无逻辑变更。
上游流错误信号
src/app/v1/_lib/proxy/forwarder.ts
修改ReadableStream错误处理器,将controller.close()改为controller.error(err)以向下游传播错误;更新注释说明错误的传播和处理行为。
下游错误检测和清理
src/app/v1/_lib/proxy/response-handler.ts
新增对传输层错误(isTransportError)的检测和处理分支,包含日志记录、内容刷新、流状态终结(STREAM_UPSTREAM_ABORTED)以及故障持久化(HTTP 502);同时导入isTransportError函数。

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed 标题准确反映了主要变更:修复上游流错误传播以防止被标记为成功的截断响应。
Linked Issues check ✅ Passed 代码变更完整实现了#916中的所有主要需求:使用controller.error()替换controller.close()、导出isTransportError()、在ResponseHandler中正确路由传输错误。
Out of Scope Changes check ✅ Passed 所有变更均直接针对#916的要求:forwarder.ts中的错误传播、errors.ts中的导出和response-handler.ts中的错误路由,没有无关的修改。
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Description check ✅ Passed PR 描述详细解释了问题、根本原因、修复方案及其关系,与代码变更密切相关。

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan
  • Generate coding plan for human review comments

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 and usage tips.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request effectively addresses a critical bug where upstream stream errors caused truncated responses to be marked as successful. The fix correctly propagates the error from the forwarder to the response-handler by using controller.error(err) instead of controller.close(). The new error handling logic in response-handler.ts for transport errors is robust and ensures that such failures are properly logged and recorded.

I have one suggestion in response-handler.ts to refactor some duplicated error handling logic into a helper function to improve maintainability. Overall, this is a solid fix that significantly improves the reliability of streaming responses.

Comment on lines +2391 to +2406
} catch (finalizeError) {
logger.error("ResponseHandler: Failed to finalize transport-error stream", {
taskId,
messageId: messageContext.id,
finalizeError,
});

await persistRequestFailure({
session,
messageContext,
statusCode: 502,
error: err,
taskId,
phase: "stream",
});
}
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.

medium

This catch block for handling finalizeStream errors is very similar to other error handling paths in this file (e.g., for isResponseTimeout and isIdleTimeout). To improve maintainability and reduce code duplication, consider extracting this error handling logic into a shared helper function. This would make the error handling more consistent and easier to manage across different stream failure scenarios.

@github-actions github-actions bot added bug Something isn't working area:core size/XS Extra Small PR (< 50 lines) labels Mar 14, 2026
Copy link
Copy Markdown
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Code Review Summary

This PR correctly fixes a critical bug where SocketError during active streaming caused truncated responses to be marked as successful. The fix properly propagates upstream stream errors to downstream readers using controller.error(err) instead of controller.close().

PR Size: XS

  • Lines changed: 45 (39 additions, 6 deletions)
  • Files changed: 3

Review Coverage

  • Logic and correctness - Clean
  • Security (OWASP Top 10) - Clean
  • Error handling - Clean (this PR fixes a silent failure bug)
  • Type safety - Clean
  • Documentation accuracy - Comments properly updated
  • Test coverage - Adequate (function already tested via categorizeError path)
  • Code clarity - Good

Technical Assessment

forwarder.ts: The change from controller.close() to controller.error(err) correctly signals an abnormal stream termination to downstream readers, allowing proper error detection.

response-handler.ts: The new isTransportError(err) branch correctly routes transport errors (SocketError, ECONNRESET, etc.) to the STREAM_UPSTREAM_ABORTED path with:

  • Proper error logging with error name/code/message
  • Circuit breaker failure recording via finalizeStream
  • 502 status in internal bookkeeping

errors.ts: Simple export addition to make isTransportError available for response-handler.

The error handling follows the existing pattern established for upstream abort handling, maintaining consistency with the codebase.


Automated review by Claude AI

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/app/v1/_lib/proxy/forwarder.ts (1)

3994-4033: ⚠️ Potential issue | 🟠 Major

不要把非 end"close" 当成成功结束。

"error" 改成 controller.error(err) 是对的,但这里的 "close" 处理仍然无条件 controller.close()。对 Node Readable 来说,"close" 不等于成功读到 EOF;如果底层流被提前销毁/中断而没有走到 "end",下游还是会拿到 done: true,截断响应仍可能被当成成功。建议只在已见到 "end" 时关闭,其余 "close" 按异常终止处理;如果希望继续落到 STREAM_UPSTREAM_ABORTED,这里合成的异常也要和 isTransportError() 的判定条件对齐。

建议修改
   private static nodeStreamToWebStreamSafe(
     nodeStream: Readable,
     providerId: number,
     providerName: string
   ): ReadableStream<Uint8Array> {
     let chunkCount = 0;
     let totalBytes = 0;
+    let ended = false;
+    let cancelled = false;

     return new ReadableStream<Uint8Array>({
       start(controller) {
         ...
         nodeStream.on("end", () => {
+          ended = true;
           logger.debug("ProxyForwarder: Node stream ended normally", {
             providerId,
             providerName,
             chunkCount,
             totalBytes,
           });
           try {
             controller.close();
           } catch {
             // 如果已关闭,忽略
           }
         });

         nodeStream.on("close", () => {
           logger.debug("ProxyForwarder: Node stream closed", {
             providerId,
             providerName,
             chunkCount,
             totalBytes,
           });
-          try {
-            controller.close();
-          } catch {
-            // 如果已关闭,忽略
-          }
+          if (!ended && !cancelled) {
+            const prematureCloseError = Object.assign(
+              new Error("Upstream stream closed before end"),
+              { code: "ECONNRESET" }
+            );
+            try {
+              controller.error(prematureCloseError);
+            } catch {
+              // 如果已关闭或已出错,忽略
+            }
+          }
         });

         nodeStream.on("error", (err) => {
           ...
         });
       },

       cancel(reason) {
+        cancelled = true;
         try {
           nodeStream.destroy(
             reason instanceof Error ? reason : reason ? new Error(String(reason)) : undefined
           );
In Node.js streams, can a Readable emit "close" without first emitting "end", and does "close" indicate normal EOF or premature termination? Please cite the Node.js stream documentation semantics.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/v1/_lib/proxy/forwarder.ts` around lines 3994 - 4033, The "close"
handler currently calls controller.close() unconditionally which is wrong
because Node Readable can emit "close" without "end"; change the nodeStream
handlers so that only the "end" event calls controller.close(), while the
"close" event should call controller.error(...) with a synthetic transport error
that aligns with isTransportError() and the STREAM_UPSTREAM_ABORTED sentinel (so
ResponseHandler treats it as an aborted upstream), and preserve the existing
"error" handler behavior that calls controller.error(err); update references in
the same block around nodeStream.on("end"), nodeStream.on("close"),
nodeStream.on("error"), controller.close, controller.error, isTransportError,
and STREAM_UPSTREAM_ABORTED accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/app/v1/_lib/proxy/forwarder.ts`:
- Line 4022: Remove the emoji from the inline comment in
src/app/v1/_lib/proxy/forwarder.ts — locate the comment containing "⭐
关键:将上游流错误传播到下游 reader,确保 ResponseHandler 能检测到截断" (near the
ResponseHandler/reader logic in forwarder.ts) and edit it to remove the "⭐"
character so it reads e.g. "关键:将上游流错误传播到下游 reader,确保 ResponseHandler 能检测到截断" to
comply with the repo rule banning emoji in code/comments.

---

Outside diff comments:
In `@src/app/v1/_lib/proxy/forwarder.ts`:
- Around line 3994-4033: The "close" handler currently calls controller.close()
unconditionally which is wrong because Node Readable can emit "close" without
"end"; change the nodeStream handlers so that only the "end" event calls
controller.close(), while the "close" event should call controller.error(...)
with a synthetic transport error that aligns with isTransportError() and the
STREAM_UPSTREAM_ABORTED sentinel (so ResponseHandler treats it as an aborted
upstream), and preserve the existing "error" handler behavior that calls
controller.error(err); update references in the same block around
nodeStream.on("end"), nodeStream.on("close"), nodeStream.on("error"),
controller.close, controller.error, isTransportError, and
STREAM_UPSTREAM_ABORTED accordingly.
🪄 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: b1d5af14-0530-4135-a2a3-051812f4047b

📥 Commits

Reviewing files that changed from the base of the PR and between 8c15634 and b968b1d.

📒 Files selected for processing (3)
  • src/app/v1/_lib/proxy/errors.ts
  • src/app/v1/_lib/proxy/forwarder.ts
  • src/app/v1/_lib/proxy/response-handler.ts

});

// ⭐ 关键:吞掉错误事件,避免 "terminated" 冒泡
// ⭐ 关键:将上游流错误传播到下游 reader,确保 ResponseHandler 能检测到截断
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

移除这条注释里的 emoji。

违反了当前仓库对 TS/JS 注释的约束,建议直接删掉,避免后续同类注释继续扩散。

As per coding guidelines **/*.{js,ts,tsx,jsx}: Never use emoji characters in any code, comments, or string literals.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/v1/_lib/proxy/forwarder.ts` at line 4022, Remove the emoji from the
inline comment in src/app/v1/_lib/proxy/forwarder.ts — locate the comment
containing "⭐ 关键:将上游流错误传播到下游 reader,确保 ResponseHandler 能检测到截断" (near the
ResponseHandler/reader logic in forwarder.ts) and edit it to remove the "⭐"
character so it reads e.g. "关键:将上游流错误传播到下游 reader,确保 ResponseHandler 能检测到截断" to
comply with the repo rule banning emoji in code/comments.

@ding113 ding113 merged commit 329bb14 into ding113:dev Mar 15, 2026
8 checks passed
@github-project-automation github-project-automation bot moved this from Backlog to Done in Claude Code Hub Roadmap Mar 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:core bug Something isn't working size/XS Extra Small PR (< 50 lines)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants