feat: stream native request bodies
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
- native 请求体改为流式读取,Content-Length 与 chunked body 不再在进入 Handler 前整体复制到内存;保留大小限制、读超时、trailer 校验和 Keep-Alive 排空。
|
||||||
- 统一标准库与 native 的真实 TCP benchmark,增加固定并发档位、响应状态与读写错误检查。
|
- 统一标准库与 native 的真实 TCP benchmark,增加固定并发档位、响应状态与读写错误检查。
|
||||||
- 新增独立进程并发与长连接内存测量工具,记录成功吞吐、P95/P99、服务端分配、空闲连接堆内存和回收情况;修正并行 `ns/op` 与客户端/服务端混合分配的解释。
|
- 新增独立进程并发与长连接内存测量工具,记录成功吞吐、P95/P99、服务端分配、空闲连接堆内存和回收情况;修正并行 `ns/op` 与客户端/服务端混合分配的解释。
|
||||||
- 加固 native HTTP/1.1 解析边界:校验 Host、重复 Content-Length、Connection token、Transfer-Encoding、控制字符和响应头名称;补充 HTTP/1.0 Keep-Alive、HEAD、204 以及异常请求测试。
|
- 加固 native HTTP/1.1 解析边界:校验 Host、重复 Content-Length、Connection token、Transfer-Encoding、控制字符和响应头名称;补充 HTTP/1.0 Keep-Alive、HEAD、204 以及异常请求测试。
|
||||||
@@ -10,13 +11,13 @@
|
|||||||
- native 在长 Handler 运行期间监测客户端断开并取消请求 context,流式写失败同步取消;短请求延迟启动监测,管线数据保留给下一次请求。补充断开、监测退出和长连接复用验收。
|
- native 在长 Handler 运行期间监测客户端断开并取消请求 context,流式写失败同步取消;短请求延迟启动监测,管线数据保留给下一次请求。补充断开、监测退出和长连接复用验收。
|
||||||
- native 增加 `ServeTLS` / `RunTLS` 和 `TLSConfig` 启动入口,App.RunWith 可以直接启用 TLS;补充真实 TLS 客户端握手、请求和优雅退出验收。
|
- native 增加 `ServeTLS` / `RunTLS` 和 `TLSConfig` 启动入口,App.RunWith 可以直接启用 TLS;补充真实 TLS 客户端握手、请求和优雅退出验收。
|
||||||
- native 请求补齐 `Request.TLS` 连接状态;Linux/arm64 容器通过全量 race、vet、build,以及 TLS 并发、Keep-Alive、慢握手和连接上限回收验收。
|
- native 请求补齐 `Request.TLS` 连接状态;Linux/arm64 容器通过全量 race、vet、build,以及 TLS 并发、Keep-Alive、慢握手和连接上限回收验收。
|
||||||
- 补充慢请求体超时、大请求体和同连接管线请求验收,确认请求体未完整读取时不会进入业务 Handler,连续请求不会串线。
|
- 补充慢请求体超时、大请求体和同连接管线请求验收,确认流式 body 读取超时会取消请求并关闭连接,连续请求不会串线。
|
||||||
- 补充 `MaxConnections` 满载拒绝与槽位释放验收,确认请求完成后连接容量可再次使用。
|
- 补充 `MaxConnections` 满载拒绝与槽位释放验收,确认请求完成后连接容量可再次使用。
|
||||||
- 增加高频建连/断连的状态池 churn 验收,覆盖连接对象回收与容量复用。
|
- 增加高频建连/断连的状态池 churn 验收,覆盖连接对象回收与容量复用。
|
||||||
|
|
||||||
## v0.6.0-preview
|
## v0.6.0-preview
|
||||||
|
|
||||||
- native 网络层完成慢请求头、慢 Content-Length / chunked body、慢 TLS 握手和 TLS 请求元数据验收,未完整读取的请求不会进入业务 Handler。
|
- native 网络层完成流式 Content-Length / chunked body、慢请求头、慢 body、慢 TLS 握手和 TLS 请求元数据验收;Handler 可在 body 未完全到达时开始读取,超时、畸形和超限 body 会被拒绝并关闭连接。
|
||||||
- 将 native 连接 churn race 验收扩展为 16 个 worker、3,200 次建连/断连,并覆盖明文与 TLS 的连接上限、Keep-Alive 和槽位回收。
|
- 将 native 连接 churn race 验收扩展为 16 个 worker、3,200 次建连/断连,并覆盖明文与 TLS 的连接上限、Keep-Alive 和槽位回收。
|
||||||
- perfcheck 支持最长 30 分钟持续场景,新增 Linux 文件描述符采样;固定记录服务端堆、goroutine、连接数、accept 数和 FD 回收状态。
|
- perfcheck 支持最长 30 分钟持续场景,新增 Linux 文件描述符采样;固定记录服务端堆、goroutine、连接数、accept 数和 FD 回收状态。
|
||||||
- 新增 `scripts/verifyLinux.py`,在 `golang:1.25` Linux 容器内统一执行 race、vet、build 和可选 TLS / 非 TLS 压测。
|
- 新增 `scripts/verifyLinux.py`,在 `golang:1.25` Linux 容器内统一执行 race、vet、build 和可选 TLS / 非 TLS 压测。
|
||||||
|
|||||||
@@ -42,6 +42,7 @@
|
|||||||
- 标准库与 native 共用 TCP benchmark,固定 1 / 32 / 128 并发;相同应用、客户端、超时和响应检查,避免两个测试入口逐渐失去可比性。
|
- 标准库与 native 共用 TCP benchmark,固定 1 / 32 / 128 并发;相同应用、客户端、超时和响应检查,避免两个测试入口逐渐失去可比性。
|
||||||
- 新增独立服务进程压测 `internal/perfcheck`,记录成功吞吐、P50/P95/P99、服务端分配、采样堆峰值,以及 128 / 1,024 条空闲长连接的堆内存、goroutine 和断开后的回收情况。原始报告保存在 `performance/tcpBaseline.json`。
|
- 新增独立服务进程压测 `internal/perfcheck`,记录成功吞吐、P50/P95/P99、服务端分配、采样堆峰值,以及 128 / 1,024 条空闲长连接的堆内存、goroutine 和断开后的回收情况。原始报告保存在 `performance/tcpBaseline.json`。
|
||||||
- native 已支持受 `MaxBodyBytes` 约束的 `Content-Length` / chunked 请求体,仍使用标准 `http.Request` / `http.ResponseWriter` 作为桥接;解析器、连接抽象和生产压测将在后续阶段继续下沉。
|
- native 已支持受 `MaxBodyBytes` 约束的 `Content-Length` / chunked 请求体,仍使用标准 `http.Request` / `http.ResponseWriter` 作为桥接;解析器、连接抽象和生产压测将在后续阶段继续下沉。
|
||||||
|
- native 请求体已改为流式读取,Handler 可边到边处理,服务端只在 Handler 返回后排空剩余 body,超时、畸形和超限 body 会取消请求并关闭连接。
|
||||||
- native 响应已支持标准 `http.Flusher`;HTTP/1.1 流式响应使用 chunked framing,普通响应继续走 `Content-Length`,HTTP/1.0 回退到关闭连接分帧。
|
- native 响应已支持标准 `http.Flusher`;HTTP/1.1 流式响应使用 chunked framing,普通响应继续走 `Content-Length`,HTTP/1.0 回退到关闭连接分帧。
|
||||||
- native 长请求已补客户端断开及流式写失败取消,监测退出后再复用 Reader;已验收普通/流式断开、慢 Handler 后连续 Keep-Alive 和管线数据不被误消费。短请求延迟启动监测,已有管线数据时取消依赖后续写失败或 Handler 返回。
|
- native 长请求已补客户端断开及流式写失败取消,监测退出后再复用 Reader;已验收普通/流式断开、慢 Handler 后连续 Keep-Alive 和管线数据不被误消费。短请求延迟启动监测,已有管线数据时取消依赖后续写失败或 Handler 返回。
|
||||||
- 已补充慢请求头、慢 Content-Length body、慢 chunked body、慢 TLS 握手、大请求体、HTTP/1.0 Keep-Alive、HEAD/204 和管线请求验收;Linux/arm64 容器已通过全量 race、vet、build,以及 3 秒 TLS 并发/空闲连接矩阵。原始报告见 [tlsBaselineLinux.json](performance/tlsBaselineLinux.json)。
|
- 已补充慢请求头、慢 Content-Length body、慢 chunked body、慢 TLS 握手、大请求体、HTTP/1.0 Keep-Alive、HEAD/204 和管线请求验收;Linux/arm64 容器已通过全量 race、vet、build,以及 3 秒 TLS 并发/空闲连接矩阵。原始报告见 [tlsBaselineLinux.json](performance/tlsBaselineLinux.json)。
|
||||||
|
|||||||
@@ -410,12 +410,49 @@ func (s *Server) serveConnection(baseCtx context.Context, state *connection) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
handler := s.handler
|
handler := s.handler
|
||||||
disconnectWatch := newClientDisconnectWatch(reader, state.conn, cancel)
|
var disconnectWatch *clientDisconnectWatch
|
||||||
|
if body, ok := request.Body.(*requestBody); ok {
|
||||||
|
// A body reader and the disconnect watcher must never read the
|
||||||
|
// bufio.Reader concurrently. The body starts the watcher once it
|
||||||
|
// reaches EOF; body read errors cancel the request directly.
|
||||||
|
body.cancel = cancel
|
||||||
|
body.onComplete = func() {
|
||||||
|
disconnectWatch = newClientDisconnectWatch(reader, state.conn, cancel)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
disconnectWatch = newClientDisconnectWatch(reader, state.conn, cancel)
|
||||||
|
}
|
||||||
handler.ServeHTTP(writer, request)
|
handler.ServeHTTP(writer, request)
|
||||||
|
bodyErr := request.Body.Close()
|
||||||
|
if disconnectWatch != nil {
|
||||||
disconnectWatch.stop()
|
disconnectWatch.stop()
|
||||||
|
}
|
||||||
cancel()
|
cancel()
|
||||||
|
|
||||||
closing := s.isClosing() || state.closeWhenIdle.Load()
|
closing := s.isClosing() || state.closeWhenIdle.Load() || bodyErr != nil
|
||||||
|
if bodyErr != nil {
|
||||||
|
var protocolErr *protocolError
|
||||||
|
if !errors.As(bodyErr, &protocolErr) {
|
||||||
|
if requestSlot {
|
||||||
|
<-s.requestSlots
|
||||||
|
}
|
||||||
|
writer.reset("")
|
||||||
|
s.writerPool.Put(writer)
|
||||||
|
state.active.Store(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Request bodies are streamed into the handler, so a malformed
|
||||||
|
// chunk or an over-limit body can be discovered while draining
|
||||||
|
// after the handler returns. Replace the buffered response with
|
||||||
|
// the same protocol error that eager parsing used to return.
|
||||||
|
if !writer.streaming {
|
||||||
|
writer.reset(request.Method)
|
||||||
|
writer.configure(request, state.conn, true, s.WriteTimeout, cancel)
|
||||||
|
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||||
|
writer.WriteHeader(protocolErr.status)
|
||||||
|
writer.body.WriteString(protocolErr.message + "\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
if err := writer.writeTo(state.conn, closeAfter || closing, s.WriteTimeout); err != nil {
|
if err := writer.writeTo(state.conn, closeAfter || closing, s.WriteTimeout); err != nil {
|
||||||
if requestSlot {
|
if requestSlot {
|
||||||
<-s.requestSlots
|
<-s.requestSlots
|
||||||
@@ -577,27 +614,23 @@ func readRequest(reader *bufio.Reader, conn net.Conn, maxHeaderBytes int, maxBod
|
|||||||
if !chunked && contentLength > maxBodyBytes {
|
if !chunked && contentLength > maxBodyBytes {
|
||||||
return nil, true, &protocolError{status: http.StatusRequestEntityTooLarge, message: "request body too large"}
|
return nil, true, &protocolError{status: http.StatusRequestEntityTooLarge, message: "request body too large"}
|
||||||
}
|
}
|
||||||
var body []byte
|
var body io.ReadCloser = http.NoBody
|
||||||
var trailer http.Header
|
var trailer http.Header
|
||||||
if chunked {
|
if chunked {
|
||||||
body, trailer, err = readChunkedBody(reader, maxBodyBytes, setBodyDeadline, readLine)
|
trailer = make(http.Header)
|
||||||
if err != nil {
|
body = &requestBody{
|
||||||
return nil, true, err
|
reader: reader, maxBytes: maxBodyBytes, chunked: true,
|
||||||
|
trailer: trailer, setDeadline: setBodyDeadline, readLine: readLine,
|
||||||
}
|
}
|
||||||
// net/http uses -1 to indicate that the decoded length was not known
|
// net/http uses -1 to indicate that the decoded length was not known
|
||||||
// from the request headers. The body is already buffered for the
|
// from the request headers.
|
||||||
// handler, but retaining this value keeps the request contract
|
|
||||||
// compatible with the standard server.
|
|
||||||
contentLength = -1
|
contentLength = -1
|
||||||
} else {
|
} else {
|
||||||
body = make([]byte, contentLength)
|
|
||||||
if contentLength > 0 {
|
if contentLength > 0 {
|
||||||
if setBodyDeadline != nil {
|
if setBodyDeadline != nil {
|
||||||
setBodyDeadline()
|
setBodyDeadline()
|
||||||
}
|
}
|
||||||
if _, err := io.ReadFull(reader, body); err != nil {
|
body = &requestBody{reader: reader, remaining: contentLength, setDeadline: setBodyDeadline}
|
||||||
return nil, true, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
request := &http.Request{
|
request := &http.Request{
|
||||||
@@ -607,7 +640,7 @@ func readRequest(reader *bufio.Reader, conn net.Conn, maxHeaderBytes int, maxBod
|
|||||||
ProtoMajor: protoMajor,
|
ProtoMajor: protoMajor,
|
||||||
ProtoMinor: protoMinor,
|
ProtoMinor: protoMinor,
|
||||||
Header: header,
|
Header: header,
|
||||||
Body: io.NopCloser(bytes.NewReader(body)),
|
Body: body,
|
||||||
ContentLength: contentLength,
|
ContentLength: contentLength,
|
||||||
Host: header.Get("Host"),
|
Host: header.Get("Host"),
|
||||||
RequestURI: target,
|
RequestURI: target,
|
||||||
@@ -627,20 +660,125 @@ func readRequest(reader *bufio.Reader, conn net.Conn, maxHeaderBytes int, maxBod
|
|||||||
return request, closeAfter, nil
|
return request, closeAfter, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func readChunkedBody(reader *bufio.Reader, maxBodyBytes int64, setBodyDeadline func(), readLine func() (string, error)) ([]byte, http.Header, error) {
|
type requestBody struct {
|
||||||
var body bytes.Buffer
|
reader *bufio.Reader
|
||||||
initialCapacity := int64(64 << 10)
|
remaining int64
|
||||||
if maxBodyBytes < initialCapacity {
|
maxBytes int64
|
||||||
initialCapacity = maxBodyBytes
|
readBytes int64
|
||||||
}
|
chunked bool
|
||||||
if initialCapacity > 0 && initialCapacity <= int64(int(^uint(0)>>1)) {
|
chunkRemain int64
|
||||||
body.Grow(int(initialCapacity))
|
needSuffix bool
|
||||||
}
|
trailer http.Header
|
||||||
|
readLine func() (string, error)
|
||||||
|
setDeadline func()
|
||||||
|
started bool
|
||||||
|
cancel context.CancelFunc
|
||||||
|
onComplete func()
|
||||||
|
done bool
|
||||||
|
closed bool
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
for {
|
func (b *requestBody) Read(p []byte) (int, error) {
|
||||||
line, err := readLine()
|
if len(p) == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
if b.err != nil {
|
||||||
|
return 0, b.err
|
||||||
|
}
|
||||||
|
if b.done {
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
if !b.chunked {
|
||||||
|
return b.readContentLength(p)
|
||||||
|
}
|
||||||
|
return b.readChunked(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *requestBody) readContentLength(p []byte) (int, error) {
|
||||||
|
if b.remaining == 0 {
|
||||||
|
b.complete()
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
if !b.started {
|
||||||
|
b.started = true
|
||||||
|
if b.setDeadline != nil {
|
||||||
|
b.setDeadline()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if int64(len(p)) > b.remaining {
|
||||||
|
p = p[:b.remaining]
|
||||||
|
}
|
||||||
|
n, err := b.reader.Read(p)
|
||||||
|
b.remaining -= int64(n)
|
||||||
|
b.readBytes += int64(n)
|
||||||
|
if b.remaining == 0 {
|
||||||
|
b.complete()
|
||||||
|
if n > 0 {
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
b.fail(err)
|
||||||
|
}
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *requestBody) readChunked(p []byte) (int, error) {
|
||||||
|
for {
|
||||||
|
if b.chunkRemain == 0 {
|
||||||
|
if b.needSuffix {
|
||||||
|
var suffix [2]byte
|
||||||
|
if _, err := io.ReadFull(b.reader, suffix[:]); err != nil {
|
||||||
|
b.fail(err)
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if suffix[0] != '\r' || suffix[1] != '\n' {
|
||||||
|
err := &protocolError{status: http.StatusBadRequest, message: "malformed chunk terminator"}
|
||||||
|
b.fail(err)
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
b.needSuffix = false
|
||||||
|
}
|
||||||
|
if err := b.beginChunk(); err != nil {
|
||||||
|
b.fail(err)
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if b.done {
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if int64(len(p)) > b.chunkRemain {
|
||||||
|
p = p[:b.chunkRemain]
|
||||||
|
}
|
||||||
|
n, err := b.reader.Read(p)
|
||||||
|
b.chunkRemain -= int64(n)
|
||||||
|
b.readBytes += int64(n)
|
||||||
|
if b.chunkRemain == 0 {
|
||||||
|
b.needSuffix = true
|
||||||
|
}
|
||||||
|
if n > 0 {
|
||||||
|
if err != nil && b.chunkRemain > 0 {
|
||||||
|
b.fail(err)
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
b.fail(err)
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *requestBody) beginChunk() error {
|
||||||
|
if b.setDeadline != nil {
|
||||||
|
b.setDeadline()
|
||||||
|
}
|
||||||
|
line, err := b.readLine()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
chunkSizeText := line
|
chunkSizeText := line
|
||||||
if extension := strings.IndexByte(chunkSizeText, ';'); extension >= 0 {
|
if extension := strings.IndexByte(chunkSizeText, ';'); extension >= 0 {
|
||||||
@@ -648,56 +786,72 @@ func readChunkedBody(reader *bufio.Reader, maxBodyBytes int64, setBodyDeadline f
|
|||||||
}
|
}
|
||||||
chunkSizeText = strings.TrimSpace(chunkSizeText)
|
chunkSizeText = strings.TrimSpace(chunkSizeText)
|
||||||
if chunkSizeText == "" || !validHexNumber(chunkSizeText) {
|
if chunkSizeText == "" || !validHexNumber(chunkSizeText) {
|
||||||
return nil, nil, &protocolError{status: http.StatusBadRequest, message: "invalid chunk size"}
|
return &protocolError{status: http.StatusBadRequest, message: "invalid chunk size"}
|
||||||
}
|
}
|
||||||
chunkSize, parseErr := strconv.ParseUint(chunkSizeText, 16, 63)
|
chunkSize, parseErr := strconv.ParseUint(chunkSizeText, 16, 63)
|
||||||
if parseErr != nil {
|
if parseErr != nil {
|
||||||
return nil, nil, &protocolError{status: http.StatusBadRequest, message: "invalid chunk size"}
|
return &protocolError{status: http.StatusBadRequest, message: "invalid chunk size"}
|
||||||
}
|
}
|
||||||
if chunkSize == 0 {
|
if chunkSize == 0 {
|
||||||
trailer := make(http.Header)
|
|
||||||
for {
|
for {
|
||||||
line, err = readLine()
|
line, err = b.readLine()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return err
|
||||||
}
|
}
|
||||||
if line == "" {
|
if line == "" {
|
||||||
return body.Bytes(), trailer, nil
|
b.complete()
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(line, " ") || strings.HasPrefix(line, "\t") {
|
if strings.HasPrefix(line, " ") || strings.HasPrefix(line, "\t") {
|
||||||
return nil, nil, &protocolError{status: http.StatusBadRequest, message: "folded trailers are not supported"}
|
return &protocolError{status: http.StatusBadRequest, message: "folded trailers are not supported"}
|
||||||
}
|
}
|
||||||
index := strings.IndexByte(line, ':')
|
index := strings.IndexByte(line, ':')
|
||||||
if index <= 0 {
|
if index <= 0 {
|
||||||
return nil, nil, &protocolError{status: http.StatusBadRequest, message: "malformed trailer"}
|
return &protocolError{status: http.StatusBadRequest, message: "malformed trailer"}
|
||||||
}
|
}
|
||||||
name := strings.TrimSpace(line[:index])
|
name := strings.TrimSpace(line[:index])
|
||||||
value := strings.TrimSpace(line[index+1:])
|
value := strings.TrimSpace(line[index+1:])
|
||||||
if !validToken(name) || !validHeaderValue(value) {
|
if !validToken(name) || !validHeaderValue(value) {
|
||||||
return nil, nil, &protocolError{status: http.StatusBadRequest, message: "malformed trailer"}
|
return &protocolError{status: http.StatusBadRequest, message: "malformed trailer"}
|
||||||
}
|
}
|
||||||
trailer.Add(name, value)
|
b.trailer.Add(name, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if int64(body.Len()) > maxBodyBytes-int64(chunkSize) {
|
if int64(chunkSize) > b.maxBytes-b.readBytes {
|
||||||
return nil, nil, &protocolError{status: http.StatusRequestEntityTooLarge, message: "request body too large"}
|
return &protocolError{status: http.StatusRequestEntityTooLarge, message: "request body too large"}
|
||||||
}
|
}
|
||||||
if setBodyDeadline != nil {
|
b.chunkRemain = int64(chunkSize)
|
||||||
setBodyDeadline()
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *requestBody) Close() error {
|
||||||
|
if b.closed {
|
||||||
|
return b.err
|
||||||
}
|
}
|
||||||
if _, err := io.CopyN(&body, reader, int64(chunkSize)); err != nil {
|
b.closed = true
|
||||||
return nil, nil, err
|
if !b.done && b.err == nil {
|
||||||
|
_, _ = io.Copy(io.Discard, b)
|
||||||
}
|
}
|
||||||
if setBodyDeadline != nil {
|
return b.err
|
||||||
setBodyDeadline()
|
}
|
||||||
|
|
||||||
|
func (b *requestBody) complete() {
|
||||||
|
if b.done {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
var suffix [2]byte
|
b.done = true
|
||||||
if _, err := io.ReadFull(reader, suffix[:]); err != nil {
|
if b.onComplete != nil {
|
||||||
return nil, nil, err
|
b.onComplete()
|
||||||
}
|
}
|
||||||
if suffix[0] != '\r' || suffix[1] != '\n' {
|
}
|
||||||
return nil, nil, &protocolError{status: http.StatusBadRequest, message: "malformed chunk terminator"}
|
|
||||||
|
func (b *requestBody) fail(err error) {
|
||||||
|
if b.err == nil {
|
||||||
|
b.err = err
|
||||||
}
|
}
|
||||||
|
b.done = true
|
||||||
|
if b.cancel != nil {
|
||||||
|
b.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -107,6 +107,47 @@ func TestServerRejectsMalformedBodyAndLimits(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestServerStreamsContentLengthBodyToHandler(t *testing.T) {
|
||||||
|
entered := make(chan struct{})
|
||||||
|
application := omni.NewWithRouter(netrouter.New())
|
||||||
|
application.POST("/body", func(ctx *omni.Context) error {
|
||||||
|
close(entered)
|
||||||
|
body, err := io.ReadAll(ctx.Request().Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return ctx.OK(map[string]string{"body": string(body)})
|
||||||
|
})
|
||||||
|
server, listener, done := startServer(t, application)
|
||||||
|
defer stopServer(t, server, listener, done)
|
||||||
|
|
||||||
|
conn, err := net.Dial("tcp", listener.Addr().String())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
if _, err := io.WriteString(conn, "POST /body HTTP/1.1\r\nHost: test\r\nContent-Length: 5\r\n\r\nhel"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-entered:
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal("handler did not start before the complete body arrived")
|
||||||
|
}
|
||||||
|
if _, err := io.WriteString(conn, "lo"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
response, err := http.ReadResponse(bufio.NewReader(conn), &http.Request{Method: http.MethodPost})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
body, err := io.ReadAll(response.Body)
|
||||||
|
response.Body.Close()
|
||||||
|
if err != nil || response.StatusCode != http.StatusOK || !strings.Contains(string(body), `"body":"hello"`) {
|
||||||
|
t.Fatalf("unexpected streamed body response: status=%d body=%s error=%v", response.StatusCode, body, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestServerChunkedBodyAndTrailers(t *testing.T) {
|
func TestServerChunkedBodyAndTrailers(t *testing.T) {
|
||||||
application := omni.NewWithRouter(netrouter.New())
|
application := omni.NewWithRouter(netrouter.New())
|
||||||
application.POST("/body", func(ctx *omni.Context) error {
|
application.POST("/body", func(ctx *omni.Context) error {
|
||||||
@@ -154,11 +195,18 @@ func TestServerChunkedBodyLimit(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestServerSlowBodyTimesOutBeforeHandler(t *testing.T) {
|
func TestServerSlowBodyTimesOutWithoutSuccessfulHandler(t *testing.T) {
|
||||||
var handlerCalls atomic.Int32
|
var handlerCalls atomic.Int32
|
||||||
application := omni.NewWithRouter(netrouter.New())
|
application := omni.NewWithRouter(netrouter.New())
|
||||||
application.POST("/body", func(ctx *omni.Context) error {
|
application.POST("/body", func(ctx *omni.Context) error {
|
||||||
|
body, err := io.ReadAll(ctx.Request().Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
handlerCalls.Add(1)
|
handlerCalls.Add(1)
|
||||||
|
if len(body) != 4 {
|
||||||
|
return fmt.Errorf("unexpected body length %d", len(body))
|
||||||
|
}
|
||||||
return ctx.OK("ok")
|
return ctx.OK("ok")
|
||||||
})
|
})
|
||||||
server := native.New(application)
|
server := native.New(application)
|
||||||
@@ -178,14 +226,10 @@ func TestServerSlowBodyTimesOutBeforeHandler(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
conn.SetReadDeadline(time.Now().Add(time.Second))
|
|
||||||
_, _ = io.WriteString(conn, "POST /body HTTP/1.1\r\nHost: test\r\nContent-Length: 4\r\n\r\na")
|
_, _ = io.WriteString(conn, "POST /body HTTP/1.1\r\nHost: test\r\nContent-Length: 4\r\n\r\na")
|
||||||
buffer := make([]byte, 1)
|
requirePeerClose(t, conn)
|
||||||
if _, err := conn.Read(buffer); err == nil {
|
|
||||||
t.Fatal("expected slow request body to time out and close")
|
|
||||||
}
|
|
||||||
if handlerCalls.Load() != 0 {
|
if handlerCalls.Load() != 0 {
|
||||||
t.Fatal("handler ran before the request body was complete")
|
t.Fatal("handler reported success before the request body was complete")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,8 +43,10 @@ func TestServerSlowHeadersCloseBeforeHandler(t *testing.T) {
|
|||||||
|
|
||||||
func TestServerSlowChunkedBodyClosesBeforeHandler(t *testing.T) {
|
func TestServerSlowChunkedBodyClosesBeforeHandler(t *testing.T) {
|
||||||
var handlerCalls atomic.Int32
|
var handlerCalls atomic.Int32
|
||||||
server := native.New(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
|
server := native.New(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
|
||||||
|
if _, err := io.ReadAll(r.Body); err == nil {
|
||||||
handlerCalls.Add(1)
|
handlerCalls.Add(1)
|
||||||
|
}
|
||||||
}))
|
}))
|
||||||
server.ReadTimeout = 40 * time.Millisecond
|
server.ReadTimeout = 40 * time.Millisecond
|
||||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
|||||||
Reference in New Issue
Block a user