Skip to content

Commit 5db69d7

Browse files
committed
SR-12300: URLSession.dataTask doesn't follow redirects with lowercased location
- When initialising the HTTPURLResponse, store the header fields names capitalised except for X- headers. This matches Darwin. - This allows responses with lowercase fields names to be read correctly and redirects to works if the field name is 'location' instead of 'Location'. - Add missing function HTTPURLResponse.value(forHTTPHeaderField:).
1 parent 2389ca0 commit 5db69d7

File tree

3 files changed

+84
-8
lines changed

3 files changed

+84
-8
lines changed

Sources/FoundationNetworking/URLResponse.swift

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,24 @@ open class HTTPURLResponse : URLResponse {
186186
/// - Returns: the instance of the object, or `nil` if an error occurred during initialization.
187187
public init?(url: URL, statusCode: Int, httpVersion: String?, headerFields: [String : String]?) {
188188
self.statusCode = statusCode
189-
self.allHeaderFields = headerFields ?? [:]
189+
190+
self._allHeaderFields = {
191+
// Canonicalize the header fields by capitalizing the field names, but not X- Headers
192+
// This matches the behaviour of Darwin.
193+
guard let headerFields = headerFields else { return [:] }
194+
var canonicalizedFields: [String: String] = [:]
195+
196+
for (key, value) in headerFields {
197+
if key.isEmpty { continue }
198+
if key.hasPrefix("x-") || key.hasPrefix("X-") {
199+
canonicalizedFields[key] = value
200+
} else {
201+
canonicalizedFields[key.capitalized] = value
202+
}
203+
}
204+
return canonicalizedFields
205+
}()
206+
190207
super.init(url: url, mimeType: nil, expectedContentLength: 0, textEncodingName: nil)
191208
expectedContentLength = getExpectedContentLength(fromHeaderFields: headerFields) ?? -1
192209
suggestedFilename = getSuggestedFilename(fromHeaderFields: headerFields) ?? "Unknown"
@@ -204,9 +221,9 @@ open class HTTPURLResponse : URLResponse {
204221
self.statusCode = aDecoder.decodeInteger(forKey: "NS.statusCode")
205222

206223
if aDecoder.containsValue(forKey: "NS.allHeaderFields") {
207-
self.allHeaderFields = aDecoder.decodeObject(of: NSDictionary.self, forKey: "NS.allHeaderFields") as! [AnyHashable: Any]
224+
self._allHeaderFields = aDecoder.decodeObject(of: NSDictionary.self, forKey: "NS.allHeaderFields") as! [String: String]
208225
} else {
209-
self.allHeaderFields = [:]
226+
self._allHeaderFields = [:]
210227
}
211228

212229
super.init(coder: aDecoder)
@@ -236,8 +253,15 @@ open class HTTPURLResponse : URLResponse {
236253
///
237254
/// - Important: This is an *experimental* change from the
238255
/// `[NSObject: AnyObject]` type that Darwin Foundation uses.
239-
public let allHeaderFields: [AnyHashable : Any]
240-
256+
private let _allHeaderFields: [String: String]
257+
public var allHeaderFields: [AnyHashable : Any] {
258+
_allHeaderFields as [AnyHashable : Any]
259+
}
260+
261+
public func value(forHTTPHeaderField field: String) -> String? {
262+
return valueForCaseInsensitiveKey(field, fields: _allHeaderFields)
263+
}
264+
241265
/// Convenience method which returns a localized string
242266
/// corresponding to the status code for this response.
243267
/// - Parameter forStatusCode: the status code to use to produce a localized string.
@@ -315,8 +339,8 @@ open class HTTPURLResponse : URLResponse {
315339
/// This property is intended to produce readable output.
316340
override open var description: String {
317341
var result = "<\(type(of: self)) \(Unmanaged.passUnretained(self).toOpaque())> { URL: \(url!.absoluteString) }{ status: \(statusCode), headers {\n"
318-
for(aKey, aValue) in allHeaderFields {
319-
guard let key = aKey as? String, let value = aValue as? String else { continue } //shouldn't typically fail here
342+
for key in _allHeaderFields.keys.sorted() {
343+
guard let value = _allHeaderFields[key] else { continue }
320344
if((key.lowercased() == "content-disposition" && suggestedFilename != "Unknown") || key.lowercased() == "content-type") {
321345
result += " \"\(key)\" = \"\(value)\";\n"
322346
} else {

Tests/Foundation/HTTPServer.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -643,7 +643,7 @@ public class TestURLSessionServer {
643643
}
644644

645645
if uri == "/redirectToEchoHeaders" {
646-
return _HTTPResponse(response: .REDIRECT, headers: "Location: /echoHeaders\r\nSet-Cookie: redirect=true; Max-Age=7776000; path=/", body: "")
646+
return _HTTPResponse(response: .REDIRECT, headers: "location: /echoHeaders\r\nSet-Cookie: redirect=true; Max-Age=7776000; path=/", body: "")
647647
}
648648

649649
if uri == "/UnitedStates" {

Tests/Foundation/Tests/TestHTTPURLResponse.swift

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,57 @@ class TestHTTPURLResponse: XCTestCase {
173173
XCTAssertEqual(sut?.textEncodingName, "iso-8859-4")
174174
}
175175

176+
func test_fieldCapitalisation() throws {
177+
let f = [
178+
"location": "/newLocation",
179+
"conTent-lenGTH": "123",
180+
"CONTENT-type": "text/plAIn; charset=ISO-8891-1",
181+
"x-extra-HEADER": "my Header",
182+
"X-UPPERCASE": "UPPERCASE",
183+
"x-lowercase": "lowercase",
184+
"X-mixedCASE": "MIXEDcase",
185+
"vary": "much",
186+
"X-xss-protection": "1; mode=block",
187+
188+
]
189+
guard let sut = HTTPURLResponse(url: url, statusCode: 302, httpVersion: "HTTP/1.1", headerFields: f) else {
190+
XCTFail("Cant create HTTPURLResponse")
191+
return
192+
}
193+
XCTAssertEqual(sut.statusCode, 302)
194+
XCTAssertEqual(sut.expectedContentLength, 123)
195+
XCTAssertEqual(sut.mimeType, "text/plain")
196+
XCTAssertEqual(sut.textEncodingName, "iso-8891-1")
197+
198+
guard let ahf = sut.allHeaderFields as? [String: String] else {
199+
XCTFail("Cant read .allheaderFields")
200+
return
201+
}
202+
203+
XCTAssertEqual(sut.value(forHTTPHeaderField: "location"), "/newLocation")
204+
XCTAssertEqual(sut.value(forHTTPHeaderField: "LOcation"), "/newLocation")
205+
XCTAssertEqual(sut.value(forHTTPHeaderField: "locATIon"), "/newLocation")
206+
XCTAssertEqual(sut.value(forHTTPHeaderField: "x-extra-HEADER"), "my Header")
207+
XCTAssertEqual(sut.value(forHTTPHeaderField: "X-EXTRA-HEADER"), "my Header")
208+
XCTAssertEqual(sut.value(forHTTPHeaderField: "x-ExTrA-header"), "my Header")
209+
210+
XCTAssertEqual(ahf["Location"], "/newLocation")
211+
XCTAssertEqual(ahf["Content-Length"], "123")
212+
XCTAssertEqual(ahf["Content-Type"], "text/plAIn; charset=ISO-8891-1")
213+
XCTAssertEqual(ahf["x-extra-HEADER"], "my Header")
214+
XCTAssertEqual(ahf["X-UPPERCASE"], "UPPERCASE")
215+
XCTAssertEqual(ahf["x-lowercase"], "lowercase")
216+
XCTAssertEqual(ahf["X-mixedCASE"], "MIXEDcase")
217+
218+
XCTAssertNil(ahf["location"])
219+
XCTAssertNil(ahf["conTent-lenGTH"])
220+
XCTAssertNil(ahf["CONTENT-type"])
221+
XCTAssertNil(ahf["X-Extra-Header"])
222+
XCTAssertNil(ahf["X-Uppercase"])
223+
XCTAssertNil(ahf["X-Lowercase"])
224+
XCTAssertNil(ahf["X-Mixedcase"])
225+
}
226+
176227
// NSCoding
177228

178229
func test_NSCoding() {
@@ -230,6 +281,7 @@ class TestHTTPURLResponse: XCTestCase {
230281
("test_MIMETypeAndCharacterEncoding_2", test_MIMETypeAndCharacterEncoding_2),
231282
("test_MIMETypeAndCharacterEncoding_3", test_MIMETypeAndCharacterEncoding_3),
232283

284+
("test_fieldCapitalisation", test_fieldCapitalisation),
233285
("test_NSCoding", test_NSCoding),
234286
]
235287
}

0 commit comments

Comments
 (0)