Skip to content

Commit 50e1f4a

Browse files
authored
Fix request and response translation (#51)
1 parent 6b915d8 commit 50e1f4a

File tree

2 files changed

+69
-12
lines changed

2 files changed

+69
-12
lines changed

Sources/OpenAPIURLSession/URLSessionTransport.swift

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,9 @@ internal enum URLSessionTransportError: Error {
150150
/// Returned `URLResponse` could not be converted to `HTTPURLResponse`.
151151
case notHTTPResponse(URLResponse)
152152

153+
/// Returned `HTTPURLResponse` has an invalid status code
154+
case invalidResponseStatusCode(HTTPURLResponse)
155+
153156
/// Returned `URLResponse` was nil
154157
case noResponse(url: URL?)
155158

@@ -162,14 +165,18 @@ extension HTTPResponse {
162165
guard let httpResponse = urlResponse as? HTTPURLResponse else {
163166
throw URLSessionTransportError.notHTTPResponse(urlResponse)
164167
}
165-
var headerFields = HTTPFields()
166-
for (headerName, headerValue) in httpResponse.allHeaderFields {
167-
guard let rawName = headerName as? String, let name = HTTPField.Name(rawName),
168-
let value = headerValue as? String
169-
else { continue }
170-
headerFields[name] = value
168+
guard (0...999).contains(httpResponse.statusCode) else {
169+
throw URLSessionTransportError.invalidResponseStatusCode(httpResponse)
170+
}
171+
self.init(status: .init(code: httpResponse.statusCode))
172+
if let fields = httpResponse.allHeaderFields as? [String: String] {
173+
self.headerFields.reserveCapacity(fields.count)
174+
for (name, value) in fields {
175+
if let name = HTTPField.Name(name) {
176+
self.headerFields.append(HTTPField(name: name, isoLatin1Value: value))
177+
}
178+
}
171179
}
172-
self.init(status: .init(code: httpResponse.statusCode), headerFields: headerFields)
173180
}
174181
}
175182

@@ -193,7 +200,50 @@ extension URLRequest {
193200
}
194201
self.init(url: url)
195202
self.httpMethod = request.method.rawValue
196-
for header in request.headerFields { setValue(header.value, forHTTPHeaderField: header.name.canonicalName) }
203+
var combinedFields = [HTTPField.Name: String](minimumCapacity: request.headerFields.count)
204+
for field in request.headerFields {
205+
if let existingValue = combinedFields[field.name] {
206+
let separator = field.name == .cookie ? "; " : ", "
207+
combinedFields[field.name] = "\(existingValue)\(separator)\(field.isoLatin1Value)"
208+
} else {
209+
combinedFields[field.name] = field.isoLatin1Value
210+
}
211+
}
212+
var headerFields = [String: String](minimumCapacity: combinedFields.count)
213+
for (name, value) in combinedFields { headerFields[name.rawName] = value }
214+
self.allHTTPHeaderFields = headerFields
215+
}
216+
}
217+
218+
extension String { fileprivate var isASCII: Bool { self.utf8.allSatisfy { $0 & 0x80 == 0 } } }
219+
220+
extension HTTPField {
221+
fileprivate init(name: Name, isoLatin1Value: String) {
222+
if isoLatin1Value.isASCII {
223+
self.init(name: name, value: isoLatin1Value)
224+
} else {
225+
self = withUnsafeTemporaryAllocation(of: UInt8.self, capacity: isoLatin1Value.unicodeScalars.count) {
226+
buffer in
227+
for (index, scalar) in isoLatin1Value.unicodeScalars.enumerated() {
228+
if scalar.value > UInt8.max {
229+
buffer[index] = 0x20
230+
} else {
231+
buffer[index] = UInt8(truncatingIfNeeded: scalar.value)
232+
}
233+
}
234+
return HTTPField(name: name, value: buffer)
235+
}
236+
}
237+
}
238+
239+
fileprivate var isoLatin1Value: String {
240+
if self.value.isASCII { return self.value }
241+
return self.withUnsafeBytesOfValue { buffer in
242+
let scalars = buffer.lazy.map { UnicodeScalar(UInt32($0))! }
243+
var string = ""
244+
string.unicodeScalars.append(contentsOf: scalars)
245+
return string
246+
}
197247
}
198248
}
199249

@@ -211,6 +261,8 @@ extension URLSessionTransportError: CustomStringConvertible {
211261
"Invalid request URL from request path: \(path), method: \(method), relative to base URL: \(baseURL.absoluteString)"
212262
case .notHTTPResponse(let response):
213263
return "Received a non-HTTP response, of type: \(String(describing: type(of: response)))"
264+
case .invalidResponseStatusCode(let response):
265+
return "Received an HTTP response with invalid status code: \(response.statusCode))"
214266
case .noResponse(let url): return "Received a nil response for \(url?.absoluteString ?? "<nil URL>")"
215267
case .streamingNotSupported: return "Streaming is not supported on this platform"
216268
}

Tests/OpenAPIURLSessionTests/URLSessionTransportTests.swift

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,23 @@ class URLSessionTransportConverterTests: XCTestCase {
2727
static override func setUp() { OpenAPIURLSession.debugLoggingEnabled = false }
2828

2929
func testRequestConversion() async throws {
30-
let request = HTTPRequest(
30+
var request = HTTPRequest(
3131
method: .post,
3232
scheme: nil,
3333
authority: nil,
3434
path: "/hello%20world/Maria?greeting=Howdy",
35-
headerFields: [.init("x-mumble2")!: "mumble"]
35+
headerFields: [.init("x-mumble2")!: "mumble", .init("x-mumble2")!: "mumble"]
3636
)
37+
let cookie = "uid=urlsession; sid=0123456789-9876543210"
38+
request.headerFields[.cookie] = cookie
39+
request.headerFields[.init("X-Emoji")!] = "😀"
3740
let urlRequest = try URLRequest(request, baseURL: URL(string: "http://example.com/api")!)
3841
XCTAssertEqual(urlRequest.url, URL(string: "http://example.com/api/hello%20world/Maria?greeting=Howdy"))
3942
XCTAssertEqual(urlRequest.httpMethod, "POST")
40-
XCTAssertEqual(urlRequest.allHTTPHeaderFields?.count, 1)
41-
XCTAssertEqual(urlRequest.value(forHTTPHeaderField: "x-mumble2"), "mumble")
43+
XCTAssertEqual(urlRequest.allHTTPHeaderFields?.count, 3)
44+
XCTAssertEqual(urlRequest.value(forHTTPHeaderField: "x-mumble2"), "mumble, mumble")
45+
XCTAssertEqual(urlRequest.value(forHTTPHeaderField: "cookie"), cookie)
46+
XCTAssertEqual(urlRequest.value(forHTTPHeaderField: "X-Emoji"), "😀")
4247
}
4348

4449
func testResponseConversion() async throws {

0 commit comments

Comments
 (0)