HTTPURLResponse allHeaderFields Swift 3 Capitalization

Converting to Swift 3 I noticed that there was a strange error reading the header field from HTTPURLResponse.

let id = httpResponse.allHeaderFields["eTag"] as? String 

does not work any more.

I printed out a dictionary of all the headers, and all of my headers seem to be in the case of sentences.

According to Charles proxy, all my headers are in lower case. According to the backend team, the headers in the code are in the Title-Case. According to docs: headers should be case insensitive.

So I don’t know what to believe. Does anyone else find in Swift 3 that their headers are now turning into a sentence case iOS? If so, do we want to?

Should I report an error with Apple or should I just create a category in HTTPURLResponse to allow me the case to discreetly find the value of the header.

+8
ios case-insensitive swift3
source share
7 answers

Update : This is a known issue .


allHeaderFields must return a allHeaderFields dictionary, because this is what the HTTP specification requires. Sounds like a Swift bug, I would post a radar or bug report.

Here is sample code that just reproduces the problem:

 let headerFields = ["ETag" : "12345678"] let url = URL(string: "http://www.example.com")! let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: "HTTP/1.1", headerFields: headerFields)! response.allHeaderFields["eTaG"] // nil (incorrect) headerFields["eTaG"] // nil (correct) 

(Adapted from this Gist from Cedric Luti .)

+8
source

Based on @Darko's answer, I made a Swift 3 extension that will allow you to find any headers as case insensitive:

 import Foundation extension HTTPURLResponse { func find(header: String) -> String? { let keyValues = allHeaderFields.map { (String(describing: $0.key).lowercased(), String(describing: $0.value)) } if let headerValue = keyValues.filter({ $0.0 == header.lowercased() }).first { return headerValue.1 } return nil } } 
+2
source

As a fix for Swift 3, you can do this:

 // Converting to an array of tuples of type (String, String) // The key is lowercased() let keyValues = response.allHeaderFields.map { (String(describing: $0.key).lowercased(), String(describing: $0.value)) } // Now filter the array, searching for your header-key, also lowercased if let myHeaderValue = keyValues.filter({ $0.0 == "X-MyHeaderKey".lowercased() }).first { print(myHeaderValue.1) } 

UPDATE: This issue is still not fixed in Swift 4.

+1
source

I hit this and worked around it using a Dictionary extension to create custom indexes.

 extension Dictionary { subscript(key: String) -> Value? { get { let anyKey = key as! Key if let value = self[anyKey] { return value // 1213ns } if let value = self[key.lowercased() as! Key] { return value // 2511ns } if let value = self[key.capitalized as! Key] { return value // 8928ns } for (storedKey, storedValue) in self { if let stringKey = storedKey as? String { if stringKey.caseInsensitiveCompare(key) == .orderedSame { return storedValue // 22317ns } } } return nil } set { self[key] = newValue } } } 

The timings in the comments relate to the comparison of different scenarios (optimized assembly, -Os , averaged over 1,000,000 iterations). Equivalent access to the standard dictionary came out at 1257ns. To make two checks effectively doubled, that is 2412ns.

In my particular case, I saw the header coming back from a server that was a camel case or lowercase, depending on the network I was connecting to (something else to investigate). The advantage of this is that if it is fixed, I can simply remove the extension and nothing else needs to be changed. In addition, anyone who uses the code does not need to remember any workarounds - they get it for free.

I checked and did not see that the ETag was changed by HTTPURLResponse - if I passed it to ETag or ETag , I got them back to allHeaderFields . In case performance is a problem and you are facing this problem, you can create a second index that takes a Hashable structure containing an array. Then pass it to the dictionary, with the tags you want to process.

 struct DictionaryKey: Hashable { let keys: [String] var hashValue: Int { return 0 } // Don't care what is returned, not going to use it } func ==(lhs: DictionaryKey, rhs: DictionaryKey) -> Bool { return lhs.keys == rhs.keys // Just filling expectations } extension Dictionary { subscript(key: DictionaryKey) -> Value? { get { for string in key.keys { if let value = self[string as! Key] { return value } } return nil } } } print("\(allHeaderFields[DictionaryKey(keys: ["ETag", "Etag"])])" 

This, as one would expect, is almost equivalent to performing separate dictionary searches.

+1
source

There is a slightly shorter version than Darko for swift 3.0.

Due to the fact that the case of header names may differ in iOS8 and iOS10, it is best to use case insensitivity.

 response.allHeaderFields.keys.contains(where: {$0.description.caseInsensitiveCompare("CaSe-InSeNsItIvE-HeAdEr") == .orderedSame}) 

So now all types of types are supported:

  • case insensitive header
  • Case-Insensitivity-Header
  • CASE-INSENSITIVE-HEADER
0
source

Here is mine. Instead of messing with how the dictionary works, I created the obj-c category on NSHTTPURLResponse . The Obj-C allHeaderFields is still case insensitive.

 @import Foundation; @implementation NSHTTPURLResponse (CaseInsensitive) - (nullable NSString *)allHeaderFieldsValueForCaseInsensitiveKey:(nonnull NSString *)key { NSString *value = self.allHeaderFields[key]; if ([value isKindOfClass:[NSString class]]) { return value; } else { return nil; } } @end 
0
source

For easy use of the case

 if let ix = headers.index(where: {$0.key.caseInsensitiveCompare("eTag") == .orderedSame}){ let tag = headers[ix].value } 
0
source

All Articles