diff --git a/FlyingFox/Sources/HTTPDecoder.swift b/FlyingFox/Sources/HTTPDecoder.swift index e47959b6..06d6834c 100644 --- a/FlyingFox/Sources/HTTPDecoder.swift +++ b/FlyingFox/Sources/HTTPDecoder.swift @@ -88,7 +88,7 @@ struct HTTPDecoder { } static func makeComponents(from comps: URLComponents?) -> (path: String, query: [HTTPRequest.QueryItem]) { - let path = (comps?.path).flatMap { URL(string: $0)?.standardized.path } ?? "" + let path = (comps?.percentEncodedPath).flatMap { URL(string: $0)?.standardized.path } ?? "" let query = comps?.queryItems?.map { HTTPRequest.QueryItem(name: $0.name, value: $0.value ?? "") } diff --git a/FlyingFox/Sources/HTTPRoute.swift b/FlyingFox/Sources/HTTPRoute.swift index a550a664..6ea7d024 100644 --- a/FlyingFox/Sources/HTTPRoute.swift +++ b/FlyingFox/Sources/HTTPRoute.swift @@ -49,7 +49,8 @@ public struct HTTPRoute: Sendable { init(method: String, path: String, headers: [HTTPHeader: String], body: HTTPBodyPattern?) { self.method = Component(method) - let comps = HTTPDecoder.readComponents(from: path) + + let comps = HTTPRoute.readComponents(from: path) self.path = comps.path .split(separator: "/", omittingEmptySubsequences: true) .map { Component(String($0)) } @@ -167,8 +168,8 @@ public extension HTTPRoute { } private static func components(for target: String) -> (method: String, path: String) { - let comps = target.split(separator: " ", maxSplits: 2, omittingEmptySubsequences: true) - guard comps.count > 1 else { + let comps = target.split(separator: " ", maxSplits: 1, omittingEmptySubsequences: true) + guard comps.count > 1 && !comps[0].hasPrefix("/") else { return (method: "*", path: target) } return (method: String(comps[0]), path: String(comps[1])) @@ -184,6 +185,18 @@ public extension HTTPRoute { } } +private extension HTTPRoute { + + static func readComponents(from path: String) -> (path: String, query: [HTTPRequest.QueryItem]) { + guard path.removingPercentEncoding == path else { + return HTTPDecoder.readComponents(from: path) + } + + let escaped = path.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) + return HTTPDecoder.readComponents(from: escaped ?? path) + } +} + extension HTTPRoute: ExpressibleByStringLiteral { public init(stringLiteral value: String) { diff --git a/FlyingFox/Tests/HTTPDecoderTests.swift b/FlyingFox/Tests/HTTPDecoderTests.swift index 146b9a68..0e74ecc6 100644 --- a/FlyingFox/Tests/HTTPDecoderTests.swift +++ b/FlyingFox/Tests/HTTPDecoderTests.swift @@ -218,6 +218,28 @@ final class HTTPDecoderTests: XCTestCase { ) } + func testPercentEncodedPathDecodes() { + XCTAssertEqual( + HTTPDecoder.readComponents(from: "/fish%20chips").path, + "/fish chips" + ) + XCTAssertEqual( + HTTPDecoder.readComponents(from: "/ocean/fish%20and%20chips").path, + "/ocean/fish and chips" + ) + } + + func testPercentQueryStringDecodes() { + XCTAssertEqual( + HTTPDecoder.readComponents(from: "/?fish=%F0%9F%90%9F").query, + [.init(name: "fish", value: "🐟")] + ) + XCTAssertEqual( + HTTPDecoder.readComponents(from: "?%F0%9F%90%A1=chips").query, + [.init(name: "🐡", value: "chips")] + ) + } + func testEmptyQueryItem_Decodes() { var urlComps = URLComponents() urlComps.queryItems = [.init(name: "name", value: nil)] diff --git a/FlyingFox/Tests/HTTPRouteTests.swift b/FlyingFox/Tests/HTTPRouteTests.swift index f60a051f..b86e1e14 100644 --- a/FlyingFox/Tests/HTTPRouteTests.swift +++ b/FlyingFox/Tests/HTTPRouteTests.swift @@ -62,6 +62,39 @@ final class HTTPRouteTests: XCTestCase { ) } + func testPercentEncodedPathComponents() { + XCTAssertEqual( + HTTPRoute("GET /hello world").path, + [.caseInsensitive("hello world")] + ) + + XCTAssertEqual( + HTTPRoute("/hello%20world").path, + [.caseInsensitive("hello world")] + ) + + XCTAssertEqual( + HTTPRoute("🐡/*").path, + [.caseInsensitive("🐡"), .wildcard] + ) + + XCTAssertEqual( + HTTPRoute("%F0%9F%90%A1/*").path, + [.caseInsensitive("🐡"), .wildcard] + ) + } + + func testPercentEncodedQueryItems() { + XCTAssertEqual( + HTTPRoute("/?fish=%F0%9F%90%9F").query, + [.init(name: "fish", value: .caseInsensitive("🐟"))] + ) + XCTAssertEqual( + HTTPRoute("/?%F0%9F%90%A1=chips").query, + [.init(name: "🐡", value: .caseInsensitive("chips"))] + ) + } + func testMethod() { XCTAssertEqual( HTTPRoute("hello/world").method,