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,