diff --git a/Sources/AsyncHTTPClient/ConnectionPool/HTTPExecutingRequest.swift b/Sources/AsyncHTTPClient/ConnectionPool/HTTPExecutingRequest.swift index 5c4f0f6ab..4ab740314 100644 --- a/Sources/AsyncHTTPClient/ConnectionPool/HTTPExecutingRequest.swift +++ b/Sources/AsyncHTTPClient/ConnectionPool/HTTPExecutingRequest.swift @@ -189,10 +189,18 @@ protocol HTTPRequestExecutor { protocol HTTPExecutingRequest: AnyObject { /// The request's head. /// - /// Based on the content of the request head the task executor will call `startRequestBodyStream` - /// after `requestHeadSent` was called. + /// The HTTP request head, that shall be sent. The HTTPRequestExecutor **will not** run any validation + /// check on the request head. All necessary metadata about the request head the executor expects in + /// the ``requestFramingMetadata``. var requestHead: HTTPRequestHead { get } + /// The request's framing metadata. + /// + /// The request framing metadata that is derived from the ``requestHead``. Based on the content of the + /// request framing metadata the executor will call ``startRequestBodyStream`` after + /// ``requestHeadSent``. + var requestFramingMetadata: RequestFramingMetadata { get } + /// The maximal `TimeAmount` that is allowed to pass between `channelRead`s from the Channel. var idleReadTimeout: TimeAmount? { get } diff --git a/Sources/AsyncHTTPClient/ConnectionPool/RequestFramingMetadata.swift b/Sources/AsyncHTTPClient/ConnectionPool/RequestFramingMetadata.swift index 741f06c2e..cc000c88e 100644 --- a/Sources/AsyncHTTPClient/ConnectionPool/RequestFramingMetadata.swift +++ b/Sources/AsyncHTTPClient/ConnectionPool/RequestFramingMetadata.swift @@ -12,8 +12,8 @@ // //===----------------------------------------------------------------------===// -struct RequestFramingMetadata { - enum Body { +struct RequestFramingMetadata: Hashable { + enum Body: Hashable { case none case stream case fixedSize(Int) diff --git a/Sources/AsyncHTTPClient/HTTPHandler.swift b/Sources/AsyncHTTPClient/HTTPHandler.swift index 1ae8cdd60..6b99a7bd4 100644 --- a/Sources/AsyncHTTPClient/HTTPHandler.swift +++ b/Sources/AsyncHTTPClient/HTTPHandler.swift @@ -293,6 +293,28 @@ extension HTTPClient { public var port: Int { return self.url.port ?? (self.useTLS ? 443 : 80) } + + func createRequestHead() throws -> (HTTPRequestHead, RequestFramingMetadata) { + var head = HTTPRequestHead( + version: .http1_1, + method: self.method, + uri: self.uri, + headers: self.headers + ) + + if !head.headers.contains(name: "host") { + let port = self.port + var host = self.host + if !(port == 80 && self.scheme == "http"), !(port == 443 && self.scheme == "https") { + host += ":\(port)" + } + head.headers.add(name: "host", value: host) + } + + let metadata = try head.headers.validate(method: self.method, body: self.body) + + return (head, metadata) + } } /// Represent HTTP response. @@ -877,46 +899,29 @@ extension TaskHandler: ChannelDuplexHandler { typealias OutboundOut = HTTPClientRequestPart func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) { - self.state = .sendingBodyWaitingResponseHead - let request = self.unwrapOutboundIn(data) + self.state = .sendingBodyWaitingResponseHead - var head = HTTPRequestHead(version: HTTPVersion(major: 1, minor: 1), - method: request.method, - uri: request.uri) - var headers = request.headers - - if !request.headers.contains(name: "host") { - let port = request.port - var host = request.host - if !(port == 80 && request.scheme == "http"), !(port == 443 && request.scheme == "https") { - host += ":\(port)" - } - headers.add(name: "host", value: host) - } + let head: HTTPRequestHead + let metadata: RequestFramingMetadata do { - try headers.validate(method: request.method, body: request.body) + (head, metadata) = try request.createRequestHead() } catch { self.errorCaught(context: context, error: error) promise?.fail(error) return } - head.headers = headers - - if head.headers[canonicalForm: "connection"].map({ $0.lowercased() }).contains("close") { - self.closing = true - } // This assert can go away when (if ever!) the above `if` correctly handles other HTTP versions. For example // in HTTP/1.0, we need to treat the absence of a 'connection: keep-alive' as a close too. - assert(head.version == HTTPVersion(major: 1, minor: 1), + assert(head.version == .http1_1, "Sending a request in HTTP version \(head.version) which is unsupported by the above `if`") - let contentLengths = head.headers[canonicalForm: "content-length"] - assert(contentLengths.count <= 1) - - self.expectedBodyLength = contentLengths.first.flatMap { Int($0) } + if case .fixedSize(let length) = metadata.body { + self.expectedBodyLength = length + } + self.closing = metadata.connectionClose context.write(wrapOutboundOut(.head(head))).map { self.callOutToDelegateFireAndForget(value: head, self.delegate.didSendRequestHead) diff --git a/Sources/AsyncHTTPClient/RequestBag.swift b/Sources/AsyncHTTPClient/RequestBag.swift index 222a1472b..94a02edeb 100644 --- a/Sources/AsyncHTTPClient/RequestBag.swift +++ b/Sources/AsyncHTTPClient/RequestBag.swift @@ -41,6 +41,7 @@ final class RequestBag { let idleReadTimeout: TimeAmount? let requestHead: HTTPRequestHead + let requestFramingMetadata: RequestFramingMetadata let eventLoopPreference: HTTPClient.EventLoopPreference @@ -50,7 +51,7 @@ final class RequestBag { redirectHandler: RedirectHandler?, connectionDeadline: NIODeadline, idleReadTimeout: TimeAmount?, - delegate: Delegate) { + delegate: Delegate) throws { self.eventLoopPreference = eventLoopPreference self.task = task self.state = .init(redirectHandler: redirectHandler) @@ -59,12 +60,9 @@ final class RequestBag { self.idleReadTimeout = idleReadTimeout self.delegate = delegate - self.requestHead = HTTPRequestHead( - version: .http1_1, - method: request.method, - uri: request.uri, - headers: request.headers - ) + let (head, metadata) = try request.createRequestHead() + self.requestHead = head + self.requestFramingMetadata = metadata // TODO: comment in once we switch to using the Request bag in AHC // self.task.taskDelegate = self diff --git a/Sources/AsyncHTTPClient/RequestValidation.swift b/Sources/AsyncHTTPClient/RequestValidation.swift index 4c8fd9d21..3f2e2e683 100644 --- a/Sources/AsyncHTTPClient/RequestValidation.swift +++ b/Sources/AsyncHTTPClient/RequestValidation.swift @@ -16,7 +16,13 @@ import NIO import NIOHTTP1 extension HTTPHeaders { - mutating func validate(method: HTTPMethod, body: HTTPClient.Body?) throws { + mutating func validate(method: HTTPMethod, body: HTTPClient.Body?) throws -> RequestFramingMetadata { + var metadata = RequestFramingMetadata(connectionClose: false, body: .none) + + if self[canonicalForm: "connection"].lazy.map({ $0.lowercased() }).contains("close") { + metadata.connectionClose = true + } + // validate transfer encoding and content length (https://tools.ietf.org/html/rfc7230#section-3.3.1) if self.contains(name: "Transfer-Encoding"), self.contains(name: "Content-Length") { throw HTTPClientError.incompatibleHeaders @@ -43,13 +49,13 @@ extension HTTPHeaders { // A user agent SHOULD NOT send a Content-Length header field when the request // message does not contain a payload body and the method semantics do not // anticipate such a body. - return + return metadata default: // A user agent SHOULD send a Content-Length in a request message when // no Transfer-Encoding is sent and the request method defines a meaning // for an enclosed payload body. self.add(name: "Content-Length", value: "0") - return + return metadata } } @@ -85,14 +91,18 @@ extension HTTPHeaders { // add headers if required if let enc = transferEncoding { self.add(name: "Transfer-Encoding", value: enc) + metadata.body = .stream } else if let length = contentLength { // A sender MUST NOT send a Content-Length header field in any message // that contains a Transfer-Encoding header field. self.add(name: "Content-Length", value: String(length)) + metadata.body = .fixedSize(length) } + + return metadata } - func validateFieldNames() throws { + private func validateFieldNames() throws { let invalidFieldNames = self.compactMap { (name, _) -> String? in let satisfy = name.utf8.allSatisfy { (char) -> Bool in switch char { diff --git a/Tests/AsyncHTTPClientTests/RequestBagTests.swift b/Tests/AsyncHTTPClientTests/RequestBagTests.swift index 818c989e7..80365e5db 100644 --- a/Tests/AsyncHTTPClientTests/RequestBagTests.swift +++ b/Tests/AsyncHTTPClientTests/RequestBagTests.swift @@ -53,9 +53,10 @@ final class RequestBagTests: XCTestCase { var maybeRequest: HTTPClient.Request? XCTAssertNoThrow(maybeRequest = try HTTPClient.Request(url: "https://swift.org", method: .POST, body: requestBody)) guard let request = maybeRequest else { return XCTFail("Expected to have a request") } - let delegate = UploadCountingDelegate(eventLoop: embeddedEventLoop) - let bag = RequestBag( + + var maybeRequestBag: RequestBag? + XCTAssertNoThrow(maybeRequestBag = try RequestBag( request: request, eventLoopPreference: .delegate(on: embeddedEventLoop), task: .init(eventLoop: embeddedEventLoop, logger: logger), @@ -63,7 +64,9 @@ final class RequestBagTests: XCTestCase { connectionDeadline: .now() + .seconds(30), idleReadTimeout: nil, delegate: delegate - ) + )) + guard let bag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag.") } + XCTAssert(bag.task.eventLoop === embeddedEventLoop) let executor = MockRequestExecutor(pauseRequestBodyPartStreamAfterASingleWrite: true) @@ -161,7 +164,8 @@ final class RequestBagTests: XCTestCase { guard let request = maybeRequest else { return XCTFail("Expected to have a request") } let delegate = UploadCountingDelegate(eventLoop: embeddedEventLoop) - let bag = RequestBag( + var maybeRequestBag: RequestBag? + XCTAssertNoThrow(maybeRequestBag = try RequestBag( request: request, eventLoopPreference: .delegate(on: embeddedEventLoop), task: .init(eventLoop: embeddedEventLoop, logger: logger), @@ -169,7 +173,8 @@ final class RequestBagTests: XCTestCase { connectionDeadline: .now() + .seconds(30), idleReadTimeout: nil, delegate: delegate - ) + )) + guard let bag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag.") } XCTAssert(bag.task.eventLoop === embeddedEventLoop) let executor = MockRequestExecutor() @@ -202,7 +207,8 @@ final class RequestBagTests: XCTestCase { guard let request = maybeRequest else { return XCTFail("Expected to have a request") } let delegate = UploadCountingDelegate(eventLoop: embeddedEventLoop) - let bag = RequestBag( + var maybeRequestBag: RequestBag? + XCTAssertNoThrow(maybeRequestBag = try RequestBag( request: request, eventLoopPreference: .delegate(on: embeddedEventLoop), task: .init(eventLoop: embeddedEventLoop, logger: logger), @@ -210,7 +216,8 @@ final class RequestBagTests: XCTestCase { connectionDeadline: .now() + .seconds(30), idleReadTimeout: nil, delegate: delegate - ) + )) + guard let bag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag.") } XCTAssert(bag.eventLoop === embeddedEventLoop) let executor = MockRequestExecutor() @@ -233,7 +240,8 @@ final class RequestBagTests: XCTestCase { guard let request = maybeRequest else { return XCTFail("Expected to have a request") } let delegate = UploadCountingDelegate(eventLoop: embeddedEventLoop) - let bag = RequestBag( + var maybeRequestBag: RequestBag? + XCTAssertNoThrow(maybeRequestBag = try RequestBag( request: request, eventLoopPreference: .delegate(on: embeddedEventLoop), task: .init(eventLoop: embeddedEventLoop, logger: logger), @@ -241,7 +249,8 @@ final class RequestBagTests: XCTestCase { connectionDeadline: .now() + .seconds(30), idleReadTimeout: nil, delegate: delegate - ) + )) + guard let bag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag.") } XCTAssert(bag.eventLoop === embeddedEventLoop) let executor = MockRequestExecutor() @@ -273,7 +282,8 @@ final class RequestBagTests: XCTestCase { guard let request = maybeRequest else { return XCTFail("Expected to have a request") } let delegate = UploadCountingDelegate(eventLoop: embeddedEventLoop) - let bag = RequestBag( + var maybeRequestBag: RequestBag? + XCTAssertNoThrow(maybeRequestBag = try RequestBag( request: request, eventLoopPreference: .delegate(on: embeddedEventLoop), task: .init(eventLoop: embeddedEventLoop, logger: logger), @@ -281,7 +291,8 @@ final class RequestBagTests: XCTestCase { connectionDeadline: .now() + .seconds(30), idleReadTimeout: nil, delegate: delegate - ) + )) + guard let bag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag.") } let queuer = MockTaskQueuer() bag.requestWasQueued(queuer) @@ -328,7 +339,8 @@ final class RequestBagTests: XCTestCase { guard let request = maybeRequest else { return XCTFail("Expected to have a request") } let delegate = UploadCountingDelegate(eventLoop: embeddedEventLoop) - let bag = RequestBag( + var maybeRequestBag: RequestBag? + XCTAssertNoThrow(maybeRequestBag = try RequestBag( request: request, eventLoopPreference: .delegate(on: embeddedEventLoop), task: .init(eventLoop: embeddedEventLoop, logger: logger), @@ -336,7 +348,8 @@ final class RequestBagTests: XCTestCase { connectionDeadline: .now() + .seconds(30), idleReadTimeout: nil, delegate: delegate - ) + )) + guard let bag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag.") } let executor = MockRequestExecutor() bag.willExecuteRequest(executor) diff --git a/Tests/AsyncHTTPClientTests/RequestValidationTests+XCTest.swift b/Tests/AsyncHTTPClientTests/RequestValidationTests+XCTest.swift index 08d4cfb32..7a21dffdd 100644 --- a/Tests/AsyncHTTPClientTests/RequestValidationTests+XCTest.swift +++ b/Tests/AsyncHTTPClientTests/RequestValidationTests+XCTest.swift @@ -32,6 +32,8 @@ extension RequestValidationTests { ("testGET_HEAD_DELETE_CONNECTRequestCanHaveBody", testGET_HEAD_DELETE_CONNECTRequestCanHaveBody), ("testInvalidHeaderFieldNames", testInvalidHeaderFieldNames), ("testValidHeaderFieldNames", testValidHeaderFieldNames), + ("testMetadataDetectConnectionClose", testMetadataDetectConnectionClose), + ("testMetadataDefaultIsConnectionCloseIsFalse", testMetadataDefaultIsConnectionCloseIsFalse), ("testNoHeadersNoBody", testNoHeadersNoBody), ("testNoHeadersHasBody", testNoHeadersHasBody), ("testContentLengthHeaderNoBody", testContentLengthHeaderNoBody), diff --git a/Tests/AsyncHTTPClientTests/RequestValidationTests.swift b/Tests/AsyncHTTPClientTests/RequestValidationTests.swift index 3e34f905c..ed3f28ce9 100644 --- a/Tests/AsyncHTTPClientTests/RequestValidationTests.swift +++ b/Tests/AsyncHTTPClientTests/RequestValidationTests.swift @@ -20,26 +20,34 @@ import XCTest class RequestValidationTests: XCTestCase { func testContentLengthHeaderIsRemovedFromGETIfNoBody() { var headers = HTTPHeaders([("Content-Length", "0")]) - XCTAssertNoThrow(try headers.validate(method: .GET, body: .none)) + var metadata: RequestFramingMetadata? + XCTAssertNoThrow(metadata = try headers.validate(method: .GET, body: .none)) XCTAssertNil(headers.first(name: "Content-Length")) + XCTAssertEqual(metadata?.body, .some(.none)) } func testContentLengthHeaderIsAddedToPOSTAndPUTWithNoBody() { var putHeaders = HTTPHeaders() - XCTAssertNoThrow(try putHeaders.validate(method: .PUT, body: .none)) + var putMetadata: RequestFramingMetadata? + XCTAssertNoThrow(putMetadata = try putHeaders.validate(method: .PUT, body: .none)) XCTAssertEqual(putHeaders.first(name: "Content-Length"), "0") + XCTAssertEqual(putMetadata?.body, .some(.none)) var postHeaders = HTTPHeaders() - XCTAssertNoThrow(try postHeaders.validate(method: .POST, body: .none)) + var postMetadata: RequestFramingMetadata? + XCTAssertNoThrow(postMetadata = try postHeaders.validate(method: .POST, body: .none)) XCTAssertEqual(postHeaders.first(name: "Content-Length"), "0") + XCTAssertEqual(postMetadata?.body, .some(.none)) } func testContentLengthHeaderIsChangedIfBodyHasDifferentLength() { var headers = HTTPHeaders([("Content-Length", "0")]) var buffer = ByteBufferAllocator().buffer(capacity: 200) buffer.writeBytes([UInt8](repeating: 12, count: 200)) - XCTAssertNoThrow(try headers.validate(method: .PUT, body: .byteBuffer(buffer))) + var metadata: RequestFramingMetadata? + XCTAssertNoThrow(metadata = try headers.validate(method: .PUT, body: .byteBuffer(buffer))) XCTAssertEqual(headers.first(name: "Content-Length"), "200") + XCTAssertEqual(metadata?.body, .fixedSize(200)) } func testTRACERequestMustNotHaveBody() { @@ -87,6 +95,22 @@ class RequestValidationTests: XCTestCase { XCTAssertNoThrow(try headers.validate(method: .GET, body: nil)) } + func testMetadataDetectConnectionClose() { + var headers = HTTPHeaders([ + ("Connection", "close"), + ]) + var metadata: RequestFramingMetadata? + XCTAssertNoThrow(metadata = try headers.validate(method: .GET, body: nil)) + XCTAssertEqual(metadata?.connectionClose, true) + } + + func testMetadataDefaultIsConnectionCloseIsFalse() { + var headers = HTTPHeaders([]) + var metadata: RequestFramingMetadata? + XCTAssertNoThrow(metadata = try headers.validate(method: .GET, body: nil)) + XCTAssertEqual(metadata?.connectionClose, false) + } + // MARK: - Content-Length/Transfer-Encoding Matrix // Method kind User sets Body Expectation @@ -96,16 +120,20 @@ class RequestValidationTests: XCTestCase { func testNoHeadersNoBody() throws { for method: HTTPMethod in [.GET, .HEAD, .DELETE, .CONNECT, .TRACE] { var headers: HTTPHeaders = .init() - XCTAssertNoThrow(try headers.validate(method: method, body: nil)) + var metadata: RequestFramingMetadata? + XCTAssertNoThrow(metadata = try headers.validate(method: method, body: nil)) XCTAssertTrue(headers["content-length"].isEmpty) XCTAssertTrue(headers["transfer-encoding"].isEmpty) + XCTAssertEqual(metadata?.body, .some(.none)) } for method: HTTPMethod in [.POST, .PUT] { var headers: HTTPHeaders = .init() - XCTAssertNoThrow(try headers.validate(method: method, body: nil)) + var metadata: RequestFramingMetadata? + XCTAssertNoThrow(metadata = try headers.validate(method: method, body: nil)) XCTAssertEqual(headers["content-length"].first, "0") XCTAssertFalse(headers["transfer-encoding"].contains("chunked")) + XCTAssertEqual(metadata?.body, .some(.none)) } } @@ -117,9 +145,11 @@ class RequestValidationTests: XCTestCase { // Body length is known for method: HTTPMethod in [.GET, .HEAD, .DELETE, .CONNECT] { var headers: HTTPHeaders = .init() - XCTAssertNoThrow(try headers.validate(method: method, body: .byteBuffer(ByteBuffer(bytes: [0])))) + var metadata: RequestFramingMetadata? + XCTAssertNoThrow(metadata = try headers.validate(method: method, body: .byteBuffer(ByteBuffer(bytes: [0])))) XCTAssertEqual(headers["content-length"].first, "1") XCTAssertTrue(headers["transfer-encoding"].isEmpty) + XCTAssertEqual(metadata?.body, .fixedSize(1)) } // Body length is _not_ known @@ -128,17 +158,21 @@ class RequestValidationTests: XCTestCase { let body: HTTPClient.Body = .stream { writer in writer.write(.byteBuffer(ByteBuffer(bytes: [0]))) } - XCTAssertNoThrow(try headers.validate(method: method, body: body)) + var metadata: RequestFramingMetadata? + XCTAssertNoThrow(metadata = try headers.validate(method: method, body: body)) XCTAssertTrue(headers["content-length"].isEmpty) XCTAssertTrue(headers["transfer-encoding"].contains("chunked")) + XCTAssertEqual(metadata?.body, .stream) } // Body length is known for method: HTTPMethod in [.POST, .PUT] { var headers: HTTPHeaders = .init() - XCTAssertNoThrow(try headers.validate(method: method, body: .byteBuffer(ByteBuffer(bytes: [0])))) + var metadata: RequestFramingMetadata? + XCTAssertNoThrow(metadata = try headers.validate(method: method, body: .byteBuffer(ByteBuffer(bytes: [0])))) XCTAssertEqual(headers["content-length"].first, "1") XCTAssertTrue(headers["transfer-encoding"].isEmpty) + XCTAssertEqual(metadata?.body, .fixedSize(1)) } // Body length is _not_ known @@ -147,9 +181,11 @@ class RequestValidationTests: XCTestCase { let body: HTTPClient.Body = .stream { writer in writer.write(.byteBuffer(ByteBuffer(bytes: [0]))) } - XCTAssertNoThrow(try headers.validate(method: method, body: body)) + var metadata: RequestFramingMetadata? + XCTAssertNoThrow(metadata = try headers.validate(method: method, body: body)) XCTAssertTrue(headers["content-length"].isEmpty) XCTAssertTrue(headers["transfer-encoding"].contains("chunked")) + XCTAssertEqual(metadata?.body, .stream) } } @@ -160,16 +196,20 @@ class RequestValidationTests: XCTestCase { func testContentLengthHeaderNoBody() throws { for method: HTTPMethod in [.GET, .HEAD, .DELETE, .CONNECT, .TRACE] { var headers: HTTPHeaders = .init([("Content-Length", "1")]) - XCTAssertNoThrow(try headers.validate(method: method, body: nil)) + var metadata: RequestFramingMetadata? + XCTAssertNoThrow(metadata = try headers.validate(method: method, body: nil)) XCTAssertTrue(headers["content-length"].isEmpty) XCTAssertTrue(headers["transfer-encoding"].isEmpty) + XCTAssertEqual(metadata?.body, .some(.none)) } for method: HTTPMethod in [.POST, .PUT] { var headers: HTTPHeaders = .init([("Content-Length", "1")]) - XCTAssertNoThrow(try headers.validate(method: method, body: nil)) + var metadata: RequestFramingMetadata? + XCTAssertNoThrow(metadata = try headers.validate(method: method, body: nil)) XCTAssertEqual(headers["content-length"].first, "0") XCTAssertTrue(headers["transfer-encoding"].isEmpty) + XCTAssertEqual(metadata?.body, .some(.none)) } } @@ -180,16 +220,20 @@ class RequestValidationTests: XCTestCase { func testContentLengthHeaderHasBody() throws { for method: HTTPMethod in [.GET, .HEAD, .DELETE, .CONNECT] { var headers: HTTPHeaders = .init([("Content-Length", "1")]) - XCTAssertNoThrow(try headers.validate(method: method, body: .byteBuffer(ByteBuffer(bytes: [0])))) + var metadata: RequestFramingMetadata? + XCTAssertNoThrow(metadata = try headers.validate(method: method, body: .byteBuffer(ByteBuffer(bytes: [0])))) XCTAssertEqual(headers["content-length"].first, "1") XCTAssertTrue(headers["transfer-encoding"].isEmpty) + XCTAssertEqual(metadata?.body, .fixedSize(1)) } for method: HTTPMethod in [.POST, .PUT] { var headers: HTTPHeaders = .init([("Content-Length", "1")]) - XCTAssertNoThrow(try headers.validate(method: method, body: .byteBuffer(ByteBuffer(bytes: [0])))) + var metadata: RequestFramingMetadata? + XCTAssertNoThrow(metadata = try headers.validate(method: method, body: .byteBuffer(ByteBuffer(bytes: [0])))) XCTAssertEqual(headers["content-length"].first, "1") XCTAssertTrue(headers["transfer-encoding"].isEmpty) + XCTAssertEqual(metadata?.body, .fixedSize(1)) } } @@ -200,16 +244,20 @@ class RequestValidationTests: XCTestCase { func testTransferEncodingHeaderNoBody() throws { for method: HTTPMethod in [.GET, .HEAD, .DELETE, .CONNECT, .TRACE] { var headers: HTTPHeaders = .init([("Transfer-Encoding", "chunked")]) - XCTAssertNoThrow(try headers.validate(method: method, body: nil)) + var metadata: RequestFramingMetadata? + XCTAssertNoThrow(metadata = try headers.validate(method: method, body: nil)) XCTAssertTrue(headers["content-length"].isEmpty) XCTAssertFalse(headers["transfer-encoding"].contains("chunked")) + XCTAssertEqual(metadata?.body, .some(.none)) } for method: HTTPMethod in [.POST, .PUT] { var headers: HTTPHeaders = .init([("Transfer-Encoding", "chunked")]) - XCTAssertNoThrow(try headers.validate(method: method, body: nil)) + var metadata: RequestFramingMetadata? + XCTAssertNoThrow(metadata = try headers.validate(method: method, body: nil)) XCTAssertEqual(headers["content-length"].first, "0") XCTAssertFalse(headers["transfer-encoding"].contains("chunked")) + XCTAssertEqual(metadata?.body, .some(.none)) } } @@ -220,16 +268,20 @@ class RequestValidationTests: XCTestCase { func testTransferEncodingHeaderHasBody() throws { for method: HTTPMethod in [.GET, .HEAD, .DELETE, .CONNECT] { var headers: HTTPHeaders = .init([("Transfer-Encoding", "chunked")]) - XCTAssertNoThrow(try headers.validate(method: method, body: .byteBuffer(ByteBuffer(bytes: [0])))) + var metadata: RequestFramingMetadata? + XCTAssertNoThrow(metadata = try headers.validate(method: method, body: .byteBuffer(ByteBuffer(bytes: [0])))) XCTAssertTrue(headers["content-length"].isEmpty) XCTAssertTrue(headers["transfer-encoding"].contains("chunked")) + XCTAssertEqual(metadata?.body, .stream) } for method: HTTPMethod in [.POST, .PUT] { var headers: HTTPHeaders = .init([("Transfer-Encoding", "chunked")]) - XCTAssertNoThrow(try headers.validate(method: method, body: .byteBuffer(ByteBuffer(bytes: [0])))) + var metadata: RequestFramingMetadata? + XCTAssertNoThrow(metadata = try headers.validate(method: method, body: .byteBuffer(ByteBuffer(bytes: [0])))) XCTAssertTrue(headers["content-length"].isEmpty) XCTAssertTrue(headers["transfer-encoding"].contains("chunked")) + XCTAssertEqual(metadata?.body, .stream) } }