Skip to content

Commit

Permalink
Fix encoding and decoding of HTTPHeaders (#3116)
Browse files Browse the repository at this point in the history
* Fix encoding and decoding of HTTPHeaders with multiple of the same header name via Codable. Add test.
* Add back the ability to decode the previous encoding, in case anyone was using it
  • Loading branch information
gwynne committed Dec 8, 2023
1 parent 3d62c0c commit 00c902c
Show file tree
Hide file tree
Showing 2 changed files with 64 additions and 9 deletions.
35 changes: 26 additions & 9 deletions Sources/Vapor/HTTP/Headers/HTTPHeaders.swift
Expand Up @@ -35,20 +35,37 @@ extension HTTPHeaders {
}

extension HTTPHeaders: Codable {
public init(from decoder: Decoder) throws {
let dictionary = try decoder.singleValueContainer().decode([String: String].self)
private enum CodingKeys: String, CodingKey { case name, value }

public init(from decoder: any Decoder) throws {
self.init()
for (name, value) in dictionary {
self.add(name: name, value: value)
do {
var container = try decoder.unkeyedContainer()

while !container.isAtEnd {
let nested = try container.nestedContainer(keyedBy: Self.CodingKeys.self)
let name = try nested.decode(String.self, forKey: .name)
let value = try nested.decode(String.self, forKey: .value)

self.add(name: name, value: value)
}
} catch DecodingError.typeMismatch(let type, _) where "\(type)".starts(with: "Array<") {
// Try the old format
let container = try decoder.singleValueContainer()
let dict = try container.decode([String: String].self)

self.add(contentsOf: dict.map { ($0.key, $0.value) })
}
}

public func encode(to encoder: Encoder) throws {
var dictionary: [String: String] = [:]
public func encode(to encoder: any Encoder) throws {
var container = encoder.unkeyedContainer()

for (name, value) in self {
dictionary[name] = value
var nested = container.nestedContainer(keyedBy: Self.CodingKeys.self)

try nested.encode(name, forKey: .name)
try nested.encode(value, forKey: .value)
}
var container = encoder.singleValueContainer()
try container.encode(dictionary)
}
}
38 changes: 38 additions & 0 deletions Tests/VaporTests/HTTPHeaderTests.swift
Expand Up @@ -439,4 +439,42 @@ final class HTTPHeaderTests: XCTestCase {
XCTAssertEqual(cacheControl.serialize(), "immutable")

}

/// Test that multiple same-named headers round-trip through Codable
func testCodableMultipleHeadersRountrip() throws {
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
encoder.outputFormatting = [.withoutEscapingSlashes, .sortedKeys]
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601

var headers = HTTPHeaders()
headers.add(name: .date, value: "\(Date(timeIntervalSinceReferenceDate: 100.0))")
headers.add(name: .date, value: "\(Date(timeIntervalSinceReferenceDate: -100.0))")
headers.add(name: .connection, value: "be-strange")

let encodedHeaders = try encoder.encode(headers)

XCTAssertEqual(String(decoding: encodedHeaders, as: UTF8.self), #"[{"name":"date","value":"2001-01-01 00:01:40 +0000"},{"name":"date","value":"2000-12-31 23:58:20 +0000"},{"name":"connection","value":"be-strange"}]"#)

let decodedHeaders = try decoder.decode(HTTPHeaders.self, from: encodedHeaders)

XCTAssertEqual(decodedHeaders.count, headers.count)
for ((k1, v1), (k2, v2)) in zip(headers, decodedHeaders) {
XCTAssertEqual(k1, k2)
XCTAssertEqual(v1, v2)
}
}

/// Make sure the old HTTPHeaders encoding can still be decoded
func testOldHTTPHeadersEncoding() throws {
let decoder = JSONDecoder()
let json = #"{"connection":"fun","attention":"none"}"#
var headers = HTTPHeaders()

XCTAssertNoThrow(headers = try decoder.decode(HTTPHeaders.self, from: Data(json.utf8)))
XCTAssertEqual(headers.count, 2)
XCTAssertEqual(headers.first(name: "connection"), "fun")
XCTAssertEqual(headers.first(name: "attention"), "none")
}
}

0 comments on commit 00c902c

Please sign in to comment.