From c078f06670c1b161c7683c4101be944cdf3fab56 Mon Sep 17 00:00:00 2001 From: brennobemoura <37243584+brennobemoura@users.noreply.github.com> Date: Fri, 2 Oct 2026 08:57:42 -0300 Subject: [PATCH] Fix the redirect-with-body test endpoint announcing a length it does not send /redirect/302-with-body announces a Content-Length of `size`, but the test server wrapped every body into the JSON description of the request, which is longer. The bytes after the announced length were taken by the client for the start of the next response, failed to parse, and closed the connection, so the next request of a redirect chain failed with `I/O on closed channel` in 15 to 20 percent of the tries. It was not a bug of the client: the same sequence of requests against the fixed endpoint did not fail in 2400 tries. Add a way to send a body as it is, use it for this endpoint, and add a test that follows redirects with bodies that are not read, which fails when the endpoint announces a length that it does not send. Co-Authored-By: Claude Sonnet 5.5 --- .../AsyncAwaitEndToEndTests.swift | 23 +++++++++++++++++++ .../HTTPClientTestUtils.swift | 11 ++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/Tests/AsyncHTTPClientTests/AsyncAwaitEndToEndTests.swift b/Tests/AsyncHTTPClientTests/AsyncAwaitEndToEndTests.swift index 2f01c96bf..0d6371944 100644 --- a/Tests/AsyncHTTPClientTests/AsyncAwaitEndToEndTests.swift +++ b/Tests/AsyncHTTPClientTests/AsyncAwaitEndToEndTests.swift @@ -733,6 +733,29 @@ final class AsyncAwaitEndToEndTests: XCTestCase { _ = try await httpClient.execute(request, deadline: .now() + .seconds(2)) } + func testFollowingRedirectsWithBodiesThatAreNotReadDoesNotFailTheNextRequest() { + XCTAsyncTest { + let bin = HTTPBin(.http1_1(compress: false)) + defer { XCTAssertNoThrow(try bin.shutdown()) } + var config = HTTPClient.Configuration() + config.redirectConfiguration = .follow(max: 5, allowCycles: false) + let client = HTTPClient(eventLoopGroupProvider: .singleton, configuration: config) + defer { XCTAssertNoThrow(try client.syncShutdown()) } + + // The body of a redirect is dropped without being read. This is repeated because what could go wrong is + // up to timing: the next request must not get a connection that is not usable. + for _ in 0..<150 { + let response = try await client.execute( + HTTPClientRequest(url: "http://localhost:\(bin.port)/redirect/302-with-body?size=4096"), + deadline: .now() + .seconds(10) + ) + XCTAssertEqual(response.status, .ok) + XCTAssertEqual(response.history.count, 2) + _ = try await response.body.collect(upTo: 1024 * 1024) + } + } + } + func testRedirectChangesHostHeader() { XCTAsyncTest { let bin = HTTPBin(.http2(compress: false)) diff --git a/Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift b/Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift index cd5f0f485..7e9d3a4be 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift @@ -807,6 +807,9 @@ internal struct HTTPResponseBuilder { var requestBodyByteCount: Int let responseBodyIsRequestBodyByteCount: Bool let trailers: HTTPHeaders? + /// Sends `body` as it is, instead of wrapping it into the JSON description of the request that other endpoints + /// answer with. Needed to answer with a body that has exactly the length that the response head announces. + var sendsBodyVerbatim = false init( _ version: HTTPVersion = HTTPVersion(major: 1, minor: 1), @@ -1064,6 +1067,10 @@ internal final class HTTPBinHandler: ChannelInboundHandler { headers.replaceOrAdd(name: "content-length", value: "\(size)") var builder = HTTPResponseBuilder(status: .found, headers: headers) builder.body = ByteBuffer(repeating: UInt8(ascii: "x"), count: size) + // The head announces `size` bytes, the body must have exactly that many. Wrapped into the JSON description of + // the request it would be longer, and the bytes after the announced length would be taken for the start of + // the next response. + builder.sendsBodyVerbatim = true self.resps.append(builder) return case "/percent%20encoded": @@ -1160,7 +1167,9 @@ internal final class HTTPBinHandler: ChannelInboundHandler { var response = self.resps.removeFirst() response.head.headers.add(contentsOf: self.responseHeaders) context.write(wrapOutboundOut(.head(response.head)), promise: nil) - if let body = response.body { + if response.sendsBodyVerbatim, let body = response.body { + context.write(wrapOutboundOut(.body(.byteBuffer(body))), promise: nil) + } else if let body = response.body { let requestInfo = RequestInfo( data: String(buffer: body), requestNumber: self.requestId,