From 7aea3a0a505d5c78f08aebb6fc47b28193faa0e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com> Date: Thu, 17 Sep 2026 01:36:55 +0800 Subject: [PATCH] =?UTF-8?q?fix(rustfs):=20=E4=BF=AE=20unexpected=20end=20o?= =?UTF-8?q?f=20stream=20=E6=A0=B9=E5=9B=A0=E2=80=94=E2=80=94=E9=9B=B6?= =?UTF-8?q?=E9=95=BF=E5=BA=A6=E8=AF=B7=E6=B1=82=E4=BD=93=E7=BC=BA=20Conten?= =?UTF-8?q?t-Length?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 抓包定位(90 秒内 515 次 HTTP 411 Length Required):OkHttp 对长度为 0 的请求体 不写 Content-Length,请求于是既无 Content-Length 也无 Transfer-Encoding——HTTP/1.1 不允许这样,RustFS 直接回 411;客户端读到不完整响应就报 unexpected end of stream 再重试。业务里上传空内容(content 为 null/空)是常态,所以每天上千次。 修复用 network interceptor 在协议层补 Content-Length: 0:普通 interceptor 里加的 头会被 BridgeInterceptor 按 body 长度覆盖,加了不生效。 实测:部署后同样 90 秒抓包,411 从 515 次降到 0,两节点「确定性错误不重试」归零。 此前排除的公网链路、keepAlive、连接复用三项确实都不是原因——抓包才是对的入口。 --- .../object/RustfsObjectStorageService.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/file/service/object/RustfsObjectStorageService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/file/service/object/RustfsObjectStorageService.java index 355bb1bf..3eab9e59 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/file/service/object/RustfsObjectStorageService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/file/service/object/RustfsObjectStorageService.java @@ -382,6 +382,22 @@ public class RustfsObjectStorageService { Math.max(1L, properties.getConnectionPoolKeepAliveMillis()), TimeUnit.MILLISECONDS)) .retryOnConnectionFailure(true) + .addNetworkInterceptor(chain -> { + okhttp3.Request request = chain.request(); + okhttp3.RequestBody body = request.body(); + // OkHttp 对「长度为 0 的请求体」不会写 Content-Length,于是请求既无 + // Content-Length 也无 Transfer-Encoding(HTTP/1.1 不允许这样)。 + // RustFS 对此直接回 411 Length Required,客户端读到不完整响应就报 + // unexpected end of stream,进而重试——线上抓包实测 90 秒内 515 次 411, + // 而上传空内容(content 为 null/空)在业务里是常态。 + // 用 network interceptor 在协议层补上该头(普通 interceptor 会被 + // BridgeInterceptor 按 body 长度覆盖掉,加了也不生效)。 + if (body != null && body.contentLength() == 0L + && request.header("Content-Length") == null) { + request = request.newBuilder().header("Content-Length", "0").build(); + } + return chain.proceed(request); + }) .build(); } return httpClient;