Skip to content

修复gateway提前关闭通道导致gateway返回响应被截断问题#19

Open
Wanxp wants to merge 3 commits into
continew-org:devfrom
Wanxp:dev
Open

修复gateway提前关闭通道导致gateway返回响应被截断问题#19
Wanxp wants to merge 3 commits into
continew-org:devfrom
Wanxp:dev

Conversation

@Wanxp
Copy link
Copy Markdown

@Wanxp Wanxp commented Mar 13, 2026

PR 类型

  • 新 feature
  • Bug 修复
  • 功能增强
  • 文档变更
  • 代码样式变更
  • 重构
  • 性能改进
  • 单元测试
  • CI/CD
  • 其他

PR 目的

修复gateway提前关闭通道导致gateway返回响应被截断问题。

解决方案

spring-gateway webflux 模式请求 servlet类型服务时,当响应报文内容太长时,gateway提前关闭通道导致gateway返回响应被截断问题。
修改修改 RepeatReadResponseWrapper 的实现,使其在写入时同时写入缓存和原始响应流。

PR 测试

线上生产引入 continew-starter-log-interceptor 导致网关(spring-gateway )请求服务(微服务的服务)时,网关返回的响应体是被截断的(前提是响应体足够大才会触发),经过此次修复之后,线上功能正常且高并发亦能打。

Changelog

模块 Changelog Related issues
continew-starter-core 修改 RepeatReadResponseWrapper 的实现,使其在写入时同时写入缓存和原始响应流

其他信息

提交前确认

  • PR 代码经过了完整测试,并且通过了代码规范检查
  • 已经完整填写 Changelog,并链接到了相关 issues
  • PR 代码将要提交到 dev 分支

Summary by CodeRabbit

发布说明

  • 优化改进
    • 优化了响应内容处理的内存管理机制,提高了数据传输效率,同时保持了现有的公共接口不变。

Wanxp added 3 commits January 9, 2026 17:06
问题: spring-gateway webflux 模式请求 servlet类型服务时,当响应报文内容太长时,gateway提前关闭通道导致gateway返回响应被截断问题。
修复方式: 修改 RepeatReadResponseWrapper 的实现,使其在写入时同时写入缓存和原始响应流
@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 13, 2026

📝 Walkthrough

概览

该改动重构了 RepeatReadResponseWrapper 类中的响应体缓存机制。通过直接将写操作同步镜像至原始流,消除了之前的分离式内存副本步骤,改进了数据流向的处理逻辑。公共方法签名保持不变。

变更

内聚组 / 文件 变更摘要
响应流缓存重构
continew-starter-core/src/main/java/top/continew/starter/core/wrapper/RepeatReadResponseWrapper.java
writer 字段改为懒初始化的 cachedWriter;在 getOutputStream()getWriter() 中实现写操作实时镜像至原始流;getResponseContent() 刷新缓存后返回内容;copyBodyToResponse() 改为无操作,因为数据已同步写入原始响应。

估算代码审查工作量

🎯 2 (简单) | ⏱️ ~10 分钟

🐰✨ 一只兔子的庆贺

缓存不再分家住,
流水实时共舞动,
镜像轻巧无负担,
响应轻盈舞翩翩。

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 标题清晰准确地总结了主要变更:修复网关提前关闭通道导致响应被截断的问题,与代码改动(优化RepeatReadResponseWrapper实现以同时写入缓存和原始响应流)高度相关。

✏️ 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.

Tip

CodeRabbit can generate a title for your PR based on the changes.

Add @coderabbitai placeholder anywhere in the title of your PR and CodeRabbit will replace it with a title based on the changes in the PR. You can change the placeholder by changing the reviews.auto_title_placeholder setting.

@Wanxp Wanxp changed the title 修复gateway提前关闭通道导致gateway返回响应被截断问 修复gateway提前关闭通道导致gateway返回响应被截断问题 Mar 13, 2026
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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@continew-starter-core/src/main/java/top/continew/starter/core/wrapper/RepeatReadResponseWrapper.java`:
- Around line 141-143: The empty copyBodyToResponse() in
RepeatReadResponseWrapper removes the final flush step and can leave PrintWriter
buffers unwritten; restore a finalization step that flushes the wrapped response
output streams/writers (e.g., call flush on the stored ServletOutputStream
and/or PrintWriter used by RepeatReadResponseWrapper) and handle IOExceptions as
declared so LogFilter's post-filter cleanup actually forces buffered data to be
sent; locate the copyBodyToResponse() method in class RepeatReadResponseWrapper
and add safe flush() calls on the internal output/writer objects (and optionally
a close if appropriate).
- Around line 96-109: The getWriter() implementation in
RepeatReadResponseWrapper incorrectly treats raw bytes as characters and
hardcodes UTF-8 when caching, causing encoding corruption; change getWriter() to
return a character-level tee Writer that writes characters to the original
PrintWriter AND simultaneously encodes those characters into bytes using the
response encoding from getCharacterEncoding() before writing into
cachedOutputStream (do not call originalWriter.write(int) with raw bytes or
decode byte[] with UTF-8); likewise update getResponseContent() to decode
cachedOutputStream using the same getCharacterEncoding() instead of fixed UTF-8;
reference RepeatReadResponseWrapper, getWriter(), cachedWriter,
cachedOutputStream, originalWriter and getResponseContent() when making these
changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 54b618d3-6708-4a6d-8a5c-ddb24ea4fd79

📥 Commits

Reviewing files that changed from the base of the PR and between 0ec793e and 66ce9ae.

📒 Files selected for processing (1)
  • continew-starter-core/src/main/java/top/continew/starter/core/wrapper/RepeatReadResponseWrapper.java

Comment on lines +96 to +109
if (cachedWriter == null) {
PrintWriter originalWriter = super.getWriter();
cachedWriter = new PrintWriter(new java.io.OutputStream() {
@Override
public void write(int b) throws IOException {
cachedOutputStream.write(b);
originalWriter.write(b);
}

@Override
public void write(byte[] b, int off, int len) throws IOException {
cachedOutputStream.write(b, off, len);
originalWriter.write(new String(b, off, len, StandardCharsets.UTF_8));
}
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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 验证当前实现是否存在固定 UTF-8 解码和 write(int) 字符语义写入
rg -n --type=java -C2 'originalWriter\.write\(b\)|new String\(b, off, len, StandardCharsets\.UTF_8\)|getCharacterEncoding\('

Repository: continew-org/continew-starter

Length of output: 5706


🏁 Script executed:

#!/bin/bash
# Read the complete RepeatReadResponseWrapper file to understand the full context
cat -n continew-starter-core/src/main/java/top/continew/starter/core/wrapper/RepeatReadResponseWrapper.java | head -150

Repository: continew-org/continew-starter

Length of output: 5733


🏁 Script executed:

#!/bin/bash
# Check if getCharacterEncoding() method exists in the response wrapper
rg -n "getCharacterEncoding|getContentType|charset" continew-starter-core/src/main/java/top/continew/starter/core/wrapper/RepeatReadResponseWrapper.java

Repository: continew-org/continew-starter

Length of output: 117


🏁 Script executed:

#!/bin/bash
# Check the class declaration and what it extends
rg -n "class RepeatReadResponseWrapper|extends|implements" continew-starter-core/src/main/java/top/continew/starter/core/wrapper/RepeatReadResponseWrapper.java | head -20

Repository: continew-org/continew-starter

Length of output: 151


🌐 Web query:

PrintWriter.write(int) behavior character encoding semantics

💡 Result:

PrintWriter.write(int c) does not write the decimal text of the integer. It writes one 16‑bit character (a single Java char / UTF‑16 code unit). In terms of Writer.write(int), only the low 16 bits of the int are used and the high 16 bits are ignored. [1]

Character encoding happens elsewhere: PrintWriter is a character stream. If it ultimately targets a byte sink (e.g., a file or socket), then an OutputStreamWriter (often created under the hood by certain PrintWriter constructors) performs the charset encoding step, i.e., converts characters to bytes using the specified (or default) charset. [2][3]

So, the effective behavior is:

  • pw.write(65) writes the character '\u0041' ('A'), not the string "65". [1]
  • The bytes produced depend on the charset of the underlying OutputStreamWriter (explicitly provided, or the platform/default used by convenience constructors). [2][3]

Sources:
[1] Oracle JDK Writer.write(int) spec (low 16 bits) https://docs.oracle.com/en/java/javase/16/docs/api/java.base/java/io/Writer.html
[2] Oracle JDK OutputStreamWriter (chars encoded to bytes via charset) https://docs.oracle.com/en/java/javase/16/docs/api/java.base/java/io/OutputStreamWriter.html
[3] Oracle JDK PrintWriter docs (constructors may create OutputStreamWriter using default/specified charset) https://docs.oracle.com/javase/10/docs/api/java/io/PrintWriter.html


🏁 Script executed:

#!/bin/bash
# Verify if HttpServletResponseWrapper provides getCharacterEncoding() access
# Check if the parent response object can be accessed through getResponse()
rg -n "getResponse|extends HttpServletResponseWrapper" continew-starter-core/src/main/java/top/continew/starter/core/wrapper/RepeatReadResponseWrapper.java

Repository: continew-org/continew-starter

Length of output: 196


🏁 Script executed:

#!/bin/bash
# Check how other similar response wrappers in the codebase handle encoding
fd -e java "ResponseWrapper|Response" continew-starter-core/src/main/java/top/continew/starter/core/wrapper/ | head -10

Repository: continew-org/continew-starter

Length of output: 173


🏁 Script executed:

#!/bin/bash
# Check for test files related to RepeatReadResponseWrapper
fd -type f ".*RepeatReadResponseWrapper.*Test" continew-starter-core/

Repository: continew-org/continew-starter

Length of output: 243


🏁 Script executed:

#!/bin/bash
# Verify getCharacterEncoding() is accessible through HttpServletResponseWrapper
# Check Jakarta/Spring documentation via looking at how it's used in request wrapper
rg -n "getCharacterEncoding()" continew-starter-core/src/main/java/top/continew/starter/core/wrapper/RepeatReadRequestWrapper.java -A3

Repository: continew-org/continew-starter

Length of output: 777


🏁 Script executed:

#!/bin/bash
# Check what getResponseContent() actually returns and how it's used
rg -n "getResponseContent|ResponseWrapper" continew-starter-core/src/main/java/top/continew/starter/core/ -l | head -10

Repository: continew-org/continew-starter

Length of output: 173


🏁 Script executed:

#!/bin/bash
# Check if getCharacterEncoding() is accessible in RepeatReadResponseWrapper through inheritance
# HttpServletResponseWrapper inherits from ServletResponseWrapper which has getCharacterEncoding()
rg -n "class.*Wrapper extends" continew-starter-core/src/main/java/top/continew/starter/core/wrapper/ -A2

Repository: continew-org/continew-starter

Length of output: 946


🏁 Script executed:

#!/bin/bash
# Look for actual usage of RepeatReadResponseWrapper to see if charset issues manifest
rg -n "RepeatReadResponseWrapper|getResponseContent" continew-starter-core/src/main/java/top/continew/starter/core/ -l --type=java

Repository: continew-org/continew-starter

Length of output: 173


🏁 Script executed:

#!/bin/bash
# Check if there are integration tests that might use non-UTF-8 encodings
find continew-starter-core -name "*Test*.java" -type f | head -5 | xargs grep -l "ResponseWrapper\|charset\|encoding" 2>/dev/null

Repository: continew-org/continew-starter

Length of output: 57


getWriter() 的字节到字符串转换存在编码与字符语义错误,会导致非 UTF-8 编码场景下数据损坏。

问题确认:

  • 第 102 行 originalWriter.write(b) 将字节值错误地作为 Unicode 码点传入。PrintWriter 的 write(int) 期望字符,但 b 是来自编码流的字节(0-255),导致非 ASCII 字节产生乱码。
  • 第 108 行硬编码 UTF-8 解码是根本性错误。该字节来源于 PrintWriter 的内部编码(使用响应的字符编码),若响应使用 GBK、ISO-8859-1 等其他编码,此处 UTF-8 解码会破坏数据。
  • 第 130 行 getResponseContent() 同样硬编码 UTF-8,必须与响应编码一致。

应改为字符级别的 Tee 写入器,调用 getCharacterEncoding() 获取响应编码,并在缓存字节流时使用正确的编码。

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

In
`@continew-starter-core/src/main/java/top/continew/starter/core/wrapper/RepeatReadResponseWrapper.java`
around lines 96 - 109, The getWriter() implementation in
RepeatReadResponseWrapper incorrectly treats raw bytes as characters and
hardcodes UTF-8 when caching, causing encoding corruption; change getWriter() to
return a character-level tee Writer that writes characters to the original
PrintWriter AND simultaneously encodes those characters into bytes using the
response encoding from getCharacterEncoding() before writing into
cachedOutputStream (do not call originalWriter.write(int) with raw bytes or
decode byte[] with UTF-8); likewise update getResponseContent() to decode
cachedOutputStream using the same getCharacterEncoding() instead of fixed UTF-8;
reference RepeatReadResponseWrapper, getWriter(), cachedWriter,
cachedOutputStream, originalWriter and getResponseContent() when making these
changes.

Comment on lines 141 to 143
public void copyBodyToResponse() throws IOException {
if (!isStreamingResponse && cachedOutputStream.size() > 0) {
getResponse().getOutputStream().write(cachedOutputStream.toByteArray());
}
// 不再需要复制,因为数据已经在写入时同步到原始响应流
}
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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 验证 copyBodyToResponse 的调用点与当前空实现
rg -n --type=java -C3 'copyBodyToResponse\s*\('
rg -n --type=java -C3 'void copyBodyToResponse\(\) throws IOException'

Repository: continew-org/continew-starter

Length of output: 2770


🏁 Script executed:

cat -n continew-starter-core/src/main/java/top/continew/starter/core/wrapper/RepeatReadResponseWrapper.java

Repository: continew-org/continew-starter

Length of output: 5798


应在 copyBodyToResponse() 中保留必要的 flush 操作。

虽然实现了实时双写策略,但方法已被完全置空,导致丢失最终刷新语义。当使用 PrintWriter.write() 写入(而非 println())时,缓冲区可能未被显式刷新。LogFilter(Line 69)在 doFilter() 后仍调用此方法,这是明确的清理/最终化检查点。建议补充 flush 逻辑以确保缓冲数据被完全下发。

建议修复
 public void copyBodyToResponse() throws IOException {
-    // 不再需要复制,因为数据已经在写入时同步到原始响应流
+    if (isStreamingResponse) {
+        return;
+    }
+    if (cachedWriter != null) {
+        cachedWriter.flush();
+    }
+    super.flushBuffer();
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@continew-starter-core/src/main/java/top/continew/starter/core/wrapper/RepeatReadResponseWrapper.java`
around lines 141 - 143, The empty copyBodyToResponse() in
RepeatReadResponseWrapper removes the final flush step and can leave PrintWriter
buffers unwritten; restore a finalization step that flushes the wrapped response
output streams/writers (e.g., call flush on the stored ServletOutputStream
and/or PrintWriter used by RepeatReadResponseWrapper) and handle IOExceptions as
declared so LogFilter's post-filter cleanup actually forces buffered data to be
sent; locate the copyBodyToResponse() method in class RepeatReadResponseWrapper
and add safe flush() calls on the internal output/writer objects (and optionally
a close if appropriate).

@Wanxp
Copy link
Copy Markdown
Author

Wanxp commented Mar 18, 2026

现在AI这么强大吗,连修复bug的bug都能检测到

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