修复gateway提前关闭通道导致gateway返回响应被截断问题#19
Conversation
问题: spring-gateway webflux 模式请求 servlet类型服务时,当响应报文内容太长时,gateway提前关闭通道导致gateway返回响应被截断问题。 修复方式: 修改 RepeatReadResponseWrapper 的实现,使其在写入时同时写入缓存和原始响应流
|
|
📝 Walkthrough概览该改动重构了 变更
估算代码审查工作量🎯 2 (简单) | ⏱️ ~10 分钟 诗
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
📝 Coding Plan
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 Tip CodeRabbit can generate a title for your PR based on the changes.Add |
There was a problem hiding this comment.
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
📒 Files selected for processing (1)
continew-starter-core/src/main/java/top/continew/starter/core/wrapper/RepeatReadResponseWrapper.java
| 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)); | ||
| } |
There was a problem hiding this comment.
🧩 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 -150Repository: 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.javaRepository: 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 -20Repository: 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.javaRepository: 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 -10Repository: 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 -A3Repository: 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 -10Repository: 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/ -A2Repository: 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=javaRepository: 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/nullRepository: 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.
| public void copyBodyToResponse() throws IOException { | ||
| if (!isStreamingResponse && cachedOutputStream.size() > 0) { | ||
| getResponse().getOutputStream().write(cachedOutputStream.toByteArray()); | ||
| } | ||
| // 不再需要复制,因为数据已经在写入时同步到原始响应流 | ||
| } |
There was a problem hiding this comment.
🧩 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.javaRepository: 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).
|
现在AI这么强大吗,连修复bug的bug都能检测到 |
PR 类型
PR 目的
修复gateway提前关闭通道导致gateway返回响应被截断问题。
解决方案
spring-gateway webflux 模式请求 servlet类型服务时,当响应报文内容太长时,gateway提前关闭通道导致gateway返回响应被截断问题。
修改修改 RepeatReadResponseWrapper 的实现,使其在写入时同时写入缓存和原始响应流。
PR 测试
线上生产引入
continew-starter-log-interceptor导致网关(spring-gateway )请求服务(微服务的服务)时,网关返回的响应体是被截断的(前提是响应体足够大才会触发),经过此次修复之后,线上功能正常且高并发亦能打。Changelog
其他信息
提交前确认
Summary by CodeRabbit
发布说明