Skip to content

Commit

Permalink
Fix request and response translation (#51)
Browse files Browse the repository at this point in the history
  • Loading branch information
guoye-zhang authored Jul 3, 2024
1 parent 6b915d8 commit 50e1f4a
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 12 deletions.
68 changes: 60 additions & 8 deletions Sources/OpenAPIURLSession/URLSessionTransport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@ internal enum URLSessionTransportError: Error {
/// Returned `URLResponse` could not be converted to `HTTPURLResponse`.
case notHTTPResponse(URLResponse)

/// Returned `HTTPURLResponse` has an invalid status code
case invalidResponseStatusCode(HTTPURLResponse)

/// Returned `URLResponse` was nil
case noResponse(url: URL?)

Expand All @@ -162,14 +165,18 @@ extension HTTPResponse {
guard let httpResponse = urlResponse as? HTTPURLResponse else {
throw URLSessionTransportError.notHTTPResponse(urlResponse)
}
var headerFields = HTTPFields()
for (headerName, headerValue) in httpResponse.allHeaderFields {
guard let rawName = headerName as? String, let name = HTTPField.Name(rawName),
let value = headerValue as? String
else { continue }
headerFields[name] = value
guard (0...999).contains(httpResponse.statusCode) else {
throw URLSessionTransportError.invalidResponseStatusCode(httpResponse)
}
self.init(status: .init(code: httpResponse.statusCode))
if let fields = httpResponse.allHeaderFields as? [String: String] {
self.headerFields.reserveCapacity(fields.count)
for (name, value) in fields {
if let name = HTTPField.Name(name) {
self.headerFields.append(HTTPField(name: name, isoLatin1Value: value))
}
}
}
self.init(status: .init(code: httpResponse.statusCode), headerFields: headerFields)
}
}

Expand All @@ -193,7 +200,50 @@ extension URLRequest {
}
self.init(url: url)
self.httpMethod = request.method.rawValue
for header in request.headerFields { setValue(header.value, forHTTPHeaderField: header.name.canonicalName) }
var combinedFields = [HTTPField.Name: String](minimumCapacity: request.headerFields.count)
for field in request.headerFields {
if let existingValue = combinedFields[field.name] {
let separator = field.name == .cookie ? "; " : ", "
combinedFields[field.name] = "\(existingValue)\(separator)\(field.isoLatin1Value)"
} else {
combinedFields[field.name] = field.isoLatin1Value
}
}
var headerFields = [String: String](minimumCapacity: combinedFields.count)
for (name, value) in combinedFields { headerFields[name.rawName] = value }
self.allHTTPHeaderFields = headerFields
}
}

extension String { fileprivate var isASCII: Bool { self.utf8.allSatisfy { $0 & 0x80 == 0 } } }

extension HTTPField {
fileprivate init(name: Name, isoLatin1Value: String) {
if isoLatin1Value.isASCII {
self.init(name: name, value: isoLatin1Value)
} else {
self = withUnsafeTemporaryAllocation(of: UInt8.self, capacity: isoLatin1Value.unicodeScalars.count) {
buffer in
for (index, scalar) in isoLatin1Value.unicodeScalars.enumerated() {
if scalar.value > UInt8.max {
buffer[index] = 0x20
} else {
buffer[index] = UInt8(truncatingIfNeeded: scalar.value)
}
}
return HTTPField(name: name, value: buffer)
}
}
}

fileprivate var isoLatin1Value: String {
if self.value.isASCII { return self.value }
return self.withUnsafeBytesOfValue { buffer in
let scalars = buffer.lazy.map { UnicodeScalar(UInt32($0))! }
var string = ""
string.unicodeScalars.append(contentsOf: scalars)
return string
}
}
}

Expand All @@ -211,6 +261,8 @@ extension URLSessionTransportError: CustomStringConvertible {
"Invalid request URL from request path: \(path), method: \(method), relative to base URL: \(baseURL.absoluteString)"
case .notHTTPResponse(let response):
return "Received a non-HTTP response, of type: \(String(describing: type(of: response)))"
case .invalidResponseStatusCode(let response):
return "Received an HTTP response with invalid status code: \(response.statusCode))"
case .noResponse(let url): return "Received a nil response for \(url?.absoluteString ?? "<nil URL>")"
case .streamingNotSupported: return "Streaming is not supported on this platform"
}
Expand Down
13 changes: 9 additions & 4 deletions Tests/OpenAPIURLSessionTests/URLSessionTransportTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,23 @@ class URLSessionTransportConverterTests: XCTestCase {
static override func setUp() { OpenAPIURLSession.debugLoggingEnabled = false }

func testRequestConversion() async throws {
let request = HTTPRequest(
var request = HTTPRequest(
method: .post,
scheme: nil,
authority: nil,
path: "/hello%20world/Maria?greeting=Howdy",
headerFields: [.init("x-mumble2")!: "mumble"]
headerFields: [.init("x-mumble2")!: "mumble", .init("x-mumble2")!: "mumble"]
)
let cookie = "uid=urlsession; sid=0123456789-9876543210"
request.headerFields[.cookie] = cookie
request.headerFields[.init("X-Emoji")!] = "😀"
let urlRequest = try URLRequest(request, baseURL: URL(string: "http://example.com/api")!)
XCTAssertEqual(urlRequest.url, URL(string: "http://example.com/api/hello%20world/Maria?greeting=Howdy"))
XCTAssertEqual(urlRequest.httpMethod, "POST")
XCTAssertEqual(urlRequest.allHTTPHeaderFields?.count, 1)
XCTAssertEqual(urlRequest.value(forHTTPHeaderField: "x-mumble2"), "mumble")
XCTAssertEqual(urlRequest.allHTTPHeaderFields?.count, 3)
XCTAssertEqual(urlRequest.value(forHTTPHeaderField: "x-mumble2"), "mumble, mumble")
XCTAssertEqual(urlRequest.value(forHTTPHeaderField: "cookie"), cookie)
XCTAssertEqual(urlRequest.value(forHTTPHeaderField: "X-Emoji"), "😀")
}

func testResponseConversion() async throws {
Expand Down

0 comments on commit 50e1f4a

Please sign in to comment.