From be2dfe25fb2a4f0464c7c44a7e767a21dd043284 Mon Sep 17 00:00:00 2001 From: Wenyi Tang Date: Tue, 8 Sep 2026 22:47:51 +0800 Subject: [PATCH] =?UTF-8?q?[dev]=20=E4=BF=AE=E5=A4=8D=20HTTP/2=20=E9=87=8D?= =?UTF-8?q?=E8=AF=95=E6=B5=8B=E8=AF=95=E7=9A=84=E5=81=B6=E5=8F=91=E5=A4=B1?= =?UTF-8?q?=E8=B4=A5=EF=BC=88RST=20=E4=B8=8E=20GOAWAY=20=E7=AB=9E=E6=80=81?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TestUpstreamGetBody_HTTP2RetryAfterGracefulGoAway_PassThrough 等用例 偶发失败,报错为 wsarecv: connection forcibly closed(TCP RST)。 根因:测试服务器写完 GOAWAY/响应帧后立即 conn.Close(),此时连接中 仍有客户端未读的入站帧(SETTINGS ACK、WINDOW_UPDATE 等)。Windows 上关闭仍有未读数据的 socket 会发送 RST 而非 FIN;RST 与客户端读取 GOAWAY 帧竞速,抢先到达时连接级错误取代了优雅关闭,transport 不再 重试,client.Do 直接报错。 修复:模拟真实服务器的优雅关闭(RFC 9113 §6.8)——新增 drainH2TestConnection,写完末帧后由后台 goroutine 持续读取并丢弃 帧直到对端关闭(受既有 15s 连接 deadline 约束)再关闭 socket, 彻底消除 RST。GOAWAY 服务器与 RST_STREAM 服务器两个辅助函数的 所有关闭路径统一改为排水后关闭;错误路径仍立即关闭。 验证:目标用例 90/90 连续通过(修复前约 10–20% 失败率),整个 relay/channel 包及全量 33 个包测试通过。 --- relay/channel/api_request_getbody_test.go | 39 ++++++++++++++++++----- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/relay/channel/api_request_getbody_test.go b/relay/channel/api_request_getbody_test.go index 9a2de73337cd..39125453da6f 100644 --- a/relay/channel/api_request_getbody_test.go +++ b/relay/channel/api_request_getbody_test.go @@ -351,6 +351,24 @@ func awaitH2ServerResult(t *testing.T, resultCh <-chan h2ServerResult) h2ServerR // retry-safe reset some proxy/CDN-fronted upstreams send under load or during // graceful shutdown, see RFC 9113 section 8.7). When expectRetry is true it // serves the retried stream a 200 response; otherwise it stops after the reset. +// drainH2TestConnection keeps reading and discarding frames until the peer +// closes the connection (bounded by the deadline set at accept time), then +// closes the socket. Closing a connection while inbound frames are still +// buffered unread sends a TCP RST instead of a FIN, which races the client's +// processing of the final GOAWAY/response frames and turns the graceful +// shutdown the tests simulate into a hard connection error. A real server +// drains like this after GOAWAY too (RFC 9113 section 6.8). +func drainH2TestConnection(conn net.Conn, framer *http2.Framer) { + go func() { + defer conn.Close() + for { + if _, err := framer.ReadFrame(); err != nil { + return + } + } + }() +} + func runResetOnFirstStreamServer(ln net.Listener, expectRetry bool) <-chan h2ServerResult { resCh := make(chan h2ServerResult, 1) go func() { @@ -362,12 +380,11 @@ func runResetOnFirstStreamServer(ln net.Listener, expectRetry bool) <-chan h2Ser res.err = err return } - defer conn.Close() - attempts: for attempt := 0; ; attempt++ { streamID, body, err := readH2TestRequest(framer) if err != nil { + conn.Close() res.err = err return } @@ -376,16 +393,20 @@ func runResetOnFirstStreamServer(ln net.Listener, expectRetry bool) <-chan h2Ser if attempt == 0 { if err := framer.WriteRSTStream(streamID, http2.ErrCodeRefusedStream); err != nil { + conn.Close() res.err = err return } if !expectRetry { - break attempts + drainH2TestConnection(conn, framer) + return } continue } - if err := writeH2TestResponse(framer, streamID); err != nil { + err = writeH2TestResponse(framer, streamID) + drainH2TestConnection(conn, framer) + if err != nil { res.err = err } return @@ -416,17 +437,19 @@ func runGoAwayAfterFirstRequestServer(ln net.Listener) <-chan h2ServerResult { res.attemptBodies = append(res.attemptBodies, body) if attempt == 0 { - err = framer.WriteGoAway(0, http2.ErrCodeNo, nil) - conn.Close() - if err != nil { + if err := framer.WriteGoAway(0, http2.ErrCodeNo, nil); err != nil { + conn.Close() res.err = err return } + // hand the connection to the drain goroutine and accept the + // retry connection right away + drainH2TestConnection(conn, framer) continue } err = writeH2TestResponse(framer, streamID) - conn.Close() + drainH2TestConnection(conn, framer) if err != nil { res.err = err }