Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions Sources/HTTPClientConformance/HTTPClientConformance.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ public enum ConformanceTestCase: Sendable, Hashable, CaseIterable {
case testEmptyChunkedBody
case testURLParams
case testETag
case testTrailerRead
case testTrailerWrite
}

// Runs an HTTP client through all the conformance tests,
Expand Down Expand Up @@ -147,6 +149,8 @@ struct ConformanceTestSuite<Client: HTTPClient & ~Copyable> {
case .testEmptyChunkedBody: try await testEmptyChunkedBody()
case .testURLParams: try await testURLParams()
case .testETag: try await testETag()
case .testTrailerRead: try await testTrailerRead()
case .testTrailerWrite: try await testTrailerWrite()
}
}

Expand Down Expand Up @@ -1140,4 +1144,64 @@ struct ConformanceTestSuite<Client: HTTPClient & ~Copyable> {
)
}
}

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")
}
}

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.body == "Hello World")
#expect(jsonRequest.trailers["X-Request-Trailer-One"] == ["first-trailer-value"])
#expect(jsonRequest.trailers["X-Request-Trailer-Two"] == ["second-trailer-value"])
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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, *)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -407,6 +418,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)
Expand Down
4 changes: 4 additions & 0 deletions Tests/HTTPClientTests/DarwinHTTPClientTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ 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,
.testTrailerWrite,
]) {
return DefaultHTTPClient.shared
}
Expand Down
Loading