Skip to content

Commit cce39c3

Browse files
authored
Ignore empty values when returning canonical form of header values (#307)
Motivation: The return value from `HPACKHeaders.subscript(canonicalForm:)` is inconsistent with respect to returning empty strings. If a value for a header field is an empty string then the canonical form will not return that empty string. However, if a value is a string containing only whitespace then an empty string will be returned. Modifications: - Filter out empty substrings after trimming whitespace - Add a test Result: - `HPACKHeaders.subscript(canonicalForm:)` does not produce empty strings
1 parent d34f4b0 commit cce39c3

File tree

3 files changed

+30
-11
lines changed

3 files changed

+30
-11
lines changed

Sources/NIOHPACK/HPACKHeader.swift

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public struct HPACKHeaders: ExpressibleByDictionaryLiteral {
4949
public init(httpHeaders: HTTPHeaders) {
5050
self.init(httpHeaders: httpHeaders, normalizeHTTPHeaders: true)
5151
}
52-
52+
5353
/// Construct a `HPACKHeaders` structure.
5454
///
5555
/// The indexability of all headers is assumed to be the default, i.e. indexable and
@@ -86,7 +86,7 @@ public struct HPACKHeaders: ExpressibleByDictionaryLiteral {
8686
// We no longer use an allocator so we don't need this method anymore.
8787
self.init(headers)
8888
}
89-
89+
9090
/// Internal initializer to make things easier for unit tests.
9191
@inlinable
9292
init(fullHeaders: [(HPACKIndexing, String, String)]) {
@@ -98,7 +98,7 @@ public struct HPACKHeaders: ExpressibleByDictionaryLiteral {
9898
init(headers: [HPACKHeader]) {
9999
self.headers = headers
100100
}
101-
101+
102102
/// Add a header name/value pair to the block.
103103
///
104104
/// This method is strictly additive: if there are other values for the given header name
@@ -174,7 +174,7 @@ public struct HPACKHeaders: ExpressibleByDictionaryLiteral {
174174
return nameToRemove.isEqualCaseInsensitiveASCIIBytes(to: header.name)
175175
}
176176
}
177-
177+
178178
/// Retrieve all of the values for a given header field name from the block.
179179
///
180180
/// This method uses case-insensitive comparisons for the header field name. It
@@ -198,7 +198,7 @@ public struct HPACKHeaders: ExpressibleByDictionaryLiteral {
198198
array.append(header.value)
199199
}
200200
}
201-
201+
202202
return array
203203
}
204204

@@ -240,7 +240,7 @@ public struct HPACKHeaders: ExpressibleByDictionaryLiteral {
240240
}
241241
return false
242242
}
243-
243+
244244
/// Retrieves the header values for the given header field in "canonical form": that is,
245245
/// splitting them on commas as extensively as possible such that multiple values received on the
246246
/// one line are returned as separate entries. Also respects the fact that Set-Cookie should not
@@ -254,15 +254,23 @@ public struct HPACKHeaders: ExpressibleByDictionaryLiteral {
254254
guard result.count > 0 else {
255255
return []
256256
}
257-
257+
258258
// It's not safe to split Set-Cookie on comma.
259259
guard name.lowercased() != "set-cookie" else {
260260
return result
261261
}
262-
263-
return result.flatMap { $0.split(separator: ",") }.map { String($0._trimWhitespace()) }
262+
263+
return result.flatMap {
264+
$0.split(separator: ",")
265+
}.lazy.map {
266+
$0._trimWhitespace()
267+
}.filter { // `split(separator:)` drops empty strings, we should too.
268+
!$0.isEmpty
269+
}.map {
270+
String($0)
271+
}
264272
}
265-
273+
266274
/// Special internal function for use by tests.
267275
internal subscript(position: Int) -> (String, String) {
268276
precondition(position < self.headers.endIndex, "Position \(position) is beyond bounds of \(self.headers.endIndex)")
@@ -411,7 +419,7 @@ public enum HPACKIndexing: CustomStringConvertible {
411419
/// Header may not be written to the dynamic index table, and proxies must
412420
/// pass it on as-is without rewriting.
413421
case neverIndexed
414-
422+
415423
public var description: String {
416424
switch self {
417425
case .indexable:

Tests/NIOHPACKTests/HPACKCodingTests+XCTest.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ extension HPACKCodingTests {
3636
("testHPACKHeadersDescription", testHPACKHeadersDescription),
3737
("testHPACKHeadersSubscript", testHPACKHeadersSubscript),
3838
("testHPACKHeadersCanonicalFormStripsWhitespace", testHPACKHeadersCanonicalFormStripsWhitespace),
39+
("testHPACKHeadersCanonicalFormDropsEmptyStrings", testHPACKHeadersCanonicalFormDropsEmptyStrings),
3940
("testHPACKHeadersFirst", testHPACKHeadersFirst),
4041
("testHPACKHeadersExpressedByDictionaryLiteral", testHPACKHeadersExpressedByDictionaryLiteral),
4142
("testHPACKHeadersAddingSequenceOfPairs", testHPACKHeadersAddingSequenceOfPairs),

Tests/NIOHPACKTests/HPACKCodingTests.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,16 @@ class HPACKCodingTests: XCTestCase {
586586
XCTAssertEqual(headers[canonicalForm: "sp-and-htab"], expected)
587587
}
588588

589+
func testHPACKHeadersCanonicalFormDropsEmptyStrings() throws {
590+
let headers: HPACKHeaders = [
591+
"no-whitespace": "foo,,bar",
592+
"with-whitespace": "foo, ,bar"
593+
]
594+
let expected = ["foo", "bar"]
595+
XCTAssertEqual(headers[canonicalForm: "no-whitespace"], expected)
596+
XCTAssertEqual(headers[canonicalForm: "with-whitespace"], expected)
597+
}
598+
589599
func testHPACKHeadersFirst() throws {
590600
let headers = HPACKHeaders([
591601
(":method", "GET"),

0 commit comments

Comments
 (0)