HTTPURLResponse allHeaderFields Swift 3 大小写

HTTPURLResponse allHeaderFields Swift 3 Capitalisation

转换为 Swift 3 我注意到从 HTTPURLResponse 读取 header 字段时出现了一个奇怪的错误。

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

不再有效。

我打印了所有 header 的字典,我所有的 header 键似乎都是句子大小写。

根据 Charles 代理,我所有的 header 都是小写的。根据后端团队的说法,在他们的代码中 header 位于 Title-Case 中。根据文档:headers 应该是 case-insensitive。

所以我不知道该相信哪个。是否还有其他人在 Swift 3 中发现他们的 header 现在被 iOS 变成了 Sentence case?如果是这样,这是我们想要的行为吗?

我应该用 Apple 记录一个错误,还是应该在 HTTPURLResponse 上创建一个类别,让我自己不区分大小写地找到一个 header 值。

更新:这是一个known issue.


allHeaderFields 应该 return 一个不区分大小写的字典,因为这是 HTTP 规范所要求的。看起来像 Swift 错误,我会在 .

上提交雷达或错误报告

下面是一些简单重现问题的示例代码:

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)

(改编自 this Gist from Cédric Luthi。)

作为 Swift 3 的修补程序,您可以这样做:

// Converting to an array of tuples of type (String, String)
// The key is lowercased()
let keyValues = response.allHeaderFields.map { (String(describing: [=10=].key).lowercased(), String(describing: [=10=].value)) } 

// Now filter the array, searching for your header-key, also lowercased
if let myHeaderValue = keyValues.filter({ [=10=].0 == "X-MyHeaderKey".lowercased() }).first {
    print(myHeaderValue.1)
}

更新:此问题似乎仍未在 Swift 4.

中修复

我点击了这个并使用 Dictionary 上的扩展来解决它来创建自定义下标。

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
        }
    }
}

评论中的时间来自不同场景的基准测试(优化构建,-Os,平均超过 1,000,000 次迭代)。标准字典的等效访问在 1257ns 时出现。必须进行两次检查有效地使它翻了一番,2412ns。

在我的特殊情况下,我看到 header 从 camel-case 或 lower-case 的服务器返回,具体取决于我连接的网络(其他去弄清楚)。这样做的好处是,如果它得到修复,我可以删除扩展而无需更改其他任何内容。此外,使用该代码的其他任何人都不需要记住任何解决方法 - 他们可以免费获得这个。

我检查了一下,没有看到 ETagHTTPURLResponse 修改了 - 如果我通过了它 ETag,或者 Etag 我在 [=18= 中得到了那些].如果性能是一个问题,并且您遇到了这个问题,您可以创建第二个下标,它采用包含数组的 Hashable 结构。然后将其传递给 Dictionary,其中包含您要处理的标签。

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"])])"

如您所料,这几乎等同于进行单独的字典查找。

有一个 little-bit 比 Darko 的 swift 3.0 版本更短的版本。

由于 header 名称在 iOS8 和 iOS10 中的大小写可能不同,因此最好的方法是使用不区分大小写的比较。

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

所以现在支持所有类型:

  • case-insensitive-header
  • Case-Insensitive-Header
  • CASE-INSENSITIVE-HEADER

根据@Darko 的回答,我做了一个 Swift 3 扩展,可以让您找到不区分大小写的任何 headers:

import Foundation


extension HTTPURLResponse {

    func find(header: String) -> String? {
        let keyValues = allHeaderFields.map { (String(describing: [=10=].key).lowercased(), String(describing: [=10=].value)) }

        if let headerValue = keyValues.filter({ [=10=].0 == header.lowercased() }).first {
            return headerValue.1
        }
        return nil
    }

}

这是我的。我没有弄乱字典的工作方式,而是在 NSHTTPURLResponse 上创建了一个 obj-c 类别。 Obj-C allHeaderFields 字典仍然不区分大小写。

@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

简单案例使用

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

swift 4.1 它对我有用。

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

如果不能像下面那样工作,您也可以使用 .lowercased().capitalized 值(根据您的情况)

httpResponse.allHeaderFields["eTag".capitalized] as? String
httpResponse.allHeaderFields["eTag". lowercased()] as? String

更有效的解决方法:

(response.allHeaderFields as NSDictionary)["etag"]

由于 bug in Swift and a new solution in iOS 13,我进行了扩展:

这里是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
        }
    }
}