HTTPURLResponse allHeaderFields Swift 3 Capitalisation
Asked Answered
I

10

12

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.

Impassioned answered 20/10, 2016 at 10:57 Comment(0)
T
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.)

Taiga answered 20/10, 2016 at 11:6 Comment(0)
H
7

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
    }

}
Hinder answered 24/6, 2017 at 22:55 Comment(0)
T
6

More efficient workaround:

(response.allHeaderFields as NSDictionary)["etag"]
Theatrician answered 11/2, 2019 at 23:55 Comment(0)
R
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.

Rader answered 2/1, 2017 at 15:8 Comment(0)
G
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.

Glabella answered 2/2, 2017 at 1:20 Comment(0)
U
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
        }
    }
}

Ungava answered 18/11, 2019 at 13:58 Comment(0)
U
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
Unfrequented answered 17/5, 2017 at 11:59 Comment(0)
R
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
Reminiscence answered 29/6, 2017 at 19:58 Comment(0)
C
0

For simple case use

if let ix = headers.index(where: {$0.key.caseInsensitiveCompare("eTag") == .orderedSame}){
    let tag = headers[ix].value
}
Cardioid answered 19/9, 2017 at 9:8 Comment(0)
C
-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
Clonus answered 13/11, 2018 at 13:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.