From 7393833feb017aaff0785107493f10c8bb51cb29 Mon Sep 17 00:00:00 2001 From: Simon Whitty Date: Sat, 13 Sep 2025 16:35:55 +1000 Subject: [PATCH] multiple HTTPHeaders --- FlyingFox/Sources/HTTPEncoder.swift | 15 ++- FlyingFox/Sources/HTTPHeader.swift | 22 +--- FlyingFox/Sources/HTTPHeaders.swift | 116 ++++++++++++++++++ FlyingFox/Sources/HTTPRequest.swift | 27 +++- FlyingFox/Sources/HTTPResponse.swift | 44 ++++++- FlyingFox/Sources/HTTPRoute.swift | 2 +- .../Sources/Handlers/FileHTTPHandler.swift | 2 +- .../Handlers/WebSocketHTTPHandler.swift | 2 +- FlyingFox/Tests/HTTPEncoderTests.swift | 22 ++++ FlyingFox/Tests/HTTPHeaderTests.swift | 16 ++- FlyingFox/Tests/HTTPRequest+Mock.swift | 4 +- FlyingFox/Tests/HTTPResponse+Mock.swift | 6 +- FlyingFox/Tests/HTTPRouteTests.swift | 15 +-- .../Tests/Handlers/HTTPHandlerTests.swift | 2 +- .../Handlers/WebSocketHTTPHandlerTests.swift | 6 +- FlyingFox/XCTests/HTTPHeaderTests.swift | 2 +- FlyingFox/XCTests/HTTPRequest+Mock.swift | 10 +- FlyingFox/XCTests/HTTPRouteTests.swift | 12 +- .../Handlers/WebSocketHTTPHandlerTests.swift | 6 +- 19 files changed, 267 insertions(+), 64 deletions(-) create mode 100644 FlyingFox/Sources/HTTPHeaders.swift diff --git a/FlyingFox/Sources/HTTPEncoder.swift b/FlyingFox/Sources/HTTPEncoder.swift index 29726556..06258e8b 100644 --- a/FlyingFox/Sources/HTTPEncoder.swift +++ b/FlyingFox/Sources/HTTPEncoder.swift @@ -46,8 +46,19 @@ struct HTTPEncoder { httpHeaders.addValue(encoding, for: .transferEncoding) } - let headers = httpHeaders.map { "\($0.key.rawValue): \($0.value)" } - + var headers = [String]() + + for header in httpHeaders.storage.keys.sorted(by: { $0.rawValue < $1.rawValue }) { + let values = httpHeaders.storage[header]! + if HTTPHeaders.canCombineValues(for: header) { + let joinedValues = values.joined(separator: ", ") + headers.append("\(header.rawValue): \(joinedValues)") + } else { + headers.append( + contentsOf: values.map { "\(header.rawValue): \($0)" } + ) + } + } return [status] + headers + ["\r\n"] } diff --git a/FlyingFox/Sources/HTTPHeader.swift b/FlyingFox/Sources/HTTPHeader.swift index f2e81f5d..194dd4ee 100644 --- a/FlyingFox/Sources/HTTPHeader.swift +++ b/FlyingFox/Sources/HTTPHeader.swift @@ -52,15 +52,19 @@ public struct HTTPHeader: Sendable, RawRepresentable, Hashable { public extension HTTPHeader { static let acceptRanges = HTTPHeader("Accept-Ranges") static let authorization = HTTPHeader("Authorization") + static let cookie = HTTPHeader("Cookie") static let connection = HTTPHeader("Connection") static let contentDisposition = HTTPHeader("Content-Disposition") static let contentEncoding = HTTPHeader("Content-Encoding") static let contentLength = HTTPHeader("Content-Length") static let contentRange = HTTPHeader("Content-Range") static let contentType = HTTPHeader("Content-Type") + static let date = HTTPHeader("Date") + static let eTag = HTTPHeader("ETag") static let host = HTTPHeader("Host") static let location = HTTPHeader("Location") static let range = HTTPHeader("Range") + static let setCookie = HTTPHeader("Set-Cookie") static let transferEncoding = HTTPHeader("Transfer-Encoding") static let upgrade = HTTPHeader("Upgrade") static let webSocketAccept = HTTPHeader("Sec-WebSocket-Accept") @@ -68,21 +72,3 @@ public extension HTTPHeader { static let webSocketVersion = HTTPHeader("Sec-WebSocket-Version") static let xForwardedFor = HTTPHeader("X-Forwarded-For") } - -public extension [HTTPHeader: String] { - - func values(for header: HTTPHeader) -> [String] { - let value = self[header] ?? "" - return value - .split(separator: ",", omittingEmptySubsequences: true) - .map { String($0.trimmingCharacters(in: .whitespaces)) } - } - - mutating func setValues(_ values: [String], for header: HTTPHeader) { - self[header] = values.joined(separator: ", ") - } - - mutating func addValue(_ value: String, for header: HTTPHeader) { - setValues(values(for: header) + [value], for: header) - } -} diff --git a/FlyingFox/Sources/HTTPHeaders.swift b/FlyingFox/Sources/HTTPHeaders.swift new file mode 100644 index 00000000..63b89494 --- /dev/null +++ b/FlyingFox/Sources/HTTPHeaders.swift @@ -0,0 +1,116 @@ +// +// HTTPHeaders.swift +// FlyingFox +// +// Created by Simon Whitty on 13/09/2025. +// Copyright © 2025 Simon Whitty. All rights reserved. +// +// Distributed under the permissive MIT license +// Get the latest version from here: +// +// https://github.com/swhitty/FlyingFox +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +public struct HTTPHeaders: Hashable, Sendable, Sequence, ExpressibleByDictionaryLiteral { + var storage: [HTTPHeader: [String]] = [:] + + public init() { } + + public init(_ headers: [HTTPHeader: String]) { + self.storage = headers.mapValues { [$0] } + } + + public init(dictionaryLiteral elements: (HTTPHeader, String)...) { + for (header, value) in elements { + if HTTPHeaders.canCombineValues(for: header) { + let values = value + .split(separator: ",", omittingEmptySubsequences: true) + .map { String($0.trimmingCharacters(in: .whitespaces)) } + storage[header, default: []].append(contentsOf: values) + } else { + storage[header, default: []].append(value) + } + } + } + + public subscript(header: HTTPHeader) -> String? { + get { + guard let values = storage[header] else { return nil } + if HTTPHeaders.canCombineValues(for: header) { + return values.joined(separator: ", ") + } else { + return values.first + } + } + set { + if let newValue { + if storage[header] != nil { + storage[header]?[0] = newValue + } else { + storage[header] = [newValue] + } + } else { + storage.removeValue(forKey: header) + } + } + } + + public var keys: some Collection { + storage.keys + } + + public var values: some Collection<[String]> { + storage.values + } + + public func values(for header: HTTPHeader) -> [String] { + storage[header] ?? [] + } + + public mutating func addValue(_ value: String, for header: HTTPHeader) { + storage[header, default: []].append(value) + } + + public mutating func setValues(_ values: [String], for header: HTTPHeader) { + storage[header] = values + } + + public mutating func removeValue(_ header: HTTPHeader) { + storage.removeValue(forKey: header) + } + + public func makeIterator() -> some IteratorProtocol<(key: HTTPHeader, value: String)> { + storage.lazy + .flatMap { (key, values) in values.lazy.map { (key, $0) } } + .makeIterator() + } +} + +package extension HTTPHeaders { + + private static let singleValueHeaders: Set = [ + .cookie, .setCookie, .date, .eTag, .contentLength, .contentType, .authorization, .host + ] + + static func canCombineValues(for header: HTTPHeader) -> Bool { + !singleValueHeaders.contains(header) + } +} diff --git a/FlyingFox/Sources/HTTPRequest.swift b/FlyingFox/Sources/HTTPRequest.swift index d92b299c..9cf9333a 100644 --- a/FlyingFox/Sources/HTTPRequest.swift +++ b/FlyingFox/Sources/HTTPRequest.swift @@ -36,7 +36,7 @@ public struct HTTPRequest: Sendable { public var version: HTTPVersion public var path: String public var query: [QueryItem] - public var headers: [HTTPHeader: String] + public var headers: HTTPHeaders public var bodySequence: HTTPBodySequence public var remoteAddress: Address? @@ -62,7 +62,7 @@ public struct HTTPRequest: Sendable { version: HTTPVersion, path: String, query: [QueryItem], - headers: [HTTPHeader: String], + headers: HTTPHeaders, body: HTTPBodySequence, remoteAddress: Address? = nil) { self.method = method @@ -84,12 +84,33 @@ public struct HTTPRequest: Sendable { self.version = version self.path = path self.query = query - self.headers = headers + self.headers = HTTPHeaders(headers) self.bodySequence = HTTPBodySequence(data: body) self.remoteAddress = nil } } +@available(*, deprecated, message: "Use ``HTTPHeaders`` instead of [HTTPHeader: String]") +public extension HTTPRequest { + + init(method: HTTPMethod, + version: HTTPVersion, + path: String, + query: [QueryItem], + headers: [HTTPHeader: String], + body: HTTPBodySequence + ) { + self.init( + method: method, + version: version, + path: path, + query: query, + headers: HTTPHeaders(headers), + body: body + ) + } +} + extension HTTPRequest { var shouldKeepAlive: Bool { headers[.connection]?.caseInsensitiveCompare("keep-alive") == .orderedSame diff --git a/FlyingFox/Sources/HTTPResponse.swift b/FlyingFox/Sources/HTTPResponse.swift index 160b052b..dd96846d 100644 --- a/FlyingFox/Sources/HTTPResponse.swift +++ b/FlyingFox/Sources/HTTPResponse.swift @@ -34,7 +34,7 @@ import Foundation public struct HTTPResponse: Sendable { public var version: HTTPVersion public var statusCode: HTTPStatusCode - public var headers: [HTTPHeader: String] + public var headers: HTTPHeaders public var payload: Payload public enum Payload: @unchecked Sendable { @@ -65,7 +65,7 @@ public struct HTTPResponse: Sendable { public init(version: HTTPVersion = .http11, statusCode: HTTPStatusCode, - headers: [HTTPHeader: String] = [:], + headers: HTTPHeaders = [:], body: Data = Data()) { self.version = version self.statusCode = statusCode @@ -75,7 +75,7 @@ public struct HTTPResponse: Sendable { public init(version: HTTPVersion = .http11, statusCode: HTTPStatusCode, - headers: [HTTPHeader: String] = [:], + headers: HTTPHeaders = [:], body: HTTPBodySequence) { self.version = version self.statusCode = statusCode @@ -83,7 +83,7 @@ public struct HTTPResponse: Sendable { self.payload = .httpBody(body) } - public init(headers: [HTTPHeader: String] = [:], + public init(headers: HTTPHeaders = [:], webSocket handler: some WSHandler) { self.version = .http11 self.statusCode = .switchingProtocols @@ -92,6 +92,42 @@ public struct HTTPResponse: Sendable { } } +@available(*, deprecated, message: "Use ``HTTPHeaders`` instead of [HTTPHeader: String]") +public extension HTTPResponse { + + init( + version: HTTPVersion = .http11, + statusCode: HTTPStatusCode, + headers: [HTTPHeader: String], + body: Data = Data() + ) { + self.init( + version: version, + statusCode: statusCode, + headers: HTTPHeaders(headers), + body: body + ) + } + + init( + version: HTTPVersion = .http11, + statusCode: HTTPStatusCode, + headers: [HTTPHeader: String], + body: HTTPBodySequence + ) { + self.init( + version: version, + statusCode: statusCode, + headers: HTTPHeaders(headers), + body: body + ) + } + + init(headers: [HTTPHeader: String], webSocket handler: some WSHandler) { + self.init(headers: HTTPHeaders(headers), webSocket: handler) + } +} + extension HTTPResponse { var shouldKeepAlive: Bool { headers[.connection]?.caseInsensitiveCompare("keep-alive") == .orderedSame diff --git a/FlyingFox/Sources/HTTPRoute.swift b/FlyingFox/Sources/HTTPRoute.swift index ba970a86..efedf3d9 100644 --- a/FlyingFox/Sources/HTTPRoute.swift +++ b/FlyingFox/Sources/HTTPRoute.swift @@ -259,7 +259,7 @@ private extension HTTPRoute { return true } - func patternMatch(headers request: [HTTPHeader: String]) -> Bool { + func patternMatch(headers request: HTTPHeaders) -> Bool { return headers.allSatisfy { header, value in value ~= request.values(for: header) } diff --git a/FlyingFox/Sources/Handlers/FileHTTPHandler.swift b/FlyingFox/Sources/Handlers/FileHTTPHandler.swift index c15b8cf9..bc05064d 100644 --- a/FlyingFox/Sources/Handlers/FileHTTPHandler.swift +++ b/FlyingFox/Sources/Handlers/FileHTTPHandler.swift @@ -153,7 +153,7 @@ public struct FileHTTPHandler: HTTPHandler { } } - static func makePartialRange(for headers: [HTTPHeader: String], fileSize: Int) -> ClosedRange? { + static func makePartialRange(for headers: HTTPHeaders, fileSize: Int) -> ClosedRange? { guard let headerValue = headers[.range] else { return nil } let scanner = Scanner(string: headerValue) guard scanner.scanString("bytes") != nil, diff --git a/FlyingFox/Sources/Handlers/WebSocketHTTPHandler.swift b/FlyingFox/Sources/Handlers/WebSocketHTTPHandler.swift index 3f268556..c84e1bdd 100644 --- a/FlyingFox/Sources/Handlers/WebSocketHTTPHandler.swift +++ b/FlyingFox/Sources/Handlers/WebSocketHTTPHandler.swift @@ -77,7 +77,7 @@ public struct WebSocketHTTPHandler: HTTPHandler, Sendable { /// - Parameter headers: The headers of the request to verify. /// - Returns: The request's key. /// - Throws: An ``WSInvalidHandshakeError`` if the headers are invalid. - static func verifyHandshakeRequestHeaders(_ headers: [HTTPHeader: String]) throws -> String { + static func verifyHandshakeRequestHeaders(_ headers: HTTPHeaders) throws -> String { // Verify the headers according to RFC 6455 section 4.2.1 (https://datatracker.ietf.org/doc/html/rfc6455#section-4.2.1) // Rule 1 isn't verified because the socket method is specified elsewhere diff --git a/FlyingFox/Tests/HTTPEncoderTests.swift b/FlyingFox/Tests/HTTPEncoderTests.swift index c7a0fe00..9cd3ae68 100644 --- a/FlyingFox/Tests/HTTPEncoderTests.swift +++ b/FlyingFox/Tests/HTTPEncoderTests.swift @@ -150,6 +150,28 @@ struct HTTPEncoderTests { ) } + @Test + func encodesMultipleCookiesHeaders() async throws { + var headers = HTTPHeaders() + headers.addValue("Fish", for: .setCookie) + headers.addValue("Chips", for: .setCookie) + let data = try await HTTPEncoder.encodeResponse( + .make(headers: headers, body: Data()) + ) + + print(String(data: data, encoding: .utf8)!) + #expect( + String(data: data, encoding: .utf8) == """ + HTTP/1.1 200 OK\r + Content-Length: 0\r + Set-Cookie: Fish\r + Set-Cookie: Chips\r + \r + + """ + ) + } + @Test func encodesRequest() async throws { #expect( diff --git a/FlyingFox/Tests/HTTPHeaderTests.swift b/FlyingFox/Tests/HTTPHeaderTests.swift index d4924d6d..56c53394 100644 --- a/FlyingFox/Tests/HTTPHeaderTests.swift +++ b/FlyingFox/Tests/HTTPHeaderTests.swift @@ -1,5 +1,5 @@ // -// HTTPHeaderTests.swift +// HTTPHeadersTests.swift // FlyingFox // // Created by Simon Whitty on 11/07/2024. @@ -33,12 +33,12 @@ import Foundation import Testing -struct HTTPHeaderTests { +struct HTTPHeadersTests { @Test func stringValue() { // given - var headers = [HTTPHeader: String]() + var headers = HTTPHeaders() headers[.transferEncoding] = "Identity" #expect( @@ -63,4 +63,14 @@ struct HTTPHeaderTests { headers.values(for: .transferEncoding) == ["Identity", "chunked"] ) } + + @Test + func values() { + var headers = HTTPHeaders() + headers.addValue("Fish", for: .setCookie) + headers.addValue("Chips", for: .setCookie) + + #expect(headers[.setCookie] == "Fish") + #expect(headers.values(for: .setCookie) == ["Fish", "Chips"]) + } } diff --git a/FlyingFox/Tests/HTTPRequest+Mock.swift b/FlyingFox/Tests/HTTPRequest+Mock.swift index 5ee97d97..40c68fc0 100644 --- a/FlyingFox/Tests/HTTPRequest+Mock.swift +++ b/FlyingFox/Tests/HTTPRequest+Mock.swift @@ -37,7 +37,7 @@ extension HTTPRequest { version: HTTPVersion = .http11, path: String = "/", query: [QueryItem] = [], - headers: [HTTPHeader: String] = [:], + headers: HTTPHeaders = [:], body: Data = Data(), remoteAddress: Address? = nil) -> Self { HTTPRequest(method: method, @@ -49,7 +49,7 @@ extension HTTPRequest { remoteAddress: remoteAddress) } - static func make(method: HTTPMethod = .GET, _ url: String, headers: [HTTPHeader: String] = [:]) -> Self { + static func make(method: HTTPMethod = .GET, _ url: String, headers: HTTPHeaders = [:]) -> Self { let (path, query) = HTTPDecoder.make().readComponents(from: url.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!) return HTTPRequest.make( method: method, diff --git a/FlyingFox/Tests/HTTPResponse+Mock.swift b/FlyingFox/Tests/HTTPResponse+Mock.swift index 9975a0c2..8839400e 100644 --- a/FlyingFox/Tests/HTTPResponse+Mock.swift +++ b/FlyingFox/Tests/HTTPResponse+Mock.swift @@ -37,7 +37,7 @@ extension HTTPResponse { static func make(version: HTTPVersion = .http11, statusCode: HTTPStatusCode = .ok, - headers: [HTTPHeader: String] = [:], + headers: HTTPHeaders = [:], body: Data = Data()) -> Self { HTTPResponse(version: version, statusCode: statusCode, @@ -47,7 +47,7 @@ extension HTTPResponse { static func makeChunked(version: HTTPVersion = .http11, statusCode: HTTPStatusCode = .ok, - headers: [HTTPHeader: String] = [:], + headers: HTTPHeaders = [:], body: Data = Data(), chunkSize: Int = 5) -> Self { let consuming = ConsumingAsyncSequence(body) @@ -61,7 +61,7 @@ extension HTTPResponse { static func make(version: HTTPVersion = .http11, statusCode: HTTPStatusCode = .ok, - headers: [HTTPHeader: String] = [:], + headers: HTTPHeaders = [:], body: HTTPBodySequence) -> Self { HTTPResponse(version: version, statusCode: statusCode, diff --git a/FlyingFox/Tests/HTTPRouteTests.swift b/FlyingFox/Tests/HTTPRouteTests.swift index 9de6329f..ac0a45ad 100644 --- a/FlyingFox/Tests/HTTPRouteTests.swift +++ b/FlyingFox/Tests/HTTPRouteTests.swift @@ -359,31 +359,26 @@ struct HTTPRouteTests { @Test func header_MatchesRoute() async { - let route = HTTPRoute("GET /mock", headers: [.contentType: "json"]) + let route = HTTPRoute("GET /mock", headers: [.contentEncoding: "json"]) - #expect( - await route ~= HTTPRequest.make(method: .GET, - path: "/mock", - headers: [.contentType: "json"]) - ) #expect( await route ~= HTTPRequest.make(method: .GET, path: "/mock", - headers: [.contentType: "xml, json"]) + headers: [.contentEncoding: "xml, json"]) ) #expect( await route ~= HTTPRequest.make(method: .GET, path: "/mock", - headers: [.contentEncoding: "xml", - .contentType: "json"]) + headers: [.contentType: "xml", + .contentEncoding: "json"]) ) #expect( !(await route ~= HTTPRequest.make(method: .GET, path: "/mock", - headers: [.contentType: "xml"])) + headers: [.contentEncoding: "xml"])) ) } diff --git a/FlyingFox/Tests/Handlers/HTTPHandlerTests.swift b/FlyingFox/Tests/Handlers/HTTPHandlerTests.swift index ee7e363f..8130925a 100644 --- a/FlyingFox/Tests/Handlers/HTTPHandlerTests.swift +++ b/FlyingFox/Tests/Handlers/HTTPHandlerTests.swift @@ -330,7 +330,7 @@ struct HTTPHandlerTests { } private extension FileHTTPHandler { - static func makePartialRange(for headers: [HTTPHeader: String]) -> ClosedRange? { + static func makePartialRange(for headers: HTTPHeaders) -> ClosedRange? { makePartialRange(for: headers, fileSize: 10000) } } diff --git a/FlyingFox/Tests/Handlers/WebSocketHTTPHandlerTests.swift b/FlyingFox/Tests/Handlers/WebSocketHTTPHandlerTests.swift index 61ac78e5..a4873c96 100644 --- a/FlyingFox/Tests/Handlers/WebSocketHTTPHandlerTests.swift +++ b/FlyingFox/Tests/Handlers/WebSocketHTTPHandlerTests.swift @@ -69,7 +69,7 @@ struct WebSocketHTTPHandlerTests { let handler = WebSocketHTTPHandler.make() - let headers: [HTTPHeader: String] = [ + let headers: HTTPHeaders = [ .host: "localhost", .connection: "Upgrade", .upgrade: "websocket", @@ -179,14 +179,14 @@ struct WebSocketHTTPHandlerTests { } } -private extension Dictionary where Key == HTTPHeader, Value == String { +private extension HTTPHeaders { static func makeWSHeaders(host: String? = "localhost", connection: String? = "Upgrade", upgrade: String? = "websocket", webSocketKey: String? = "ABCDEFGHIJKLMNOP", webSocketVersion: String? = "13") -> Self { - var headers = [HTTPHeader: String] () + var headers = HTTPHeaders() headers[.host] = host headers[.connection] = connection headers[.upgrade] = upgrade diff --git a/FlyingFox/XCTests/HTTPHeaderTests.swift b/FlyingFox/XCTests/HTTPHeaderTests.swift index 5be330bf..7ab1c69a 100644 --- a/FlyingFox/XCTests/HTTPHeaderTests.swift +++ b/FlyingFox/XCTests/HTTPHeaderTests.swift @@ -37,7 +37,7 @@ final class HTTPHeaderTests: XCTestCase { func testStringValue() { // given - var headers = [HTTPHeader: String]() + var headers = HTTPHeaders() headers[.transferEncoding] = "Identity" XCTAssertEqual( diff --git a/FlyingFox/XCTests/HTTPRequest+Mock.swift b/FlyingFox/XCTests/HTTPRequest+Mock.swift index 69b80a00..40c68fc0 100644 --- a/FlyingFox/XCTests/HTTPRequest+Mock.swift +++ b/FlyingFox/XCTests/HTTPRequest+Mock.swift @@ -37,7 +37,7 @@ extension HTTPRequest { version: HTTPVersion = .http11, path: String = "/", query: [QueryItem] = [], - headers: [HTTPHeader: String] = [:], + headers: HTTPHeaders = [:], body: Data = Data(), remoteAddress: Address? = nil) -> Self { HTTPRequest(method: method, @@ -49,7 +49,7 @@ extension HTTPRequest { remoteAddress: remoteAddress) } - static func make(method: HTTPMethod = .GET, _ url: String, headers: [HTTPHeader: String] = [:]) -> Self { + static func make(method: HTTPMethod = .GET, _ url: String, headers: HTTPHeaders = [:]) -> Self { let (path, query) = HTTPDecoder.make().readComponents(from: url.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!) return HTTPRequest.make( method: method, @@ -58,6 +58,12 @@ extension HTTPRequest { headers: headers ) } + + var bodyString: String { + get async throws { + try await String(decoding: bodyData, as: UTF8.self) + } + } } extension HTTPDecoder { diff --git a/FlyingFox/XCTests/HTTPRouteTests.swift b/FlyingFox/XCTests/HTTPRouteTests.swift index aed92536..7167987f 100644 --- a/FlyingFox/XCTests/HTTPRouteTests.swift +++ b/FlyingFox/XCTests/HTTPRouteTests.swift @@ -328,31 +328,31 @@ final class HTTPRouteTests: XCTestCase { } func testHeader_MatchesRoute() async { - let route = HTTPRoute("GET /mock", headers: [.contentType: "json"]) + let route = HTTPRoute("GET /mock", headers: [.contentEncoding: "json"]) await AsyncAssertTrue( await route ~= HTTPRequest.make(method: .GET, path: "/mock", - headers: [.contentType: "json"]) + headers: [.contentEncoding: "json"]) ) await AsyncAssertTrue( await route ~= HTTPRequest.make(method: .GET, path: "/mock", - headers: [.contentType: "xml, json"]) + headers: [.contentEncoding: "xml, json"]) ) await AsyncAssertTrue( await route ~= HTTPRequest.make(method: .GET, path: "/mock", - headers: [.contentEncoding: "xml", - .contentType: "json"]) + headers: [.contentEncoding: "json", + .contentType: "xml"]) ) await AsyncAssertFalse( await route ~= HTTPRequest.make(method: .GET, path: "/mock", - headers: [.contentType: "xml"]) + headers: [.contentEncoding: "xml"]) ) } diff --git a/FlyingFox/XCTests/Handlers/WebSocketHTTPHandlerTests.swift b/FlyingFox/XCTests/Handlers/WebSocketHTTPHandlerTests.swift index 59a3b129..714d1cbf 100644 --- a/FlyingFox/XCTests/Handlers/WebSocketHTTPHandlerTests.swift +++ b/FlyingFox/XCTests/Handlers/WebSocketHTTPHandlerTests.swift @@ -70,7 +70,7 @@ final class WebSocketHTTPHandlerTests: XCTestCase { let handler = WebSocketHTTPHandler.make() - let headers: [HTTPHeader: String] = [ + let headers: HTTPHeaders = [ .host: "localhost", .connection: "Upgrade", .upgrade: "websocket", @@ -180,14 +180,14 @@ final class WebSocketHTTPHandlerTests: XCTestCase { } } -private extension Dictionary where Key == HTTPHeader, Value == String { +private extension HTTPHeaders { static func makeWSHeaders(host: String? = "localhost", connection: String? = "Upgrade", upgrade: String? = "websocket", webSocketKey: String? = "ABCDEFGHIJKLMNOP", webSocketVersion: String? = "13") -> Self { - var headers = [HTTPHeader: String] () + var headers = HTTPHeaders() headers[.host] = host headers[.connection] = connection headers[.upgrade] = upgrade