From 094786bae6bced6e2d74f3c40bfa5e6d1e865543 Mon Sep 17 00:00:00 2001 From: dyrnq Date: Fri, 18 Sep 2026 18:59:00 +0800 Subject: [PATCH] =?UTF-8?q?=E5=93=8D=E5=BA=94=E5=A4=B4=E5=86=92=E5=8F=B7?= =?UTF-8?q?=E5=90=8E=E8=A1=A5=E7=A9=BA=E6=A0=BC=EF=BC=8C=E7=AC=A6=E5=90=88?= =?UTF-8?q?=20RFC=209110=20field-name=20":"=20OWS=20field-value=20?= =?UTF-8?q?=E6=83=AF=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue: https://gitee.com/smartboot/feat/issues/IKGY2G 服务端与客户端写头时冒号后均未输出空格,产出 Content-Type:text/plain 这类形式。RFC 允许 OWS 为零,但 Java 生态多数客户端/网关按 ": " 切分, 会解析失败。线上表现为所有头里只有 Transfer-Encoding 是合规的,因为 它是 HttpOutputStream 里唯一一个手写完整的字面量。 改动点: - server/impl/HttpOutputStream - TEXT_PLAIN_FAST_WRITE / APPLICATION_JSON 两个快速路径常量的 ":feat/"、"Date:"、"Content-Type:"、"Content-Length:" 后补空格 - writeHeaders() 通用头循环冒号后补空格 - 静态块 arraycopy 偏移 DATE_INDEX + 5 -> + 6 - writeCommonHeadPart() 从常量切片写入的固定长度同步 +1 (34->35、15->16、17->18),并加注释说明这组索引与常量文本耦合 - common/io/FeatOutputStream.close() trailer 分支补空格 - client/impl/AbstractOutputStream 三处(Content-Type、Content-Length、 通用头循环)补空格 验证:裸 socket 抓原始字节,服务端 7 条路径(text/plain、application/json、 text/html、自定义头、chunked、common-chunked、trailer)+ 客户端 2 条请求, 补丁前 26 处缺空格、补丁后 0 处。 --- .../core/client/impl/AbstractOutputStream.java | 3 +++ .../feat/core/common/io/FeatOutputStream.java | 2 +- .../feat/core/server/impl/HttpOutputStream.java | 17 ++++++++++------- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/feat-core/src/main/java/tech/smartboot/feat/core/client/impl/AbstractOutputStream.java b/feat-core/src/main/java/tech/smartboot/feat/core/client/impl/AbstractOutputStream.java index e9b05ec7..ca533e13 100644 --- a/feat-core/src/main/java/tech/smartboot/feat/core/client/impl/AbstractOutputStream.java +++ b/feat-core/src/main/java/tech/smartboot/feat/core/client/impl/AbstractOutputStream.java @@ -55,6 +55,7 @@ abstract class AbstractOutputStream extends FeatOutputStream { if (request.getContentType() != null) { writeString(HeaderName.CONTENT_TYPE.getName()); writeBuffer.writeByte((byte) ':'); + writeBuffer.writeByte((byte) ' '); writeBuffer.write(getBytes(String.valueOf(request.getContentType()))); writeBuffer.write(FeatUtils.CRLF_BYTES); } @@ -62,6 +63,7 @@ abstract class AbstractOutputStream extends FeatOutputStream { if (request.getContentLength() >= 0) { writeString(HeaderName.CONTENT_LENGTH.getName()); writeBuffer.writeByte((byte) ':'); + writeBuffer.writeByte((byte) ' '); writeBuffer.write(getBytes(String.valueOf(request.getContentLength()))); writeBuffer.write(FeatUtils.CRLF_BYTES); } else if (chunkedSupport) { @@ -75,6 +77,7 @@ abstract class AbstractOutputStream extends FeatOutputStream { while (headerValue != null) { writeString(entry.getKey()); writeBuffer.writeByte((byte) ':'); + writeBuffer.writeByte((byte) ' '); writeString(headerValue.getValue()); writeBuffer.write(FeatUtils.CRLF_BYTES); headerValue = headerValue.getNextValue(); diff --git a/feat-core/src/main/java/tech/smartboot/feat/core/common/io/FeatOutputStream.java b/feat-core/src/main/java/tech/smartboot/feat/core/common/io/FeatOutputStream.java index 3d86912a..9fb877ba 100644 --- a/feat-core/src/main/java/tech/smartboot/feat/core/common/io/FeatOutputStream.java +++ b/feat-core/src/main/java/tech/smartboot/feat/core/common/io/FeatOutputStream.java @@ -149,7 +149,7 @@ public abstract class FeatOutputStream extends OutputStream implements Reset { writeBuffer.write("0\r\n".getBytes()); Map map = trailerSupplier.get(); for (String key : map.keySet()) { - writeBuffer.write((key + ":" + map.get(key) + "\r\n").getBytes()); + writeBuffer.write((key + ": " + map.get(key) + "\r\n").getBytes()); } writeBuffer.write(FeatUtils.CRLF_BYTES); } else { diff --git a/feat-core/src/main/java/tech/smartboot/feat/core/server/impl/HttpOutputStream.java b/feat-core/src/main/java/tech/smartboot/feat/core/server/impl/HttpOutputStream.java index 999a1e32..bd02af5c 100644 --- a/feat-core/src/main/java/tech/smartboot/feat/core/server/impl/HttpOutputStream.java +++ b/feat-core/src/main/java/tech/smartboot/feat/core/server/impl/HttpOutputStream.java @@ -29,7 +29,9 @@ import java.util.concurrent.TimeUnit; * @version v1.0.0 */ final class HttpOutputStream extends FeatOutputStream { - private static final String TEXT_PLAIN_FAST_WRITE = HttpProtocol.HTTP_11.getProtocol() + " 200 OK\r\n" + HeaderName.SERVER.getName() + ":feat/" + Feat.VERSION + "\r\nDate:" + FeatUtils.formatRFC1123(FeatUtils.currentTime()) + "\r\nContent-Type:" + HeaderValue.ContentType.TEXT_PLAIN_UTF8 + "\r\nContent-Length:"; + private static final String TEXT_PLAIN_FAST_WRITE = HttpProtocol.HTTP_11.getProtocol() + " 200 OK\r\n" + HeaderName.SERVER.getName() + ": feat/" + Feat.VERSION + "\r\nDate: " + FeatUtils.formatRFC1123(FeatUtils.currentTime()) + "\r\nContent-Type: " + HeaderValue.ContentType.TEXT_PLAIN_UTF8 + "\r\nContent-Length: "; + // 以下索引/长度全部从 TEXT_PLAIN_FAST_WRITE 的文本中推导,改动上面常量里的 + // 头名前缀(例如冒号后补空格)时必须同步核对。 private static final int SERVER_INDEX = TEXT_PLAIN_FAST_WRITE.indexOf(HeaderName.SERVER.getName()); private static final int DATE_INDEX = TEXT_PLAIN_FAST_WRITE.indexOf("Date:"); private static final int SERVER_INDEX_LENGTH = TEXT_PLAIN_FAST_WRITE.indexOf(HeaderName.DATE.getName()) - SERVER_INDEX; @@ -37,7 +39,7 @@ final class HttpOutputStream extends FeatOutputStream { private static final int PLAIN_CONTENT_LENGTH_INDEX = TEXT_PLAIN_FAST_WRITE.indexOf(HeaderName.CONTENT_LENGTH.getName()) - 2; private static final byte[] TEXT_PLAIN_FAST_WRITE_BYTES = TEXT_PLAIN_FAST_WRITE.getBytes(); - private static final String APPLICATION_JSON = HttpProtocol.HTTP_11.getProtocol() + " 200 OK\r\n" + HeaderName.SERVER.getName() + ":feat/" + Feat.VERSION + "\r\nDate:" + FeatUtils.formatRFC1123(FeatUtils.currentTime()) + "\r\nContent-Type:" + HeaderValue.ContentType.APPLICATION_JSON + "\r\nContent-Length:"; + private static final String APPLICATION_JSON = HttpProtocol.HTTP_11.getProtocol() + " 200 OK\r\n" + HeaderName.SERVER.getName() + ": feat/" + Feat.VERSION + "\r\nDate: " + FeatUtils.formatRFC1123(FeatUtils.currentTime()) + "\r\nContent-Type: " + HeaderValue.ContentType.APPLICATION_JSON + "\r\nContent-Length: "; private static final int JSON_CONTENT_LENGTH_INDEX = APPLICATION_JSON.indexOf(HeaderName.CONTENT_LENGTH.getName()) - 2; private static final byte[] APPLICATION_JSON_FAST_WRITE_BYTES = APPLICATION_JSON.getBytes(); private static final byte[] CHUNKED = "\r\nTransfer-Encoding: chunked\r\n\r\n".getBytes(); @@ -48,8 +50,8 @@ final class HttpOutputStream extends FeatOutputStream { HashedWheelTimer.DEFAULT_TIMER.scheduleWithFixedDelay(() -> { byte[] bytes = FeatUtils.formatRFC1123(FeatUtils.currentTime()).getBytes(); - System.arraycopy(bytes, 0, TEXT_PLAIN_FAST_WRITE_BYTES, DATE_INDEX + 5, bytes.length); - System.arraycopy(bytes, 0, APPLICATION_JSON_FAST_WRITE_BYTES, DATE_INDEX + 5, bytes.length); + System.arraycopy(bytes, 0, TEXT_PLAIN_FAST_WRITE_BYTES, DATE_INDEX + 6, bytes.length); + System.arraycopy(bytes, 0, APPLICATION_JSON_FAST_WRITE_BYTES, DATE_INDEX + 6, bytes.length); }, 800, TimeUnit.MILLISECONDS); } @@ -96,6 +98,7 @@ final class HttpOutputStream extends FeatOutputStream { while (headerValue != null) { writeString(entry.getKey()); writeBuffer.writeByte((byte) ':'); + writeBuffer.writeByte((byte) ' '); writeString(headerValue.getValue()); writeBuffer.write(FeatUtils.CRLF_BYTES); headerValue = headerValue.getNextValue(); @@ -129,15 +132,15 @@ final class HttpOutputStream extends FeatOutputStream { writeBuffer.write(TEXT_PLAIN_FAST_WRITE_BYTES, SERVER_INDEX, SERVER_INDEX_LENGTH); } // Date - writeBuffer.write(TEXT_PLAIN_FAST_WRITE_BYTES, DATE_INDEX, 34); + writeBuffer.write(TEXT_PLAIN_FAST_WRITE_BYTES, DATE_INDEX, 35); if (contentType != null) { - writeBuffer.write(TEXT_PLAIN_FAST_WRITE_BYTES, PLAIN_CONTENT_TYPE_INDEX, 15); + writeBuffer.write(TEXT_PLAIN_FAST_WRITE_BYTES, PLAIN_CONTENT_TYPE_INDEX, 16); writeString(contentType); } if (contentLength >= 0) { - writeBuffer.write(TEXT_PLAIN_FAST_WRITE_BYTES, PLAIN_CONTENT_LENGTH_INDEX, 17); + writeBuffer.write(TEXT_PLAIN_FAST_WRITE_BYTES, PLAIN_CONTENT_LENGTH_INDEX, 18); writeLongString(contentLength); if (hasHeader) { writeBuffer.write(FeatUtils.CRLF_BYTES); -- Gitee