diff --git a/docs/root/intro/version_history.rst b/docs/root/intro/version_history.rst index a32a3b08bf..819ccd3e9c 100644 --- a/docs/root/intro/version_history.rst +++ b/docs/root/intro/version_history.rst @@ -24,6 +24,7 @@ Bugfixes: - Android: update Kotlin standard libraries to 1.6.21 (:issue:`#2256 <2256>`) - fix bug where finalStreamIntel was not consistently set on cancel (:issue:`#2285 <2285>`) - iOS: fix termination crash in ProvisionalDispatcher (:issue:`#2059 <2059>`) +- iOS: make headers lookup in ``HeadersBuilder`` and ``Headers`` case-insensitive. Rename ``allHeaders`` method to ``caseSensitiveHeaders``. (:issue:`#2383 <2383>`) - iOS: use correct DNS resolver when using C++ config builder (:issue: `#2378 <2378 >`) Features: diff --git a/examples/objective-c/hello_world/ViewController.m b/examples/objective-c/hello_world/ViewController.m index 3a989e42cf..d01dd8a750 100644 --- a/examples/objective-c/hello_world/ViewController.m +++ b/examples/objective-c/hello_world/ViewController.m @@ -90,9 +90,9 @@ - (void)performRequest { NSString *message = [NSString stringWithFormat:@"received headers with status %i", statusCode]; NSMutableString *headerMessage = [NSMutableString new]; - for (NSString *name in headers.allHeaders) { + for (NSString *name in headers.caseSensitiveHeaders) { if ([self.filteredHeaders containsObject:name]) { - NSArray *values = headers.allHeaders[name]; + NSArray *values = headers.caseSensitiveHeaders[name]; NSString *joined = [values componentsJoinedByString:@", "]; NSString *pair = [NSString stringWithFormat:@"%@: %@\n", name, joined]; [headerMessage appendString:pair]; diff --git a/examples/swift/async_await/ContentView.swift b/examples/swift/async_await/ContentView.swift index 2502352c82..6772ecbf5c 100644 --- a/examples/swift/async_await/ContentView.swift +++ b/examples/swift/async_await/ContentView.swift @@ -72,12 +72,8 @@ private extension StreamClient { let stream = self .newStreamPrototype() .setOnResponseHeaders { headers, _, _ in - let allHeaders = headers.allHeaders() - - if allHeaders[":status"]?.first == "200", - // TODO(jpsim): Expose an API that enforces case-insensitive lookups - let contentLengthValue = allHeaders["Content-Length"] ?? - allHeaders["content-length"], + if headers.value(forName: ":status")?.first == "200", + let contentLengthValue = headers.value(forName: "content-length"), let firstContentLength = contentLengthValue.first, let contentLengthInt = Int64(firstContentLength) { @@ -87,7 +83,7 @@ private extension StreamClient { return } - let headerMessage = allHeaders + let headerMessage = headers.caseSensitiveHeaders() .map { "\($0.key): \($0.value.joined(separator: ", "))" } .joined(separator: "\n") diff --git a/examples/swift/hello_world/ViewController.swift b/examples/swift/hello_world/ViewController.swift index 8676c01002..f9f998e65d 100644 --- a/examples/swift/hello_world/ViewController.swift +++ b/examples/swift/hello_world/ViewController.swift @@ -70,7 +70,7 @@ final class ViewController: UITableViewController { let statusCode = headers.httpStatus.map(String.init) ?? "nil" let message = "received headers with status \(statusCode)" - let headerMessage = headers.allHeaders() + let headerMessage = headers.caseSensitiveHeaders() .filter { kFilteredHeaders.contains($0.key) } .map { "\($0.key): \($0.value.joined(separator: ", "))" } .joined(separator: "\n") diff --git a/library/swift/BUILD b/library/swift/BUILD index 3fcdd55f08..6bbc9cb69d 100644 --- a/library/swift/BUILD +++ b/library/swift/BUILD @@ -17,6 +17,7 @@ swift_library( "FinalStreamIntel.swift", "Headers.swift", "HeadersBuilder.swift", + "HeadersContainer.swift", "KeyValueStore.swift", "LogLevel.swift", "NetworkMonitoringMode.swift", diff --git a/library/swift/Headers.swift b/library/swift/Headers.swift index 094a039294..779b57b940 100644 --- a/library/swift/Headers.swift +++ b/library/swift/Headers.swift @@ -4,38 +4,57 @@ import Foundation /// To instantiate new instances, see `{Request|Response}HeadersBuilder`. @objcMembers public class Headers: NSObject { - let headers: [String: [String]] + let container: HeadersContainer /// Get the value for the provided header name. /// + /// - note: The lookup for a header name is a case-insensitive operation. + /// /// - parameter name: Header name for which to get the current value. /// /// - returns: The current headers specified for the provided name. public func value(forName name: String) -> [String]? { - return self.headers[name] + return self.container.value(forName: name) } - /// Accessor for all underlying headers as a map. + /// Accessor for all underlying case-sensitive headers. When possible, + /// use case-insensitive accessors instead. + /// + /// - warning: It's discouraged to use this dictionary for equality + /// key-based lookups as this may lead to issues with headers + /// that do not follow expected casing i.e., "Content-Length" + /// instead of "content-length". /// - /// - returns: The underlying headers. - public func allHeaders() -> [String: [String]] { - return self.headers + /// - returns: The underlying case-sensitive headers. + public func caseSensitiveHeaders() -> [String: [String]] { + return self.container.allHeaders() } /// Internal initializer used by builders. /// - /// - parameter headers: Headers to set. - required init(headers: [String: [String]]) { - self.headers = headers + /// - parameter container: Headers to set. + required init(container: HeadersContainer) { + self.container = container super.init() } + + /// Inialize the receiver with a given headers map. + /// + /// - parameter headers: The headers map to use. + convenience init(headers: [String: [String]]) { + self.init(container: HeadersContainer(headers: headers)) + } + + override convenience init() { + self.init(headers: [:]) + } } // MARK: - Equatable extension Headers { public override func isEqual(_ object: Any?) -> Bool { - return (object as? Self)?.headers == self.headers + return (object as? Self)?.container == self.container } } @@ -43,6 +62,6 @@ extension Headers { extension Headers { public override var description: String { - return "\(type(of: self)) \(self.headers.description)" + return "\(type(of: self)) \(self.caseSensitiveHeaders())" } } diff --git a/library/swift/HeadersBuilder.swift b/library/swift/HeadersBuilder.swift index 31f31e18f7..b245f3aee5 100644 --- a/library/swift/HeadersBuilder.swift +++ b/library/swift/HeadersBuilder.swift @@ -3,14 +3,19 @@ import Foundation private let kRestrictedPrefixes = [":", "x-envoy-mobile"] private func isRestrictedHeader(name: String) -> Bool { - return name == "host" || kRestrictedPrefixes.contains { name.hasPrefix($0) } + let isHostHeader = name.caseInsensitiveCompare("host") == .orderedSame + lazy var hasRestrictedPrefix = kRestrictedPrefixes + .contains { name.range(of: $0, options: [.caseInsensitive, .anchored]) != nil } + return isHostHeader || hasRestrictedPrefix } /// Base builder class used to construct `Headers` instances. +/// It preserves the original casing of headers and enforces +/// a case-insensitive lookup and setting of headers. /// See `{Request|Response}HeadersBuilder` for usage. @objcMembers public class HeadersBuilder: NSObject { - private(set) var headers: [String: [String]] + private(set) var container: HeadersContainer /// Append a value to the header name. /// @@ -24,7 +29,7 @@ public class HeadersBuilder: NSObject { return self } - self.headers[name, default: []].append(value) + self.container.add(name: name, value: value) return self } @@ -40,7 +45,7 @@ public class HeadersBuilder: NSObject { return self } - self.headers[name] = value + self.container.set(name: name, value: value) return self } @@ -55,7 +60,7 @@ public class HeadersBuilder: NSObject { return self } - self.headers[name] = nil + self.container.set(name: name, value: nil) return self } @@ -69,22 +74,34 @@ public class HeadersBuilder: NSObject { /// - returns: This builder. @discardableResult func internalSet(name: String, value: [String]) -> Self { - self.headers[name] = value + self.container.set(name: name, value: value) return self } + func allHeaders() -> [String: [String]] { + return self.container.allHeaders() + } + // Only explicitly implemented to work around a swiftinterface issue in Swift 5.1. This can be // removed once envoy is only built with Swift 5.2+ public override init() { - self.headers = [:] + self.container = HeadersContainer() super.init() } - /// Initialize a new builder. Subclasses should provide their own public convenience initializers. + // Initialize a new builder using the provided headers container. /// - /// - parameter headers: The headers with which to start. - required init(headers: [String: [String]]) { - self.headers = headers + /// - parameter container: The headers container to initialize the receiver with. + init(container: HeadersContainer) { + self.container = container + super.init() + } + + // Initialize a new builder. Subclasses should provide their own public convenience initializers. + // + // - parameter headers: The headers with which to start. + init(headers: [String: [String]]) { + self.container = HeadersContainer(headers: headers) super.init() } } @@ -93,6 +110,6 @@ public class HeadersBuilder: NSObject { extension HeadersBuilder { public override func isEqual(_ object: Any?) -> Bool { - return (object as? Self)?.headers == self.headers + return (object as? Self)?.container == self.container } } diff --git a/library/swift/HeadersContainer.swift b/library/swift/HeadersContainer.swift new file mode 100644 index 0000000000..2f2e631348 --- /dev/null +++ b/library/swift/HeadersContainer.swift @@ -0,0 +1,110 @@ +/// The container which manages the underlying headers map. +/// It maintains the original casing of passed header names. +/// It treats headers names as case-insensitive for the purpose +/// of header lookups and header name conflict resolutions. +struct HeadersContainer: Equatable { + private var headers: [String: Header] + + /// Represents a headers name together with all of its values. + /// It preserves the original casing of the header name. + struct Header: Equatable { + private(set) var name: String + private(set) var value: [String] + + init(name: String, value: [String] = []) { + self.name = name + self.value = value + } + + mutating func addValue(_ value: [String]) { + self.value.append(contentsOf: value) + } + + mutating func addValue(_ value: String) { + self.value.append(value) + } + } + + /// Initialize a new instance of the receiver using the provided headers map. + /// + /// - parameter headers: The headers map. + init(headers: [String: [String]]) { + var underlyingHeaders = [String: Header]() + for (name, value) in headers { + let lowercasedName = name.lowercased() + /// Dictionaries in Swift are unordered collections. Process headers with names + /// that are the same when lowercased in an alphabetical order to avoid a situation + /// in which the result of the initialization is non-derministic i.e., we want + /// "[A: ["1"]", "a: ["2"]]" headers to be always converted to ["A": ["1", "2"]] and + /// never to "a": ["2", "1"]. + /// + /// If a given header name already exists in the processed headers map, check + /// if the currently processed header name is before the existing header name as + /// determined by an alphabetical order. + guard let existingHeader = underlyingHeaders[lowercasedName] else { + underlyingHeaders[lowercasedName] = Header(name: name, value: value) + continue + } + + if existingHeader.name > name { + underlyingHeaders[lowercasedName] = + Header(name: name, value: value + existingHeader.value) + } else { + underlyingHeaders[lowercasedName]?.addValue(value) + } + } + self.headers = underlyingHeaders + } + + /// Initialize an empty headers container. + init() { + self.headers = [:] + } + + /// Add a value to a header with a given name. + /// + /// - parameter name: The name of the header. For the purpose of headers lookup + /// and header name conflict resolution, the name of the header + /// is considered to be case-insensitive. + /// - parameter value: The value to add. + mutating func add(name: String, value: String) { + self.headers[name.lowercased(), default: Header(name: name)].addValue(value) + } + + /// Set the value of a given header. + /// + /// - parameter name: The name of the header. + /// - parameter value: The value to set the header value to. + mutating func set(name: String, value: [String]?) { + guard let value = value else { + self.headers[name.lowercased()] = nil + return + } + self.headers[name.lowercased()] = Header(name: name, value: value) + } + + /// Get the value for the provided header name. + /// + /// - parameter name: The case-insensitive header name for which to + /// get the current value. + /// + /// - returns: The value associated with a given header. + func value(forName name: String) -> [String]? { + return self.headers[name.lowercased()]?.value + } + + /// Return all underlying headers. + /// + /// - returns: The underlying headers. + func allHeaders() -> [String: [String]] { + return Dictionary(uniqueKeysWithValues: self.headers.map { _, value in + return (value.name, value.value) + }) + } +} + +extension HeadersContainer: CustomStringConvertible { + var description: String { + return self.headers.description + } +} diff --git a/library/swift/RequestHeaders.swift b/library/swift/RequestHeaders.swift index e9e8e10ff1..8da6194e10 100644 --- a/library/swift/RequestHeaders.swift +++ b/library/swift/RequestHeaders.swift @@ -29,6 +29,6 @@ public class RequestHeaders: Headers { /// /// - returns: The new builder. public func toRequestHeadersBuilder() -> RequestHeadersBuilder { - return RequestHeadersBuilder(headers: self.headers) + return RequestHeadersBuilder(container: self.container) } } diff --git a/library/swift/RequestHeadersBuilder.swift b/library/swift/RequestHeadersBuilder.swift index d36a1b318d..54b8652adf 100644 --- a/library/swift/RequestHeadersBuilder.swift +++ b/library/swift/RequestHeadersBuilder.swift @@ -52,6 +52,6 @@ public final class RequestHeadersBuilder: HeadersBuilder { /// /// - returns: New instance of request headers. public func build() -> RequestHeaders { - return RequestHeaders(headers: self.headers) + return RequestHeaders(container: self.container) } } diff --git a/library/swift/RequestTrailers.swift b/library/swift/RequestTrailers.swift index a525c6acc6..0df0760f82 100644 --- a/library/swift/RequestTrailers.swift +++ b/library/swift/RequestTrailers.swift @@ -7,6 +7,6 @@ public final class RequestTrailers: Trailers { /// /// - returns: The new builder. public func toRequestTrailersBuilder() -> RequestTrailersBuilder { - return RequestTrailersBuilder(headers: self.headers) + return RequestTrailersBuilder(container: self.container) } } diff --git a/library/swift/RequestTrailersBuilder.swift b/library/swift/RequestTrailersBuilder.swift index b5b855aa00..e146a21372 100644 --- a/library/swift/RequestTrailersBuilder.swift +++ b/library/swift/RequestTrailersBuilder.swift @@ -5,13 +5,13 @@ import Foundation public final class RequestTrailersBuilder: HeadersBuilder { /// Initialize a new instance of the builder. public override convenience init() { - self.init(headers: [:]) + self.init(container: HeadersContainer(headers: [:])) } /// Build the request trailers using the current builder. /// /// - returns: New instance of request trailers. public func build() -> RequestTrailers { - return RequestTrailers(headers: self.headers) + return RequestTrailers(container: self.container) } } diff --git a/library/swift/ResponseHeaders.swift b/library/swift/ResponseHeaders.swift index a2ca41a133..ce24f6a880 100644 --- a/library/swift/ResponseHeaders.swift +++ b/library/swift/ResponseHeaders.swift @@ -11,6 +11,6 @@ public final class ResponseHeaders: Headers { /// /// - returns: The new builder. public func toResponseHeadersBuilder() -> ResponseHeadersBuilder { - return ResponseHeadersBuilder(headers: self.headers) + return ResponseHeadersBuilder(container: self.container) } } diff --git a/library/swift/ResponseHeadersBuilder.swift b/library/swift/ResponseHeadersBuilder.swift index 56c213d191..fc62d605a2 100644 --- a/library/swift/ResponseHeadersBuilder.swift +++ b/library/swift/ResponseHeadersBuilder.swift @@ -5,7 +5,7 @@ import Foundation public final class ResponseHeadersBuilder: HeadersBuilder { /// Initialize a new instance of the builder. public override convenience init() { - self.init(headers: [:]) + self.init(container: HeadersContainer(headers: [:])) } /// Add an HTTP status to the response headers. @@ -23,6 +23,6 @@ public final class ResponseHeadersBuilder: HeadersBuilder { /// /// - returns: New instance of response headers. public func build() -> ResponseHeaders { - return ResponseHeaders(headers: self.headers) + return ResponseHeaders(container: self.container) } } diff --git a/library/swift/ResponseTrailers.swift b/library/swift/ResponseTrailers.swift index 2b25a8b45e..c3e0571904 100644 --- a/library/swift/ResponseTrailers.swift +++ b/library/swift/ResponseTrailers.swift @@ -7,6 +7,6 @@ public final class ResponseTrailers: Trailers { /// /// - returns: The new builder. public func toResponseTrailersBuilder() -> ResponseTrailersBuilder { - return ResponseTrailersBuilder(headers: self.headers) + return ResponseTrailersBuilder(container: self.container) } } diff --git a/library/swift/ResponseTrailersBuilder.swift b/library/swift/ResponseTrailersBuilder.swift index 34d4a0358d..6d02fbbf0e 100644 --- a/library/swift/ResponseTrailersBuilder.swift +++ b/library/swift/ResponseTrailersBuilder.swift @@ -5,13 +5,13 @@ import Foundation public final class ResponseTrailersBuilder: HeadersBuilder { /// Initialize a new instance of the builder. public override convenience init() { - self.init(headers: [:]) + self.init(container: HeadersContainer()) } /// Build the response trailers using the current builder. /// /// - returns: New instance of response trailers. public func build() -> ResponseTrailers { - return ResponseTrailers(headers: self.headers) + return ResponseTrailers(container: self.container) } } diff --git a/library/swift/Stream.swift b/library/swift/Stream.swift index a9b416e6f9..8be28f14cb 100644 --- a/library/swift/Stream.swift +++ b/library/swift/Stream.swift @@ -26,7 +26,7 @@ public class Stream: NSObject { /// - returns: This stream, for chaining syntax. @discardableResult public func sendHeaders(_ headers: RequestHeaders, endStream: Bool) -> Stream { - self.underlyingStream.sendHeaders(headers.headers, close: endStream) + self.underlyingStream.sendHeaders(headers.caseSensitiveHeaders(), close: endStream) return self } @@ -57,7 +57,7 @@ public class Stream: NSObject { /// /// - parameter trailers: Trailers with which to close the stream. public func close(trailers: RequestTrailers) { - self.underlyingStream.sendTrailers(trailers.headers) + self.underlyingStream.sendTrailers(trailers.caseSensitiveHeaders()) } /// Close the stream with a data frame. diff --git a/library/swift/filters/Filter.swift b/library/swift/filters/Filter.swift index ebd87a5024..4bce1feafd 100644 --- a/library/swift/filters/Filter.swift +++ b/library/swift/filters/Filter.swift @@ -27,7 +27,7 @@ extension EnvoyHTTPFilter { streamIntel: StreamIntel(streamIntel)) switch result { case .continue(let headers): - return [kEnvoyFilterHeadersStatusContinue, headers.headers] + return [kEnvoyFilterHeadersStatusContinue, headers.caseSensitiveHeaders()] case .stopIteration: return [kEnvoyFilterHeadersStatusStopIteration, NSNull()] } @@ -44,7 +44,10 @@ extension EnvoyHTTPFilter { case .stopIterationNoBuffer: return [kEnvoyFilterDataStatusStopIterationNoBuffer, NSNull()] case .resumeIteration(let headers, let data): - return [kEnvoyFilterDataStatusResumeIteration, data, headers?.headers as Any] + return [ + kEnvoyFilterDataStatusResumeIteration, data, + headers?.caseSensitiveHeaders() as Any, + ] } } @@ -53,14 +56,14 @@ extension EnvoyHTTPFilter { streamIntel: StreamIntel(streamIntel)) switch result { case .continue(let trailers): - return [kEnvoyFilterTrailersStatusContinue, trailers.headers] + return [kEnvoyFilterTrailersStatusContinue, trailers.caseSensitiveHeaders()] case .stopIteration: return [kEnvoyFilterTrailersStatusStopIteration, NSNull()] case .resumeIteration(let headers, let data, let trailers): return [ kEnvoyFilterTrailersStatusResumeIteration, - trailers.headers, - headers?.headers as Any, + trailers.caseSensitiveHeaders(), + headers?.caseSensitiveHeaders() as Any, data as Any, ] } @@ -74,7 +77,7 @@ extension EnvoyHTTPFilter { streamIntel: StreamIntel(streamIntel)) switch result { case .continue(let headers): - return [kEnvoyFilterHeadersStatusContinue, headers.headers] + return [kEnvoyFilterHeadersStatusContinue, headers.caseSensitiveHeaders()] case .stopIteration: return [kEnvoyFilterHeadersStatusStopIteration, NSNull()] } @@ -91,7 +94,10 @@ extension EnvoyHTTPFilter { case .stopIterationNoBuffer: return [kEnvoyFilterDataStatusStopIterationNoBuffer, NSNull()] case .resumeIteration(let headers, let data): - return [kEnvoyFilterDataStatusResumeIteration, data, headers?.headers as Any] + return [ + kEnvoyFilterDataStatusResumeIteration, data, + headers?.caseSensitiveHeaders() as Any, + ] } } @@ -100,14 +106,14 @@ extension EnvoyHTTPFilter { streamIntel: StreamIntel(streamIntel)) switch result { case .continue(let trailers): - return [kEnvoyFilterTrailersStatusContinue, trailers.headers] + return [kEnvoyFilterTrailersStatusContinue, trailers.caseSensitiveHeaders()] case .stopIteration: return [kEnvoyFilterTrailersStatusStopIteration, NSNull()] case .resumeIteration(let headers, let data, let trailers): return [ kEnvoyFilterTrailersStatusResumeIteration, - trailers.headers, - headers?.headers as Any, + trailers.caseSensitiveHeaders(), + headers?.caseSensitiveHeaders() as Any, data as Any, ] } @@ -146,9 +152,9 @@ extension EnvoyHTTPFilter { case .resumeIteration(let headers, let data, let trailers): return [ kEnvoyFilterResumeStatusResumeIteration, - headers?.headers as Any, + headers?.caseSensitiveHeaders() as Any, data as Any, - trailers?.headers as Any, + trailers?.caseSensitiveHeaders() as Any, ] } } @@ -172,9 +178,9 @@ extension EnvoyHTTPFilter { case .resumeIteration(let headers, let data, let trailers): return [ kEnvoyFilterResumeStatusResumeIteration, - headers?.headers as Any, + headers?.caseSensitiveHeaders() as Any, data as Any, - trailers?.headers as Any, + trailers?.caseSensitiveHeaders() as Any, ] } } diff --git a/library/swift/grpc/GRPCRequestHeaders.swift b/library/swift/grpc/GRPCRequestHeaders.swift index a5c589b5f8..cdf55cca19 100644 --- a/library/swift/grpc/GRPCRequestHeaders.swift +++ b/library/swift/grpc/GRPCRequestHeaders.swift @@ -7,6 +7,6 @@ public final class GRPCRequestHeaders: RequestHeaders { /// /// - returns: The new builder. public func toGRPCRequestHeadersBuilder() -> GRPCRequestHeadersBuilder { - return GRPCRequestHeadersBuilder(headers: self.headers) + return GRPCRequestHeadersBuilder(container: self.container) } } diff --git a/library/swift/grpc/GRPCRequestHeadersBuilder.swift b/library/swift/grpc/GRPCRequestHeadersBuilder.swift index 177ddf9003..0f3fc8ba1b 100644 --- a/library/swift/grpc/GRPCRequestHeadersBuilder.swift +++ b/library/swift/grpc/GRPCRequestHeadersBuilder.swift @@ -40,6 +40,6 @@ public final class GRPCRequestHeadersBuilder: HeadersBuilder { /// /// - returns: New instance of request headers. public func build() -> GRPCRequestHeaders { - return GRPCRequestHeaders(headers: self.headers) + return GRPCRequestHeaders(container: self.container) } } diff --git a/library/swift/mocks/MockStream.swift b/library/swift/mocks/MockStream.swift index 8aa331f03c..4bbd3fcadb 100644 --- a/library/swift/mocks/MockStream.swift +++ b/library/swift/mocks/MockStream.swift @@ -52,7 +52,8 @@ public final class MockStream: Stream { /// - parameter headers: Response headers to receive. /// - parameter endStream: Whether this is a headers-only response. public func receiveHeaders(_ headers: ResponseHeaders, endStream: Bool) { - self.mockStream.callbacks.onHeaders(headers.headers, endStream, EnvoyStreamIntel()) + self.mockStream.callbacks.onHeaders(headers.caseSensitiveHeaders(), endStream, + EnvoyStreamIntel()) } /// Simulate response data coming back over the stream. @@ -67,7 +68,7 @@ public final class MockStream: Stream { /// /// - parameter trailers: Response trailers to receive. public func receiveTrailers(_ trailers: ResponseTrailers) { - self.mockStream.callbacks.onTrailers(trailers.headers, EnvoyStreamIntel()) + self.mockStream.callbacks.onTrailers(trailers.caseSensitiveHeaders(), EnvoyStreamIntel()) } /// Simulate the stream receiving a cancellation signal from Envoy. diff --git a/test/kotlin/io/envoyproxy/envoymobile/RequestHeadersBuilderTest.kt b/test/kotlin/io/envoyproxy/envoymobile/RequestHeadersBuilderTest.kt index 7fb43c3c59..88d0b0f5f1 100644 --- a/test/kotlin/io/envoyproxy/envoymobile/RequestHeadersBuilderTest.kt +++ b/test/kotlin/io/envoyproxy/envoymobile/RequestHeadersBuilderTest.kt @@ -100,11 +100,13 @@ class RequestHeadersBuilderTest { .add(":x-foo", "123") .add("x-envoy-mobile-foo", "abc") .add("host", "example.com") + .add("hostWithSuffix", "foo.bar") .build() assertThat(headers.allHeaders()).doesNotContainKey(":x-foo") assertThat(headers.allHeaders()).doesNotContainKey("x-envoy-mobile-foo") assertThat(headers.allHeaders()).doesNotContainKey("host") + assertThat(headers.value("hostWithSuffix")).containsExactly("foo.bar") } @Test diff --git a/test/swift/BUILD b/test/swift/BUILD index 5f98ee480e..1839c45e50 100644 --- a/test/swift/BUILD +++ b/test/swift/BUILD @@ -9,6 +9,7 @@ envoy_mobile_swift_test( "GRPCRequestHeadersBuilderTests.swift", "GRPCStreamTests.swift", "HeadersBuilderTests.swift", + "HeadersContainerTests.swift", "PulseClientImplTests.swift", "RequestHeadersBuilderTests.swift", "ResponseHeadersTests.swift", diff --git a/test/swift/HeadersBuilderTests.swift b/test/swift/HeadersBuilderTests.swift index bf8990026b..a92185ad25 100644 --- a/test/swift/HeadersBuilderTests.swift +++ b/test/swift/HeadersBuilderTests.swift @@ -10,7 +10,7 @@ final class HeadersBuilderTests: XCTestCase { let headers = HeadersBuilder(headers: [:]) .add(name: "x-foo", value: "1") .add(name: "x-foo", value: "2") - .headers + .allHeaders() XCTAssertEqual(["1", "2"], headers["x-foo"]) } @@ -19,7 +19,7 @@ final class HeadersBuilderTests: XCTestCase { .add(name: "x-foo", value: "1") .add(name: "x-foo", value: "2") .remove(name: "x-foo") - .headers + .allHeaders() XCTAssertNil(headers["x-foo"]) } @@ -28,7 +28,7 @@ final class HeadersBuilderTests: XCTestCase { .add(name: "x-foo", value: "123") .add(name: "x-bar", value: "abc") .remove(name: "x-foo") - .headers + .allHeaders() XCTAssertEqual(["x-bar": ["abc"]], headers) } @@ -36,21 +36,49 @@ final class HeadersBuilderTests: XCTestCase { let headers = HeadersBuilder(headers: [:]) .add(name: "x-foo", value: "123") .set(name: "x-foo", value: ["abc"]) - .headers + .allHeaders() XCTAssertEqual(["x-foo": ["abc"]], headers) } + func testInitializationIsCaseInsensitivePreservesCasingAndProcessesInAlphabeticalOrder() { + let headers = HeadersBuilder(headers: ["a": ["456"], "A": ["123"]]) + XCTAssertEqual(["A": ["123", "456"]], headers.allHeaders()) + } + + func testAddingHeaderIsCaseInsensitiveAndHeaderCasingIsPreserved() { + let headers = HeadersBuilder(headers: [:]) + headers.add(name: "fOo", value: "abc") + headers.add(name: "foo", value: "123") + XCTAssertEqual(["fOo": ["abc", "123"]], headers.allHeaders()) + } + + func testSettingHeaderIsCaseInsensitiveAndHeaderCasingIsPreserved() { + let headers = HeadersBuilder(headers: [:]) + headers.set(name: "foo", value: ["123"]) + headers.set(name: "fOo", value: ["abc"]) + XCTAssertEqual(["fOo": ["abc"]], headers.allHeaders()) + } + + func testRemovingHeaderIsCaseInsensitive() { + let headers = HeadersBuilder(headers: [:]) + headers.set(name: "foo", value: ["123"]) + headers.remove(name: "fOo") + XCTAssertEqual([:], headers.allHeaders()) + } + func testRestrictedHeadersAreNotSettable() { let headers = RequestHeadersBuilder(method: .get, authority: "example.com", path: "/") .add(name: "host", value: "example.com") + .add(name: "hostWithSuffix", value: "foo.bar") .set(name: ":scheme", value: ["http"]) .set(name: ":path", value: ["/nope"]) - .headers + .allHeaders() let expected = [ ":authority": ["example.com"], ":path": ["/"], ":method": ["GET"], ":scheme": ["https"], + "hostWithSuffix": ["foo.bar"], ] XCTAssertEqual(expected, headers) } diff --git a/test/swift/HeadersContainerTests.swift b/test/swift/HeadersContainerTests.swift new file mode 100644 index 0000000000..d7ecd3b976 --- /dev/null +++ b/test/swift/HeadersContainerTests.swift @@ -0,0 +1,74 @@ +@testable import Envoy +import XCTest + +final class HeadersContainerTests: XCTestCase { + func testInitializationPreservesAllHeadersFromInputHeadersMap() { + let container = HeadersContainer(headers: ["a": ["456"], "b": ["123"]]) + XCTAssertEqual(["a": ["456"], "b": ["123"]], container.allHeaders()) + } + + func testInitializationIsCaseInsensitivePreservesCasingAndProcessesInAlphabeticalOrder() { + let container = HeadersContainer(headers: ["a": ["456"], "A": ["123"]]) + XCTAssertEqual(["A": ["123", "456"]], container.allHeaders()) + } + + func testAddingHeaderValueAddsToListOfHeaders() { + var container = HeadersContainer() + container.add(name: "x-foo", value: "1") + container.add(name: "x-foo", value: "2") + + XCTAssertEqual(["1", "2"], container.value(forName: "x-foo")) + } + + func testAddingHeaderValueIsCaseInsensitiveAndPreservesHeaderNameCasing() { + var container = HeadersContainer() + container.add(name: "x-FOO", value: "1") + container.add(name: "x-foo", value: "2") + + XCTAssertEqual(["1", "2"], container.value(forName: "x-foo")) + XCTAssertEqual(["x-FOO": ["1", "2"]], container.allHeaders()) + } + + func testSettingHeaderAddsToListOfHeaders() { + var container = HeadersContainer() + container.set(name: "x-foo", value: ["abc"]) + + XCTAssertEqual(["abc"], container.value(forName: "x-foo")) + } + + func testSettingHeaderOverridesPreviousHeaderValues() { + var container = HeadersContainer() + container.add(name: "x-FOO", value: "1") + container.add(name: "x-foo", value: "2") + container.set(name: "x-foo", value: ["3"]) + + XCTAssertEqual(["3"], container.value(forName: "x-foo")) + } + + func testSettingHeaderToNilRemovesAllOfItsValues() { + var container = HeadersContainer() + container.add(name: "x-foo", value: "1") + container.add(name: "x-foo", value: "2") + container.set(name: "x-foo", value: nil) + + XCTAssertNil(container.value(forName: "x-foo")) + } + + func testSettingHeaderToNilPerformsCaseInsensitiveHeaderNameLookup() { + var container = HeadersContainer() + container.add(name: "x-FOO", value: "1") + container.add(name: "x-foo", value: "2") + container.set(name: "x-foo", value: nil) + + XCTAssertNil(container.value(forName: "x-foo")) + } + + func testLookupIsCaseInsensitive() { + var container = HeadersContainer() + container.add(name: "x-FOO", value: "1") + + XCTAssertEqual(["1"], container.value(forName: "x-foo")) + XCTAssertEqual(["1"], container.value(forName: "x-fOo")) + XCTAssertEqual(["1"], container.value(forName: "x-FOO")) + } +} diff --git a/test/swift/apps/baseline/ViewController.swift b/test/swift/apps/baseline/ViewController.swift index 8676c01002..f9f998e65d 100644 --- a/test/swift/apps/baseline/ViewController.swift +++ b/test/swift/apps/baseline/ViewController.swift @@ -70,7 +70,7 @@ final class ViewController: UITableViewController { let statusCode = headers.httpStatus.map(String.init) ?? "nil" let message = "received headers with status \(statusCode)" - let headerMessage = headers.allHeaders() + let headerMessage = headers.caseSensitiveHeaders() .filter { kFilteredHeaders.contains($0.key) } .map { "\($0.key): \($0.value.joined(separator: ", "))" } .joined(separator: "\n") diff --git a/test/swift/apps/experimental/ViewController.swift b/test/swift/apps/experimental/ViewController.swift index 59644895ed..02f043617a 100644 --- a/test/swift/apps/experimental/ViewController.swift +++ b/test/swift/apps/experimental/ViewController.swift @@ -72,7 +72,7 @@ final class ViewController: UITableViewController { let statusCode = headers.httpStatus.map(String.init) ?? "nil" let message = "received headers with status \(statusCode)" - let headerMessage = headers.allHeaders() + let headerMessage = headers.caseSensitiveHeaders() .filter { kFilteredHeaders.contains($0.key) } .map { "\($0.key): \($0.value.joined(separator: ", "))" } .joined(separator: "\n")