11

Converting to Swift 3 I noticed a strange bug occur reading a header field from HTTPURLResponse.

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

no longer worked.

I printed out the all headers dictionary and all my header keys seem to be in Sentence case.

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

So I don't know which to believe. Is anyone else finding in Swift 3 that their headers are now being turned into Sentence case by iOS? If so is this behaviour we want?

Should I log a bug with Apple or should I just make a category on HTTPURLResponse to allow myself to case insensitively find a header value.

Mark Bridges
  • 8,228
  • 4
  • 50
  • 65

10 Answers10

11

Update: this is a known issue.


allHeaderFields should return a case-insensitive dictionary because that is what the HTTP spec requires. Looks like a Swift error, I would file a radar or a bug report on .

Here is some sample code that reproduces the issue simply:

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 Cédric Luthi.)

Aaron Brager
  • 65,323
  • 19
  • 161
  • 287
6

Based on @Darko's answer I have 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
    }

}
Ondrej Rafaj
  • 4,342
  • 8
  • 42
  • 65
6

More efficient workaround:

(response.allHeaderFields as NSDictionary)["etag"]
Guoye Zhang
  • 499
  • 4
  • 9
1

As a hot-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 seems still not to be fixed in Swift 4.

Darko
  • 9,655
  • 9
  • 36
  • 48
1

I hit this and worked around it using an extension on Dictionary to create custom subscripts.

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 are from benchmarking different scenarios (optimised build, -Os, averaged over 1,000,000 iterations). An equivalent access of a standard dictionary, came out at 1257ns. Having to make two checks effectively doubled that, 2412ns.

In my particular case, I was seeing a header come back from the server that was camel-case, or lower-case, depending on the network I was connecting on (something else to investigate). The advantage of this is that, should it get fixed, I can just delete the extension and nothing else needs to change. Also, anyone else using the code doesn't need to remember any workarounds - they get this one for free.

I checked and did not see ETag being modified by HTTPURLResponse - if I passed it ETag, or Etag I got those back in allHeaderFields. In the case that performance is a concern, and you are encountering this issue, you can create a second subscript which takes a Hashable struct containing an array. Then pass it to the Dictionary, with the tags you want to handle.

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 is, as you'd expect, almost equivalent to making individual dictionary lookups.

StephenG
  • 76
  • 3
1

Due to a bug in Swift and a new solution in iOS 13, I made an extension:

Here is a link to gist.

public extension HTTPURLResponse {
    func valueForHeaderField(_ headerField: String) -> String? {
        if #available(iOS 13.0, *) {
            return value(forHTTPHeaderField: headerField)
        } else {
            return (allHeaderFields as NSDictionary)[headerField] as? String
        }
    }
}

Alexander Bekert
  • 617
  • 8
  • 18
0

There is a little-bit shorter version than Darko's for swift 3.0.

Due to the fact, that the header names cases can be different in iOS8 and iOS10 so the best way is to use a case insensitive compare.

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

So all types are now supported:

  • case-insensitive-header
  • Case-Insensitive-Header
  • CASE-INSENSITIVE-HEADER
S0r13n
  • 1
0

Here's mine. Instead of messing around with the way the dictionary works, I made an obj-c category on NSHTTPURLResponse. The Obj-C allHeaderFields dictionary 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
Paul Bruneau
  • 1,026
  • 1
  • 9
  • 15
0

For simple case use

if let ix = headers.index(where: {$0.key.caseInsensitiveCompare("eTag") == .orderedSame}){
    let tag = headers[ix].value
}
john07
  • 562
  • 6
  • 16
-1

in swift 4.1 It worked for me.

if let headers = httpResponse.allHeaderFields as? [String: String], let value = headers["key"] {
                print("value: \(value)")
            }

and you can also use .lowercased() and .capitalized values if not working like below (according to your situation)

httpResponse.allHeaderFields["eTag".capitalized] as? String
httpResponse.allHeaderFields["eTag". lowercased()] as? String
Sultan Ali
  • 2,497
  • 28
  • 25