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
128 changes: 128 additions & 0 deletions Sources/HTTPClientConformance/HTTPClientConformance.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ struct BasicConformanceTests<Client: HTTPClient & ~Copyable> {
try await testPostConvenience()
try await testCancelPreHeaders()
try await testCancelPreBody()
try await testClientSendsEmptyHeaderValue()
try await testInfiniteRedirect()
try await testHeadWithContentLength()
try await testServerSendsMultiValueHeader()
try await testClientSendsMultiValueHeader()

// TODO: URLSession client hangs because of a bug where single bytes cannot be sent.
// try await testEchoInterleave()
Expand Down Expand Up @@ -308,6 +313,24 @@ struct BasicConformanceTests<Client: HTTPClient & ~Copyable> {
}
}

func testInfiniteRedirect() async throws {
let client = try await clientFactory()

let request = HTTPRequest(
method: .get,
scheme: "http",
authority: "127.0.0.1:\(port)",
path: "/redirect_ping"
)

// Infinite redirection should cause an error to be thrown
await #expect(throws: (any Error).self) {
try await client.perform(
request: request,
) { _, _ in }
}
}

func testNotFound() async throws {
let client = try await clientFactory()
let request = HTTPRequest(
Expand Down Expand Up @@ -424,6 +447,32 @@ struct BasicConformanceTests<Client: HTTPClient & ~Copyable> {
}
}

func testClientSendsEmptyHeaderValue() async throws {
let client = try await clientFactory()
let request = HTTPRequest(
method: .post,
scheme: "http",
authority: "127.0.0.1:\(port)",
path: "/request",
headerFields: [
.init("X-Test")!: ""
]
)

try await client.perform(
request: request,
) { 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.headers["X-Test"] == [""])
}
}

func testSpeakInterleave() async throws {
let request = HTTPRequest(
method: .post,
Expand Down Expand Up @@ -582,4 +631,83 @@ struct BasicConformanceTests<Client: HTTPClient & ~Copyable> {
#expect(!jsonRequest.headers.isEmpty)
#expect(jsonRequest.body == "Hello World")
}

func testHeadWithContentLength() async throws {
let client = try await clientFactory()
let request = HTTPRequest(
method: .head,
scheme: "http",
authority: "127.0.0.1:\(port)",
path: "/head_with_cl"
)
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))
}
#expect(body.isEmpty)
#expect(trailers == nil)
}
}

func testServerSendsMultiValueHeader() async throws {
let client = try await clientFactory()
let request = HTTPRequest(
method: .get,
scheme: "http",
authority: "127.0.0.1:\(port)",
path: "/header_multivalue"
)
try await client.perform(
request: request,
) { response, responseBodyAndTrailers in
#expect(response.status == .ok)
let values = response.headerFields[values: .init("X-Test")!]

// If the values are comma-separated, break them up.
var split_values: [Substring] = []
for value in values {
let iter_splits = value.split(separator: /(\s)*,(\s)*/)
split_values.append(contentsOf: iter_splits)
}

#expect(split_values == ["one", "two"])
}
}

func testClientSendsMultiValueHeader() async throws {
let client = try await clientFactory()
let request = HTTPRequest(
method: .get,
scheme: "http",
authority: "127.0.0.1:\(port)",
path: "/request",
headerFields: [
.init("X-Test")!: "one",
.init("X-Test")!: "two",
]
)
try await client.perform(
request: request,
) { response, responseBodyAndTrailers in
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)
}

let values = jsonRequest.headers["X-Test"]!

// If the values are comma-separated, break them up.
var split_values: [Substring] = []
for value in values {
let iter_splits = value.split(separator: /(\s)*,(\s)*/)
split_values.append(contentsOf: iter_splits)
}

#expect(split_values == ["one", "two"])
}
}
}
39 changes: 39 additions & 0 deletions Sources/HTTPClientConformance/TestHTTPServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,21 @@ func serve(server: NIOHTTPServer) async throws {
let responseSpan = responseData.span
let writer = try await responseSender.send(HTTPResponse(status: .ok))
try await writer.writeAndConclude(responseSpan, finalElement: nil)
case "/head_with_cl":
if request.method != .head {
try await responseSender.send(HTTPResponse(status: .methodNotAllowed))
break
}

// OK with a theoretical 1000-byte body
try await responseSender.send(
HTTPResponse(
status: .ok,
headerFields: [
.contentLength: "1000"
]
)
)
case "/200":
// OK
let writer = try await responseSender.send(HTTPResponse(status: .ok))
Expand Down Expand Up @@ -142,11 +157,35 @@ func serve(server: NIOHTTPServer) async throws {

let writer = try await responseSender.send(HTTPResponse(status: .ok, headerFields: headers))
try await writer.writeAndConclude(bytes.span, finalElement: nil)
case "/header_multivalue":
try await responseSender.send(
HTTPResponse(
status: .ok,
headerFields: [
.init("X-Test")!: "one",
.init("X-Test")!: "two",
]
)
)
case "/identity":
// This will always write out the body with no encoding.
// Used to check that a client can handle fallback to no encoding.
let writer = try await responseSender.send(HTTPResponse(status: .ok))
try await writer.writeAndConclude("TEST\n".utf8.span, finalElement: nil)
case "/redirect_ping":
// Infinite redirection as a result of arriving here
let writer = try await responseSender.send(
HTTPResponse(status: .movedPermanently, headerFields: HTTPFields([HTTPField(name: .location, value: "/redirect_pong")]))
)
try await writer
.writeAndConclude("".utf8.span, finalElement: nil)
case "/redirect_pong":
// Infinite redirection as a result of arriving here
let writer = try await responseSender.send(
HTTPResponse(status: .movedPermanently, headerFields: HTTPFields([HTTPField(name: .location, value: "/redirect_ping")]))
)
try await writer
.writeAndConclude("".utf8.span, finalElement: nil)
case "/301":
// Redirect to /request
let writer = try await responseSender.send(
Expand Down
Loading