From 4cb66e570c4ced1ee52ece11e85c6b85193ddc89 Mon Sep 17 00:00:00 2001 From: Xyan Bhatnagar Date: Mon, 9 Mar 2026 15:30:22 -0700 Subject: [PATCH 1/4] Add Trailer Read Test Makes a request to `/trailers` endpoint and verifies that the trailers are processed correctly. --- .../HTTPClientConformance.swift | 31 +++++++++++++++++++ .../HTTPServerForTesting/TestHTTPServer.swift | 14 +++++++++ .../DarwinHTTPClientTests.swift | 3 ++ 3 files changed, 48 insertions(+) diff --git a/Sources/HTTPClientConformance/HTTPClientConformance.swift b/Sources/HTTPClientConformance/HTTPClientConformance.swift index 6ba2cc0..00cdad1 100644 --- a/Sources/HTTPClientConformance/HTTPClientConformance.swift +++ b/Sources/HTTPClientConformance/HTTPClientConformance.swift @@ -66,6 +66,7 @@ public enum ConformanceTestCase: Sendable, Hashable, CaseIterable { case testEmptyChunkedBody case testURLParams case testETag + case testTrailerRead } // Runs an HTTP client through all the conformance tests, @@ -147,6 +148,7 @@ struct ConformanceTestSuite { case .testEmptyChunkedBody: try await testEmptyChunkedBody() case .testURLParams: try await testURLParams() case .testETag: try await testETag() + case .testTrailerRead: try await testTrailerRead() } } @@ -1140,4 +1142,33 @@ struct ConformanceTestSuite { ) } } + + func testTrailerRead() async throws { + let client = try await clientFactory() + let request = HTTPRequest( + method: .get, + scheme: "http", + authority: "127.0.0.1:\(testServerPort)", + path: "/trailers" + ) + try await client.perform( + request: request + ) { response, responseBodyAndTrailers in + #expect(response.status == .ok) + let (body, trailers) = try await responseBodyAndTrailers.collect(upTo: 1024) { span in + return String(copying: try UTF8Span(validating: span)) + } + + // Verify the body + #expect(body == "Response body") + + // Verify that trailers were received + #expect(trailers != nil) + + // Verify the custom trailer headers + #expect(trailers![.init("X-Trailer-One")!] == "first-value") + #expect(trailers![.init("X-Trailer-Two")!] == "second-value") + #expect(trailers![.init("X-Checksum")!] == "abc123") + } + } } diff --git a/Sources/HTTPClientConformance/HTTPServerForTesting/TestHTTPServer.swift b/Sources/HTTPClientConformance/HTTPServerForTesting/TestHTTPServer.swift index 4bc4e79..26634d7 100644 --- a/Sources/HTTPClientConformance/HTTPServerForTesting/TestHTTPServer.swift +++ b/Sources/HTTPClientConformance/HTTPServerForTesting/TestHTTPServer.swift @@ -407,6 +407,20 @@ func serve(server: NIOHTTPServer) async throws { let data = serverETag.data(using: .ascii)! try await responseBodyAndTrailers.writeAndConclude(data.span, finalElement: nil) } + case "/trailers": + // Send a response with custom trailers + let responseBodyAndTrailers = try await responseSender.send(.init(status: .ok)) + try await responseBodyAndTrailers.produceAndConclude { responseBody in + var responseBody = responseBody + // Write the body + try await responseBody.write("Response body".utf8.span) + // Return custom trailers + return [ + .init("X-Trailer-One")!: "first-value", + .init("X-Trailer-Two")!: "second-value", + .init("X-Checksum")!: "abc123", + ] + } default: let writer = try await responseSender.send(HTTPResponse(status: .internalServerError)) try await writer.writeAndConclude("Unknown path".utf8.span, finalElement: nil) diff --git a/Tests/HTTPClientTests/DarwinHTTPClientTests.swift b/Tests/HTTPClientTests/DarwinHTTPClientTests.swift index 8926994..3bcbdab 100644 --- a/Tests/HTTPClientTests/DarwinHTTPClientTests.swift +++ b/Tests/HTTPClientTests/DarwinHTTPClientTests.swift @@ -41,6 +41,9 @@ struct DarwinHTTPClientTests { // TODO: Writing just an empty span causes an indefinite stall. The terminating chunk (size 0) is not written out on the wire. .testEmptyChunkedBody, + + // TODO: Trailers are not supported by URLSession + .testTrailerRead, ]) { return DefaultHTTPClient.shared } From fc689d5e398e10fe4359ee65f436f011e31717bf Mon Sep 17 00:00:00 2001 From: Xyan Bhatnagar Date: Wed, 11 Mar 2026 08:29:25 -0700 Subject: [PATCH 2/4] Update Sources/HTTPClientConformance/HTTPClientConformance.swift Co-authored-by: Fabian Fett --- Sources/HTTPClientConformance/HTTPClientConformance.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/HTTPClientConformance/HTTPClientConformance.swift b/Sources/HTTPClientConformance/HTTPClientConformance.swift index 00cdad1..a0a405b 100644 --- a/Sources/HTTPClientConformance/HTTPClientConformance.swift +++ b/Sources/HTTPClientConformance/HTTPClientConformance.swift @@ -1166,9 +1166,9 @@ struct ConformanceTestSuite { #expect(trailers != nil) // Verify the custom trailer headers - #expect(trailers![.init("X-Trailer-One")!] == "first-value") - #expect(trailers![.init("X-Trailer-Two")!] == "second-value") - #expect(trailers![.init("X-Checksum")!] == "abc123") + #expect(trailers?[.init("X-Trailer-One")!] == "first-value") + #expect(trailers?[.init("X-Trailer-Two")!] == "second-value") + #expect(trailers?[.init("X-Checksum")!] == "abc123") } } } From c0aa94ac9dc763f17b7fe74e19972be1cf1b6c82 Mon Sep 17 00:00:00 2001 From: Xyan Bhatnagar Date: Wed, 11 Mar 2026 09:08:49 -0700 Subject: [PATCH 3/4] Add trailer write test --- .../HTTPClientConformance.swift | 32 +++++++++++++++++++ .../HTTPServerForTesting/TestHTTPServer.swift | 17 ++++++++-- .../DarwinHTTPClientTests.swift | 1 + 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/Sources/HTTPClientConformance/HTTPClientConformance.swift b/Sources/HTTPClientConformance/HTTPClientConformance.swift index a0a405b..16937c6 100644 --- a/Sources/HTTPClientConformance/HTTPClientConformance.swift +++ b/Sources/HTTPClientConformance/HTTPClientConformance.swift @@ -67,6 +67,7 @@ public enum ConformanceTestCase: Sendable, Hashable, CaseIterable { case testURLParams case testETag case testTrailerRead + case testTrailerWrite } // Runs an HTTP client through all the conformance tests, @@ -149,6 +150,7 @@ struct ConformanceTestSuite { case .testURLParams: try await testURLParams() case .testETag: try await testETag() case .testTrailerRead: try await testTrailerRead() + case .testTrailerWrite: try await testTrailerWrite() } } @@ -1171,4 +1173,34 @@ struct ConformanceTestSuite { #expect(trailers?[.init("X-Checksum")!] == "abc123") } } + + func testTrailerWrite() async throws { + let client = try await clientFactory() + let request = HTTPRequest( + method: .post, + scheme: "http", + authority: "127.0.0.1:\(testServerPort)", + path: "/request" + ) + try await client.perform( + request: request, + body: .restartable { writer in + var writer = writer + try await writer.write("Hello World".utf8.span) + return [ + .init("X-Request-Trailer-One")!: "first-trailer-value", + .init("X-Request-Trailer-Two")!: "second-trailer-value", + ] + } + ) { response, responseBodyAndTrailers in + #expect(response.status == .ok) + let (jsonRequest, _) = try await responseBodyAndTrailers.collect(upTo: 1024) { span in + let body = String(copying: try UTF8Span(validating: span)) + let data = body.data(using: .utf8)! + return try JSONDecoder().decode(JSONHTTPRequest.self, from: data) + } + #expect(jsonRequest.trailers["X-Request-Trailer-One"] == ["first-trailer-value"]) + #expect(jsonRequest.trailers["X-Request-Trailer-Two"] == ["second-trailer-value"]) + } + } } diff --git a/Sources/HTTPClientConformance/HTTPServerForTesting/TestHTTPServer.swift b/Sources/HTTPClientConformance/HTTPServerForTesting/TestHTTPServer.swift index 26634d7..55bf699 100644 --- a/Sources/HTTPClientConformance/HTTPServerForTesting/TestHTTPServer.swift +++ b/Sources/HTTPClientConformance/HTTPServerForTesting/TestHTTPServer.swift @@ -32,6 +32,9 @@ struct JSONHTTPRequest: Codable { // Method of the request let method: String + + // Trailers from the request + let trailers: [String: [String]] } @available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *) @@ -110,15 +113,23 @@ func serve(server: NIOHTTPServer) async throws { headers[field.name.rawName, default: []].append(field.value) } - // Parse the body as a UTF8 string - let (body, _) = try await requestBodyAndTrailers.collect(upTo: 1024) { span in + // Parse the body as a UTF8 string and capture trailers + let (body, requestTrailers) = try await requestBodyAndTrailers.collect(upTo: 1024) { span in return String(copying: try UTF8Span(validating: span)) } + // Collect the trailers that were sent in with the request + var trailers: [String: [String]] = [:] + if let requestTrailers { + for field in requestTrailers { + trailers[field.name.rawName, default: []].append(field.value) + } + } + let method = request.method.rawValue // Construct the JSON request object and send it as a response - let response = JSONHTTPRequest(params: params, headers: headers, body: body, method: method) + let response = JSONHTTPRequest(params: params, headers: headers, body: body, method: method, trailers: trailers) let responseData = try JSONEncoder().encode(response) let responseSpan = responseData.span diff --git a/Tests/HTTPClientTests/DarwinHTTPClientTests.swift b/Tests/HTTPClientTests/DarwinHTTPClientTests.swift index 3bcbdab..bd50815 100644 --- a/Tests/HTTPClientTests/DarwinHTTPClientTests.swift +++ b/Tests/HTTPClientTests/DarwinHTTPClientTests.swift @@ -44,6 +44,7 @@ struct DarwinHTTPClientTests { // TODO: Trailers are not supported by URLSession .testTrailerRead, + .testTrailerWrite, ]) { return DefaultHTTPClient.shared } From 2a64dd2911522eadf6c9f61832041103aed2e838 Mon Sep 17 00:00:00 2001 From: Xyan Bhatnagar Date: Wed, 11 Mar 2026 09:11:03 -0700 Subject: [PATCH 4/4] Add body check to write test --- Sources/HTTPClientConformance/HTTPClientConformance.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/HTTPClientConformance/HTTPClientConformance.swift b/Sources/HTTPClientConformance/HTTPClientConformance.swift index 16937c6..d8e4f39 100644 --- a/Sources/HTTPClientConformance/HTTPClientConformance.swift +++ b/Sources/HTTPClientConformance/HTTPClientConformance.swift @@ -1199,6 +1199,7 @@ struct ConformanceTestSuite { let data = body.data(using: .utf8)! return try JSONDecoder().decode(JSONHTTPRequest.self, from: data) } + #expect(jsonRequest.body == "Hello World") #expect(jsonRequest.trailers["X-Request-Trailer-One"] == ["first-trailer-value"]) #expect(jsonRequest.trailers["X-Request-Trailer-Two"] == ["second-trailer-value"]) }