diff --git a/mobile/examples/java/hello_world/MainActivity.java b/mobile/examples/java/hello_world/MainActivity.java index 5d1ebcd4ac55..d16842dac9f5 100644 --- a/mobile/examples/java/hello_world/MainActivity.java +++ b/mobile/examples/java/hello_world/MainActivity.java @@ -108,7 +108,8 @@ private void makeRequest() { String message = "received headers with status " + status; StringBuilder sb = new StringBuilder(); - for (Map.Entry> entry : responseHeaders.getHeaders().entrySet()) { + for (Map.Entry> entry : + responseHeaders.caseSensitiveHeaders().entrySet()) { String name = entry.getKey(); if (FILTERED_HEADERS.contains(name)) { sb.append(name).append(": ").append(String.join(", ", entry.getValue())).append("\n"); diff --git a/mobile/examples/kotlin/hello_world/MainActivity.kt b/mobile/examples/kotlin/hello_world/MainActivity.kt index d58c7b40c133..f03f4c669bec 100644 --- a/mobile/examples/kotlin/hello_world/MainActivity.kt +++ b/mobile/examples/kotlin/hello_world/MainActivity.kt @@ -117,7 +117,7 @@ class MainActivity : Activity() { val message = "received headers with status $status" val sb = StringBuilder() - for ((name, value) in responseHeaders.headers) { + for ((name, value) in responseHeaders.caseSensitiveHeaders()) { if (name in FILTERED_HEADERS) { sb.append(name).append(": ").append(value.joinToString()).append("\n") } diff --git a/mobile/library/kotlin/io/envoyproxy/envoymobile/BUILD b/mobile/library/kotlin/io/envoyproxy/envoymobile/BUILD index bda143dd19a7..e1435b528853 100644 --- a/mobile/library/kotlin/io/envoyproxy/envoymobile/BUILD +++ b/mobile/library/kotlin/io/envoyproxy/envoymobile/BUILD @@ -44,6 +44,7 @@ envoy_mobile_kt_library( "EnvoyError.kt", "Headers.kt", "HeadersBuilder.kt", + "HeadersContainer.kt", "KeyValueStore.kt", "LogLevel.kt", "PulseClient.kt", diff --git a/mobile/library/kotlin/io/envoyproxy/envoymobile/Headers.kt b/mobile/library/kotlin/io/envoyproxy/envoymobile/Headers.kt index 54b4de1e134a..a36dba772c35 100644 --- a/mobile/library/kotlin/io/envoyproxy/envoymobile/Headers.kt +++ b/mobile/library/kotlin/io/envoyproxy/envoymobile/Headers.kt @@ -5,35 +5,37 @@ package io.envoyproxy.envoymobile * To instantiate new instances, see `{Request|Response}HeadersBuilder`. */ open class Headers { - @Suppress("MemberNameEqualsClassName") - val headers: Map> + val container: HeadersContainer /** * Internal constructor used by builders. * - * @param headers: Headers to set. + * @param container: The headers container to set. */ - protected constructor(headers: Map>) { - this.headers = headers + internal constructor(container: HeadersContainer) { + this.container = container } /** - * Get the value for the provided header name. + * Get the value for the provided header name. 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". * * @param name: Header name for which to get the current value. * - * @return List?, The current headers specified for the provided name. + * @return The current headers specified for the provided name. */ fun value(name: String): List? { - return headers[name] + return container.value(name) } /** * Accessor for all underlying headers as a map. * - * @return Map>, The underlying headers. + * @return The underlying headers. */ - fun allHeaders(): Map> { - return headers + fun caseSensitiveHeaders(): Map> { + return container.caseSensitiveHeaders() } } diff --git a/mobile/library/kotlin/io/envoyproxy/envoymobile/HeadersBuilder.kt b/mobile/library/kotlin/io/envoyproxy/envoymobile/HeadersBuilder.kt index 184b2885ac66..21906ad20a17 100644 --- a/mobile/library/kotlin/io/envoyproxy/envoymobile/HeadersBuilder.kt +++ b/mobile/library/kotlin/io/envoyproxy/envoymobile/HeadersBuilder.kt @@ -5,15 +5,15 @@ package io.envoyproxy.envoymobile * See `{Request|Response}HeadersBuilder` for usage. */ open class HeadersBuilder { - protected val headers: MutableMap> + protected val container: HeadersContainer /** * Instantiate a new builder, only used by child classes. * - * @param headers: The headers to start with. + * @param container: The headers container to start with. */ - protected constructor(headers: MutableMap>) { - this.headers = headers + internal constructor(container: HeadersContainer) { + this.container = container } /** @@ -28,7 +28,7 @@ open class HeadersBuilder { if (isRestrictedHeader(name)) { return this } - headers.getOrPut(name) { mutableListOf() }.add(value) + container.add(name, value) return this } @@ -44,7 +44,7 @@ open class HeadersBuilder { if (isRestrictedHeader(name)) { return this } - headers[name] = value + container.set(name, value) return this } @@ -59,7 +59,7 @@ open class HeadersBuilder { if (isRestrictedHeader(name)) { return this } - headers.remove(name) + container.remove(name) return this } @@ -72,10 +72,10 @@ open class HeadersBuilder { * @return HeadersBuilder, This builder. */ internal open fun internalSet(name: String, value: MutableList): HeadersBuilder { - headers[name] = value + container.set(name, value) return this } private fun isRestrictedHeader(name: String) = name.startsWith(":") || - name.startsWith("x-envoy-mobile") || name == "host" + name.startsWith("x-envoy-mobile", ignoreCase = true) || name.equals("host", ignoreCase = true) } diff --git a/mobile/library/kotlin/io/envoyproxy/envoymobile/HeadersContainer.kt b/mobile/library/kotlin/io/envoyproxy/envoymobile/HeadersContainer.kt new file mode 100644 index 000000000000..ae93e631843c --- /dev/null +++ b/mobile/library/kotlin/io/envoyproxy/envoymobile/HeadersContainer.kt @@ -0,0 +1,135 @@ +package io.envoyproxy.envoymobile + +/** + * The container that 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. + */ +open class HeadersContainer { + protected val headers: MutableMap + + /** + * Represents a header name together with all of its values. + * It preserves the original casing of the header name. + */ + data class Header(val name: String, var value: MutableList) { + constructor(name: String) : this(name, mutableListOf()) + + fun add(value: List) { + this.value.addAll(value) + } + + fun add(value: String) { + this.value.add(value) + } + } + + /** + * Instantiate a new instance of the receiver using the provided headers map + * + * @param headers: The headers to start with. + */ + internal constructor(headers: Map>) { + var underlyingHeaders = mutableMapOf() + /** + * Dictionaries 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 + * mapOf("A" to listOf("1"), "a" to listOf("2")) headers to be always converted to + * mapOf("A" to listOf("1", "2")) and never to mapOf("a" to listOf("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. + */ + headers.forEach { + val lowercased = it.key.lowercase() + val existing = underlyingHeaders[lowercased] + + if (existing == null) { + underlyingHeaders[lowercased] = Header(it.key, it.value) + } else if (existing.name > it.key) { + underlyingHeaders[lowercased] = Header(it.key, (it.value + existing.value).toMutableList()) + } else { + underlyingHeaders[lowercased]?.add(it.value) + } + } + + this.headers = underlyingHeaders + } + + companion object { + /** + * Create a new instance of the receiver using a provider headers map. + * Not implemented as a constructor due to conflicting JVM signatures with + * other constructors. + * + * @param headers: The headers to create the container with. + */ + fun create(headers: Map>) : HeadersContainer { + return HeadersContainer(headers.mapValues { it.value.toMutableList() }) + } + } + + /** + * Add a value to a header with a given name. + * + * @param 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. + * @param value The value to add. + */ + fun add(name: String, value: String) { + val lowercased = name.lowercase() + headers[lowercased]?.let { it.add(value) } ?: run { + headers.put(lowercased, Header(name, mutableListOf(value))) + } + } + + + /** + * Set the value of a given header. + * + * @param name The name of the header. + * @param value The value to set the header value to. + */ + fun set(name: String, value: List) { + headers[name.lowercase()] = Header(name, value.toMutableList()) + } + + /** + * Remove a given header. + * + * @param name The name of the header to remove. + */ + fun remove(name: String) { + headers.remove(name.lowercase()) + } + + /** + * Get the value for the provided header name. + * + * @param name The case-insensitive header name for which to + * get the current value. + * @return The value associated with a given header. + */ + fun value(name: String): List? { + return headers[name.lowercase()]?.value + } + + /** + * Accessor for all underlying case-sensitive headers. When possible, + * use case-insensitive accessors instead. + * + * @return The underlying headers. + */ + fun caseSensitiveHeaders(): Map> { + var caseSensitiveHeaders = mutableMapOf>() + headers.forEach { + caseSensitiveHeaders.put(it.value.name, it.value.value) + } + + return caseSensitiveHeaders + } +} diff --git a/mobile/library/kotlin/io/envoyproxy/envoymobile/RequestHeaders.kt b/mobile/library/kotlin/io/envoyproxy/envoymobile/RequestHeaders.kt index aa27080facc6..5c0c33da7d57 100644 --- a/mobile/library/kotlin/io/envoyproxy/envoymobile/RequestHeaders.kt +++ b/mobile/library/kotlin/io/envoyproxy/envoymobile/RequestHeaders.kt @@ -9,7 +9,9 @@ open class RequestHeaders : Headers { * * @param headers: Headers to set. */ - internal constructor(headers: Map>) : super(headers) + internal constructor(headers: Map>) : super(HeadersContainer.create(headers)) + + internal constructor(container: HeadersContainer): super(container) /** * Method for the request. @@ -49,9 +51,5 @@ open class RequestHeaders : Headers { * * @return RequestHeadersBuilder, The new builder. */ - fun toRequestHeadersBuilder() = RequestHeadersBuilder( - headers.mapValues { - it.value.toMutableList() - }.toMutableMap() - ) + fun toRequestHeadersBuilder() = RequestHeadersBuilder(container) } diff --git a/mobile/library/kotlin/io/envoyproxy/envoymobile/RequestHeadersBuilder.kt b/mobile/library/kotlin/io/envoyproxy/envoymobile/RequestHeadersBuilder.kt index 0e5450c97547..ffe75da9d813 100644 --- a/mobile/library/kotlin/io/envoyproxy/envoymobile/RequestHeadersBuilder.kt +++ b/mobile/library/kotlin/io/envoyproxy/envoymobile/RequestHeadersBuilder.kt @@ -18,14 +18,15 @@ class RequestHeadersBuilder : HeadersBuilder { authority: String, path: String ) : - super( - mutableMapOf( + super(HeadersContainer( + mapOf( ":authority" to mutableListOf(authority), ":method" to mutableListOf(method.stringValue), ":path" to mutableListOf(path), ":scheme" to mutableListOf(scheme) ) ) + ) /** * Instantiate a new builder. Used only by RequestHeaders to convert back to @@ -33,7 +34,14 @@ class RequestHeadersBuilder : HeadersBuilder { * * @param headers: The headers to start with. */ - internal constructor(headers: MutableMap>) : super(headers) + internal constructor(headers: Map>) : super(HeadersContainer(headers)) + + /** + * Instantiate a new builder. + * + * @param container: The headers container to start with. + */ + internal constructor(container: HeadersContainer) : super(container) override fun add(name: String, value: String): RequestHeadersBuilder { super.add(name, value) @@ -92,6 +100,6 @@ class RequestHeadersBuilder : HeadersBuilder { * @return RequestHeaders, New instance of request headers. */ fun build(): RequestHeaders { - return RequestHeaders(headers) + return RequestHeaders(container) } } diff --git a/mobile/library/kotlin/io/envoyproxy/envoymobile/RequestTrailers.kt b/mobile/library/kotlin/io/envoyproxy/envoymobile/RequestTrailers.kt index ed7d973618db..1254a1e62b30 100644 --- a/mobile/library/kotlin/io/envoyproxy/envoymobile/RequestTrailers.kt +++ b/mobile/library/kotlin/io/envoyproxy/envoymobile/RequestTrailers.kt @@ -12,14 +12,17 @@ class RequestTrailers : Trailers { */ internal constructor(trailers: Map>) : super(trailers) + /** + * Instantiate a new builder. + * + * @param container: The headers container to start with. + */ + internal constructor(container: HeadersContainer) : super(container) + /** * Convert the trailers back to a builder for mutation. * * @return RequestTrailersBuilder, The new builder. */ - fun toRequestTrailersBuilder() = RequestTrailersBuilder( - headers.mapValues { - it.value.toMutableList() - }.toMutableMap() - ) + fun toRequestTrailersBuilder() = RequestTrailersBuilder(container) } diff --git a/mobile/library/kotlin/io/envoyproxy/envoymobile/RequestTrailersBuilder.kt b/mobile/library/kotlin/io/envoyproxy/envoymobile/RequestTrailersBuilder.kt index fc82d15fb929..b701bb6e89f1 100644 --- a/mobile/library/kotlin/io/envoyproxy/envoymobile/RequestTrailersBuilder.kt +++ b/mobile/library/kotlin/io/envoyproxy/envoymobile/RequestTrailersBuilder.kt @@ -4,18 +4,26 @@ package io.envoyproxy.envoymobile * Builder used for constructing instances of `RequestTrailers`. */ class RequestTrailersBuilder : HeadersBuilder { - /** - * Initialize a new instance of the builder. + /* + * Instantiate a new builder. */ - constructor() : super(mutableMapOf()) + internal constructor() : super(HeadersContainer(mapOf())) - /** + /* + * Instantiate a new instance of the builder. + * + * @param container: The headers container to start with. + */ + internal constructor(container: HeadersContainer) : super(container) + + /* * Instantiate a new builder. Used only by RequestTrailers to convert back to * RequestTrailersBuilder. * * @param trailers: The trailers to start with. */ - internal constructor(trailers: MutableMap>) : super(trailers) + internal constructor(trailers: MutableMap>) + : super(HeadersContainer(trailers)) override fun add(name: String, value: String): RequestTrailersBuilder { super.add(name, value) @@ -43,6 +51,6 @@ class RequestTrailersBuilder : HeadersBuilder { * @return RequestTrailers, New instance of request trailers. */ fun build(): RequestTrailers { - return RequestTrailers(headers) + return RequestTrailers(container) } } diff --git a/mobile/library/kotlin/io/envoyproxy/envoymobile/ResponseHeaders.kt b/mobile/library/kotlin/io/envoyproxy/envoymobile/ResponseHeaders.kt index 65557038931b..dbfdd8e383f8 100644 --- a/mobile/library/kotlin/io/envoyproxy/envoymobile/ResponseHeaders.kt +++ b/mobile/library/kotlin/io/envoyproxy/envoymobile/ResponseHeaders.kt @@ -9,7 +9,9 @@ class ResponseHeaders : Headers { * * @param headers: Headers to set. */ - internal constructor(headers: Map>) : super(headers) + internal constructor(headers: Map>) : super(HeadersContainer.create(headers)) + + internal constructor(container: HeadersContainer) : super(container) /** * HTTP status code received with the response. @@ -23,9 +25,5 @@ class ResponseHeaders : Headers { * * @return ResponseHeadersBuilder, The new builder. */ - fun toResponseHeadersBuilder() = ResponseHeadersBuilder( - headers.mapValues { - it.value.toMutableList() - }.toMutableMap() - ) + fun toResponseHeadersBuilder() = ResponseHeadersBuilder(container) } diff --git a/mobile/library/kotlin/io/envoyproxy/envoymobile/ResponseHeadersBuilder.kt b/mobile/library/kotlin/io/envoyproxy/envoymobile/ResponseHeadersBuilder.kt index 81c744d50fdb..7b47675eaeef 100644 --- a/mobile/library/kotlin/io/envoyproxy/envoymobile/ResponseHeadersBuilder.kt +++ b/mobile/library/kotlin/io/envoyproxy/envoymobile/ResponseHeadersBuilder.kt @@ -5,18 +5,26 @@ package io.envoyproxy.envoymobile */ class ResponseHeadersBuilder : HeadersBuilder { - /** - * Initialize a new instance of the builder. + /* + * Instantiate a new builder. */ - constructor() : super(mutableMapOf()) + internal constructor() : super(HeadersContainer(mapOf())) - /** + /* * Instantiate a new builder. Used only by ResponseHeaders to convert back to * ResponseHeadersBuilder. * * @param headers: The headers to start with. */ - internal constructor(headers: MutableMap>) : super(headers) + internal constructor(headers: MutableMap>) + : super(HeadersContainer(headers)) + + /* + * Instantiate a new builder. + * + * @param container: The headers container to start with. + */ + internal constructor(container: HeadersContainer) : super(container) override fun add(name: String, value: String): ResponseHeadersBuilder { super.add(name, value) @@ -59,6 +67,6 @@ class ResponseHeadersBuilder : HeadersBuilder { * @return ResponseHeaders, New instance of response headers. */ fun build(): ResponseHeaders { - return ResponseHeaders(headers) + return ResponseHeaders(container) } } diff --git a/mobile/library/kotlin/io/envoyproxy/envoymobile/ResponseTrailers.kt b/mobile/library/kotlin/io/envoyproxy/envoymobile/ResponseTrailers.kt index a51c10059b55..c61f71dcac85 100644 --- a/mobile/library/kotlin/io/envoyproxy/envoymobile/ResponseTrailers.kt +++ b/mobile/library/kotlin/io/envoyproxy/envoymobile/ResponseTrailers.kt @@ -12,14 +12,17 @@ class ResponseTrailers : Trailers { */ internal constructor(trailers: Map>) : super(trailers) + /** + * Instantiate a new builder. + * + * @param container: The headers container to start with. + */ + internal constructor(container: HeadersContainer) : super(container) + /** * Convert the trailers back to a builder for mutation. * * @return ResponseTrailersBuilder, The new builder. */ - fun toResponseTrailersBuilder() = ResponseTrailersBuilder( - headers.mapValues { - it.value.toMutableList() - }.toMutableMap() - ) + fun toResponseTrailersBuilder() = ResponseTrailersBuilder(container) } diff --git a/mobile/library/kotlin/io/envoyproxy/envoymobile/ResponseTrailersBuilder.kt b/mobile/library/kotlin/io/envoyproxy/envoymobile/ResponseTrailersBuilder.kt index ad97289373ef..0e21ce27d457 100644 --- a/mobile/library/kotlin/io/envoyproxy/envoymobile/ResponseTrailersBuilder.kt +++ b/mobile/library/kotlin/io/envoyproxy/envoymobile/ResponseTrailersBuilder.kt @@ -7,7 +7,7 @@ class ResponseTrailersBuilder : HeadersBuilder { /** * Initialize a new instance of the builder. */ - constructor() : super(mutableMapOf()) + constructor() : super(HeadersContainer(mapOf())) /** * Instantiate a new builder. Used only by ResponseTrailers to convert back to @@ -15,7 +15,10 @@ class ResponseTrailersBuilder : HeadersBuilder { * * @param trailers: The trailers to start with. */ - internal constructor(trailers: MutableMap>) : super(trailers) + internal constructor(trailers: MutableMap>) + : super(HeadersContainer(trailers)) + + internal constructor(container: HeadersContainer) : super(container) override fun add(name: String, value: String): ResponseTrailersBuilder { super.add(name, value) @@ -43,6 +46,6 @@ class ResponseTrailersBuilder : HeadersBuilder { * @return ResponseTrailers, New instance of response trailers. */ fun build(): ResponseTrailers { - return ResponseTrailers(headers) + return ResponseTrailers(container) } } diff --git a/mobile/library/kotlin/io/envoyproxy/envoymobile/Stream.kt b/mobile/library/kotlin/io/envoyproxy/envoymobile/Stream.kt index b51fe23b6a89..5c86dc26580b 100644 --- a/mobile/library/kotlin/io/envoyproxy/envoymobile/Stream.kt +++ b/mobile/library/kotlin/io/envoyproxy/envoymobile/Stream.kt @@ -20,7 +20,7 @@ open class Stream( * @return This stream, for chaining syntax. */ open fun sendHeaders(headers: RequestHeaders, endStream: Boolean): Stream { - underlyingStream.sendHeaders(headers.allHeaders(), endStream) + underlyingStream.sendHeaders(headers.caseSensitiveHeaders(), endStream) return this } @@ -58,7 +58,7 @@ open class Stream( * @param trailers Trailers with which to close the stream. */ open fun close(trailers: RequestTrailers) { - underlyingStream.sendTrailers(trailers.allHeaders()) + underlyingStream.sendTrailers(trailers.caseSensitiveHeaders()) } /** diff --git a/mobile/library/kotlin/io/envoyproxy/envoymobile/Trailers.kt b/mobile/library/kotlin/io/envoyproxy/envoymobile/Trailers.kt index ef7f7b2481f8..6e806ce9b39b 100644 --- a/mobile/library/kotlin/io/envoyproxy/envoymobile/Trailers.kt +++ b/mobile/library/kotlin/io/envoyproxy/envoymobile/Trailers.kt @@ -10,5 +10,8 @@ open class Trailers : Headers { * * @param trailers: Trailers to set. */ - protected constructor(trailers: Map>) : super(trailers) + protected constructor(trailers: Map>) + : super(HeadersContainer.create(trailers)) + + protected constructor(container: HeadersContainer) : super(container) } diff --git a/mobile/library/kotlin/io/envoyproxy/envoymobile/filters/Filter.kt b/mobile/library/kotlin/io/envoyproxy/envoymobile/filters/Filter.kt index d83845ff94df..ba9d419e2928 100644 --- a/mobile/library/kotlin/io/envoyproxy/envoymobile/filters/Filter.kt +++ b/mobile/library/kotlin/io/envoyproxy/envoymobile/filters/Filter.kt @@ -32,7 +32,7 @@ internal class EnvoyHTTPFilterAdapter( (filter as? RequestFilter)?.let { requestFilter -> val result = requestFilter.onRequestHeaders(RequestHeaders(headers), endStream, StreamIntel(streamIntel)) return when (result) { - is FilterHeadersStatus.Continue -> arrayOf(result.status, result.headers.headers) + is FilterHeadersStatus.Continue -> arrayOf(result.status, result.headers.caseSensitiveHeaders()) is FilterHeadersStatus.StopIteration -> arrayOf(result.status, emptyMap>()) } } @@ -43,7 +43,7 @@ internal class EnvoyHTTPFilterAdapter( (filter as? ResponseFilter)?.let { responseFilter -> val result = responseFilter.onResponseHeaders(ResponseHeaders(headers), endStream, StreamIntel(streamIntel)) return when (result) { - is FilterHeadersStatus.Continue -> arrayOf(result.status, result.headers.headers) + is FilterHeadersStatus.Continue -> arrayOf(result.status, result.headers.caseSensitiveHeaders()) is FilterHeadersStatus.StopIteration -> arrayOf(result.status, emptyMap>()) } } @@ -57,7 +57,7 @@ internal class EnvoyHTTPFilterAdapter( is FilterDataStatus.Continue<*> -> arrayOf(result.status, result.data) is FilterDataStatus.StopIterationAndBuffer<*> -> arrayOf(result.status, ByteBuffer.allocate(0)) is FilterDataStatus.StopIterationNoBuffer<*> -> arrayOf(result.status, ByteBuffer.allocate(0)) - is FilterDataStatus.ResumeIteration<*> -> arrayOf(result.status, result.data, result.headers?.headers) + is FilterDataStatus.ResumeIteration<*> -> arrayOf(result.status, result.data, result.headers?.caseSensitiveHeaders()) } } return arrayOf(0, data) @@ -70,7 +70,7 @@ internal class EnvoyHTTPFilterAdapter( is FilterDataStatus.Continue<*> -> arrayOf(result.status, result.data) is FilterDataStatus.StopIterationAndBuffer<*> -> arrayOf(result.status, ByteBuffer.allocate(0)) is FilterDataStatus.StopIterationNoBuffer<*> -> arrayOf(result.status, ByteBuffer.allocate(0)) - is FilterDataStatus.ResumeIteration<*> -> arrayOf(result.status, result.data, result.headers?.headers) + is FilterDataStatus.ResumeIteration<*> -> arrayOf(result.status, result.data, result.headers?.caseSensitiveHeaders()) } } return arrayOf(0, data) @@ -80,9 +80,9 @@ internal class EnvoyHTTPFilterAdapter( (filter as? RequestFilter)?.let { requestFilter -> val result = requestFilter.onRequestTrailers(RequestTrailers(trailers), StreamIntel(streamIntel)) return when (result) { - is FilterTrailersStatus.Continue<*, *> -> arrayOf(result.status, result.trailers.headers) + is FilterTrailersStatus.Continue<*, *> -> arrayOf(result.status, result.trailers.caseSensitiveHeaders()) is FilterTrailersStatus.StopIteration<*, *> -> arrayOf(result.status, emptyMap>()) - is FilterTrailersStatus.ResumeIteration<*, *> -> arrayOf(result.status, result.trailers.headers, result.headers?.headers, result.data) + is FilterTrailersStatus.ResumeIteration<*, *> -> arrayOf(result.status, result.trailers.caseSensitiveHeaders(), result.headers?.caseSensitiveHeaders(), result.data) } } return arrayOf(0, trailers) @@ -92,9 +92,9 @@ internal class EnvoyHTTPFilterAdapter( (filter as? ResponseFilter)?.let { responseFilter -> val result = responseFilter.onResponseTrailers(ResponseTrailers(trailers), StreamIntel(streamIntel)) return when (result) { - is FilterTrailersStatus.Continue<*, *> -> arrayOf(result.status, result.trailers.headers) + is FilterTrailersStatus.Continue<*, *> -> arrayOf(result.status, result.trailers.caseSensitiveHeaders()) is FilterTrailersStatus.StopIteration<*, *> -> arrayOf(result.status, emptyMap>()) - is FilterTrailersStatus.ResumeIteration<*, *> -> arrayOf(result.status, result.trailers.headers, result.headers?.headers, result.data) + is FilterTrailersStatus.ResumeIteration<*, *> -> arrayOf(result.status, result.trailers.caseSensitiveHeaders(), result.headers?.caseSensitiveHeaders(), result.data) } } return arrayOf(0, trailers) @@ -134,7 +134,7 @@ internal class EnvoyHTTPFilterAdapter( StreamIntel(streamIntel) ) return when (result) { - is FilterResumeStatus.ResumeIteration<*, *> -> arrayOf(result.status, result.headers?.headers, result.data, result.trailers?.headers) + is FilterResumeStatus.ResumeIteration<*, *> -> arrayOf(result.status, result.headers?.caseSensitiveHeaders(), result.data, result.trailers?.caseSensitiveHeaders()) } } return arrayOf(-1, headers, data, trailers) @@ -156,7 +156,7 @@ internal class EnvoyHTTPFilterAdapter( StreamIntel(streamIntel) ) return when (result) { - is FilterResumeStatus.ResumeIteration<*, *> -> arrayOf(result.status, result.headers?.headers, result.data, result.trailers?.headers) + is FilterResumeStatus.ResumeIteration<*, *> -> arrayOf(result.status, result.headers?.caseSensitiveHeaders(), result.data, result.trailers?.caseSensitiveHeaders()) } } return arrayOf(-1, headers, data, trailers) diff --git a/mobile/library/kotlin/io/envoyproxy/envoymobile/grpc/GRPCRequestHeaders.kt b/mobile/library/kotlin/io/envoyproxy/envoymobile/grpc/GRPCRequestHeaders.kt index 119ee4aff8ae..faacfa08477a 100644 --- a/mobile/library/kotlin/io/envoyproxy/envoymobile/grpc/GRPCRequestHeaders.kt +++ b/mobile/library/kotlin/io/envoyproxy/envoymobile/grpc/GRPCRequestHeaders.kt @@ -9,16 +9,14 @@ class GRPCRequestHeaders : RequestHeaders { * * @param headers: Headers to set. */ - internal constructor(headers: Map>) : super(headers) + internal constructor(headers: Map>) : super(HeadersContainer(headers)) + + internal constructor(container: HeadersContainer) : super(container) /** * Convert the headers back to a builder for mutation. * * @return GRPCRequestHeadersBuilder, The new builder. */ - fun toGRPCRequestHeadersBuilder() = GRPCRequestHeadersBuilder( - headers.mapValues { - it.value.toMutableList() - }.toMutableMap() - ) + fun toGRPCRequestHeadersBuilder() = GRPCRequestHeadersBuilder(container) } diff --git a/mobile/library/kotlin/io/envoyproxy/envoymobile/grpc/GRPCRequestHeadersBuilder.kt b/mobile/library/kotlin/io/envoyproxy/envoymobile/grpc/GRPCRequestHeadersBuilder.kt index 38c20b08e93c..d94721005802 100644 --- a/mobile/library/kotlin/io/envoyproxy/envoymobile/grpc/GRPCRequestHeadersBuilder.kt +++ b/mobile/library/kotlin/io/envoyproxy/envoymobile/grpc/GRPCRequestHeadersBuilder.kt @@ -9,7 +9,14 @@ class GRPCRequestHeadersBuilder : HeadersBuilder { * * @param headers: Headers to set. */ - internal constructor(headers: MutableMap>) : super(headers) + internal constructor(headers: Map>) : super(HeadersContainer(headers)) + + /** + * Instantiate a new builder. + * + * @param container: The headers container to start with. + */ + internal constructor(container: HeadersContainer) : super(container) override fun add(name: String, value: String): GRPCRequestHeadersBuilder { super.add(name, value) @@ -38,8 +45,8 @@ class GRPCRequestHeadersBuilder : HeadersBuilder { * @param authority The URL authority for the request (i.e., "api.foo.com"). * @param path Path for the RPC (i.e., `/pb.api.v1.Foo/GetBar`). */ - constructor(scheme: String, authority: String, path: String) : super( - mutableMapOf>( + constructor(scheme: String, authority: String, path: String) : super(HeadersContainer( + mapOf>( ":authority" to mutableListOf(authority), ":method" to mutableListOf("POST"), ":path" to mutableListOf(path), @@ -48,6 +55,7 @@ class GRPCRequestHeadersBuilder : HeadersBuilder { "x-envoy-mobile-upstream-protocol" to mutableListOf(UpstreamHttpProtocol.HTTP2.stringValue) ) ) + ) /** * Add a specific timeout for the gRPC request. This will be sent in the `grpc-timeout` header. @@ -71,6 +79,6 @@ class GRPCRequestHeadersBuilder : HeadersBuilder { * @return New instance of request headers. */ fun build(): GRPCRequestHeaders { - return GRPCRequestHeaders(headers) + return GRPCRequestHeaders(container) } } diff --git a/mobile/library/kotlin/io/envoyproxy/envoymobile/mocks/MockStream.kt b/mobile/library/kotlin/io/envoyproxy/envoymobile/mocks/MockStream.kt index 8bdb28460318..ef52df579a30 100644 --- a/mobile/library/kotlin/io/envoyproxy/envoymobile/mocks/MockStream.kt +++ b/mobile/library/kotlin/io/envoyproxy/envoymobile/mocks/MockStream.kt @@ -81,7 +81,7 @@ class MockStream internal constructor(underlyingStream: MockEnvoyHTTPStream) : S * @param endStream Whether this is a headers-only response. */ fun receiveHeaders(headers: ResponseHeaders, endStream: Boolean) { - mockStream.callbacks.onHeaders(headers.allHeaders(), endStream, mockStreamIntel) + mockStream.callbacks.onHeaders(headers.caseSensitiveHeaders(), endStream, mockStreamIntel) } /** @@ -100,7 +100,7 @@ class MockStream internal constructor(underlyingStream: MockEnvoyHTTPStream) : S * @param trailers Response trailers to receive. */ fun receiveTrailers(trailers: ResponseTrailers) { - mockStream.callbacks.onTrailers(trailers.allHeaders(), mockStreamIntel) + mockStream.callbacks.onTrailers(trailers.caseSensitiveHeaders(), mockStreamIntel) } /** diff --git a/mobile/library/swift/Headers.swift b/mobile/library/swift/Headers.swift index 779b57b940b0..cc477b0310d0 100644 --- a/mobile/library/swift/Headers.swift +++ b/mobile/library/swift/Headers.swift @@ -27,7 +27,7 @@ public class Headers: NSObject { /// /// - returns: The underlying case-sensitive headers. public func caseSensitiveHeaders() -> [String: [String]] { - return self.container.allHeaders() + return self.container.caseSensitiveHeaders() } /// Internal initializer used by builders. diff --git a/mobile/library/swift/HeadersBuilder.swift b/mobile/library/swift/HeadersBuilder.swift index b245f3aee55d..787a790e259d 100644 --- a/mobile/library/swift/HeadersBuilder.swift +++ b/mobile/library/swift/HeadersBuilder.swift @@ -60,7 +60,7 @@ public class HeadersBuilder: NSObject { return self } - self.container.set(name: name, value: nil) + self.container.remove(name: name) return self } @@ -78,8 +78,12 @@ public class HeadersBuilder: NSObject { return self } - func allHeaders() -> [String: [String]] { - return self.container.allHeaders() + /// Accessor for all underlying case-sensitive headers. When possible, + /// use case-insensitive accessors instead. + /// + /// - returns: The underlying case-sensitive headers. + func caseSensitiveHeaders() -> [String: [String]] { + return self.container.caseSensitiveHeaders() } // Only explicitly implemented to work around a swiftinterface issue in Swift 5.1. This can be diff --git a/mobile/library/swift/HeadersContainer.swift b/mobile/library/swift/HeadersContainer.swift index 2f2e63134857..678b6ae4ce24 100644 --- a/mobile/library/swift/HeadersContainer.swift +++ b/mobile/library/swift/HeadersContainer.swift @@ -1,11 +1,11 @@ -/// The container which manages the underlying headers map. +/// The container that 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. + /// Represents a header name together with all of its values. /// It preserves the original casing of the header name. struct Header: Equatable { private(set) var name: String @@ -32,10 +32,10 @@ struct HeadersContainer: Equatable { var underlyingHeaders = [String: Header]() for (name, value) in headers { let lowercasedName = name.lowercased() - /// Dictionaries in Swift are unordered collections. Process headers with names + /// Dictionaries 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 + /// ["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 @@ -75,14 +75,17 @@ struct HeadersContainer: Equatable { /// /// - 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 - } + mutating func set(name: String, value: [String]) { self.headers[name.lowercased()] = Header(name: name, value: value) } + /// Remove a given header. + /// + /// - parameter name: The name of the header to remove. + mutating func remove(name: String) { + self.headers[name.lowercased()] = nil + } + /// Get the value for the provided header name. /// /// - parameter name: The case-insensitive header name for which to @@ -93,10 +96,11 @@ struct HeadersContainer: Equatable { return self.headers[name.lowercased()]?.value } - /// Return all underlying headers. + /// Accessor for all underlying case-sensitive headers. When possible, + /// use case-insensitive accessors instead. /// /// - returns: The underlying headers. - func allHeaders() -> [String: [String]] { + func caseSensitiveHeaders() -> [String: [String]] { return Dictionary(uniqueKeysWithValues: self.headers.map { _, value in return (value.name, value.value) }) diff --git a/mobile/test/kotlin/apps/baseline/MainActivity.kt b/mobile/test/kotlin/apps/baseline/MainActivity.kt index e66aab379368..9e38852b70f6 100644 --- a/mobile/test/kotlin/apps/baseline/MainActivity.kt +++ b/mobile/test/kotlin/apps/baseline/MainActivity.kt @@ -116,7 +116,7 @@ class MainActivity : Activity() { val message = "received headers with status $status" val sb = StringBuilder() - for ((name, value) in responseHeaders.headers) { + for ((name, value) in responseHeaders.caseSensitiveHeaders()) { if (name in FILTERED_HEADERS) { sb.append(name).append(": ").append(value.joinToString()).append("\n") } diff --git a/mobile/test/kotlin/apps/experimental/MainActivity.kt b/mobile/test/kotlin/apps/experimental/MainActivity.kt index 45dd82fee484..704d4b34b6f3 100644 --- a/mobile/test/kotlin/apps/experimental/MainActivity.kt +++ b/mobile/test/kotlin/apps/experimental/MainActivity.kt @@ -125,7 +125,7 @@ class MainActivity : Activity() { val message = "received headers with status $status" val sb = StringBuilder() - for ((name, value) in responseHeaders.headers) { + for ((name, value) in responseHeaders.caseSensitiveHeaders()) { if (name in FILTERED_HEADERS) { sb.append(name).append(": ").append(value.joinToString()).append("\n") } diff --git a/mobile/test/kotlin/io/envoyproxy/envoymobile/BUILD b/mobile/test/kotlin/io/envoyproxy/envoymobile/BUILD index 7140a39e9b50..218fa13fa8f2 100644 --- a/mobile/test/kotlin/io/envoyproxy/envoymobile/BUILD +++ b/mobile/test/kotlin/io/envoyproxy/envoymobile/BUILD @@ -99,3 +99,13 @@ envoy_mobile_kt_test( "//library/kotlin/io/envoyproxy/envoymobile:envoy_interfaces_lib", ], ) + +envoy_mobile_kt_test( + name = "headers_container_test", + srcs = [ + "HeadersContainerTest.kt", + ], + deps = [ + "//library/kotlin/io/envoyproxy/envoymobile:envoy_interfaces_lib", + ], +) diff --git a/mobile/test/kotlin/io/envoyproxy/envoymobile/GRPCStreamTest.kt b/mobile/test/kotlin/io/envoyproxy/envoymobile/GRPCStreamTest.kt index fa3b701fdcec..c0bd2d34ea4f 100644 --- a/mobile/test/kotlin/io/envoyproxy/envoymobile/GRPCStreamTest.kt +++ b/mobile/test/kotlin/io/envoyproxy/envoymobile/GRPCStreamTest.kt @@ -104,7 +104,7 @@ class GRPCStreamTest { GRPCClient(streamClient) .newGRPCStreamPrototype() .setOnResponseHeaders { headers, endStream, _ -> - assertThat(headers.allHeaders()).isEqualTo(expectedHeaders.allHeaders()) + assertThat(headers.caseSensitiveHeaders()).isEqualTo(expectedHeaders.caseSensitiveHeaders()) assertThat(endStream).isTrue() countDownLatch.countDown() } @@ -124,7 +124,7 @@ class GRPCStreamTest { GRPCClient(streamClient) .newGRPCStreamPrototype() .setOnResponseTrailers { trailers, _ -> - assertThat(trailers.allHeaders()).isEqualTo(expectedTrailers.allHeaders()) + assertThat(trailers.caseSensitiveHeaders()).isEqualTo(expectedTrailers.caseSensitiveHeaders()) countDownLatch.countDown() } .start(Executor {}) diff --git a/mobile/test/kotlin/io/envoyproxy/envoymobile/HeadersBuilderTest.kt b/mobile/test/kotlin/io/envoyproxy/envoymobile/HeadersBuilderTest.kt index de5810f01af3..0035008f94c7 100644 --- a/mobile/test/kotlin/io/envoyproxy/envoymobile/HeadersBuilderTest.kt +++ b/mobile/test/kotlin/io/envoyproxy/envoymobile/HeadersBuilderTest.kt @@ -13,6 +13,15 @@ class HeadersBuilderTest { assertThat(headers.value("x-foo")).containsExactly("1", "2") } + @Test + fun `adding header performs a case-insensitive header name lookup`() { + val headers = RequestHeadersBuilder(mutableMapOf()) + .add("fOo", "abc") + .add("foo", "123") + .build() + assertThat(headers.caseSensitiveHeaders()).isEqualTo(mapOf("fOo" to listOf("abc", "123"))) + } + @Test fun `removing specific header key removes all of its values`() { val headers = RequestHeadersBuilder(mutableMapOf()) @@ -20,8 +29,7 @@ class HeadersBuilderTest { .add("x-foo", "2") .remove("x-foo") .build() - - assertThat(headers.allHeaders()).doesNotContainKey("x-foo") + assertThat(headers.caseSensitiveHeaders()).doesNotContainKey("x-foo") } @Test @@ -34,6 +42,15 @@ class HeadersBuilderTest { assertThat(headers.value("x-bar")).containsExactly("abc") } + @Test + fun `removing specific header key performs a case-insentive header name lookup`() { + val headers = RequestHeadersBuilder(mutableMapOf()) + .set("foo", mutableListOf("123")) + .remove("fOo") + .build() + assertThat(headers.caseSensitiveHeaders()).isEmpty() + } + @Test fun `setting header replaces existing headers with matching name`() { val headers = RequestHeadersBuilder(mutableMapOf()) @@ -42,4 +59,39 @@ class HeadersBuilderTest { .build() assertThat(headers.value("x-foo")).containsExactly("abc") } + + @Test + fun `setting header replaces performs a case-insensitive header name lookup`() { + val headers = RequestHeadersBuilder(mapOf()) + .set("foo", mutableListOf("123")) + .set("fOo", mutableListOf("abc")) + .build() + assertThat(headers.caseSensitiveHeaders()).isEqualTo(mapOf("fOo" to listOf("abc"))) + } + + @Test + fun `test initialization is case-insensitive, preserves casing and processes headers in alphabetical order`() { + val headers = RequestHeadersBuilder(mutableMapOf("a" to mutableListOf("456"), "A" to mutableListOf("123"))).build() + assertThat(headers.caseSensitiveHeaders()).isEqualTo(mapOf("A" to listOf("123", "456"))) + } + + @Test + fun `test restricted headers are not settable`() { + val headers = RequestHeadersBuilder(method = RequestMethod.GET, authority = "example.com", path = "/") + .add("host", "example.com") + .add("Host", "example.com") + .add("hostWithSuffix", "foo.bar") + .set(":scheme", mutableListOf("http")) + .set(":path", mutableListOf("/nope")) + .build() + .caseSensitiveHeaders() + val expected = mapOf( + ":authority" to listOf("example.com"), + ":path" to listOf("/"), + ":method" to listOf("GET"), + ":scheme" to listOf("https"), + "hostWithSuffix" to listOf("foo.bar"), + ) + assertThat(headers).isEqualTo(expected) + } } diff --git a/mobile/test/kotlin/io/envoyproxy/envoymobile/HeadersContainerTest.kt b/mobile/test/kotlin/io/envoyproxy/envoymobile/HeadersContainerTest.kt new file mode 100644 index 000000000000..483c4591810a --- /dev/null +++ b/mobile/test/kotlin/io/envoyproxy/envoymobile/HeadersContainerTest.kt @@ -0,0 +1,81 @@ +package io.envoyproxy.envoymobile + +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test + +class HeadersContainerest { + @Test + fun `instantiation preserves all headers from input headers map`() { + val headers = mapOf("a" to mutableListOf("456"), "b" to mutableListOf("123")) + val container = HeadersContainer(headers) + assertThat(container.caseSensitiveHeaders()).isEqualTo(headers) + } + + @Test + fun `instantiation with mutable list of values is case-insensitive, preserves casing and processes in alphabetical order`() { + val container = HeadersContainer(mapOf("a" to mutableListOf("456"), "A" to mutableListOf("123"))) + assertThat(container.caseSensitiveHeaders()).isEqualTo(mapOf("A" to listOf("123", "456"))) + } + + @Test + fun `creation with immutable list of values is case-insensitive, preserves casing and processes in alphabetical order`() { + val container = HeadersContainer.create(mapOf("a" to listOf("456"), "A" to listOf("123"))) + assertThat(container.caseSensitiveHeaders()).isEqualTo(mapOf("A" to listOf("123", "456"))) + } + + @Test + fun `adding header adds to list of headers keys`() { + val container = HeadersContainer(mutableMapOf()) + container.add("x-foo", "1") + container.add("x-foo", "2") + assertThat(container.value("x-foo")).containsExactly("1", "2") + } + + @Test + fun `adding header performs a case-insensitive header lookup and preserves header name casing`() { + val container = HeadersContainer(mapOf()) + container.add("x-FOO", "1") + container.add("x-foo", "2") + + assertThat(container.value("x-foo")).isEqualTo(listOf("1", "2")) + assertThat(container.caseSensitiveHeaders()).isEqualTo(mapOf("x-FOO" to listOf("1", "2"))) + } + + @Test + fun `setting header adds to list of headers keys`() { + val container = HeadersContainer(mapOf()) + container.set("x-foo", mutableListOf("abc")) + + assertThat( container.value("x-foo")).isEqualTo(listOf("abc")) + } + + @Test + fun `setting header overrides previous header values`() { + val container = HeadersContainer(mapOf()) + container.add("x-FOO", "1") + container.add("x-foo", "2") + container.set("x-foo", mutableListOf("3")) + + assertThat(container.value("x-foo")).isEqualTo(listOf("3")) + } + + @Test + fun `removing header removes all of its values`() { + val container = HeadersContainer(mapOf()) + container.add("x-foo", "1") + container.add("x-foo", "2") + container.remove("x-foo") + + assertThat(container.value("x-foo")).isNull() + } + + @Test + fun `removing header performs case-insensitive header name lookup`() { + val container = HeadersContainer(mapOf()) + container.add("x-FOO", "1") + container.add("x-foo", "2") + container.remove("x-fOo") + + assertThat(container.value("x-foo")).isNull() + } +} diff --git a/mobile/test/kotlin/io/envoyproxy/envoymobile/RequestHeadersBuilderTest.kt b/mobile/test/kotlin/io/envoyproxy/envoymobile/RequestHeadersBuilderTest.kt index 88d0b0f5f158..035fd19fd1ac 100644 --- a/mobile/test/kotlin/io/envoyproxy/envoymobile/RequestHeadersBuilderTest.kt +++ b/mobile/test/kotlin/io/envoyproxy/envoymobile/RequestHeadersBuilderTest.kt @@ -103,9 +103,9 @@ class RequestHeadersBuilderTest { .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.caseSensitiveHeaders()).doesNotContainKey(":x-foo") + assertThat(headers.caseSensitiveHeaders()).doesNotContainKey("x-envoy-mobile-foo") + assertThat(headers.caseSensitiveHeaders()).doesNotContainKey("host") assertThat(headers.value("hostWithSuffix")).containsExactly("foo.bar") } @@ -119,8 +119,8 @@ class RequestHeadersBuilderTest { .set("x-envoy-mobile-foo", mutableListOf("abc")) .build() - assertThat(headers.allHeaders()).doesNotContainKey(":x-foo") - assertThat(headers.allHeaders()).doesNotContainKey("x-envoy-mobile-foo") + assertThat(headers.caseSensitiveHeaders()).doesNotContainKey(":x-foo") + assertThat(headers.caseSensitiveHeaders()).doesNotContainKey("x-envoy-mobile-foo") } @Test @@ -168,7 +168,7 @@ class RequestHeadersBuilderTest { .addRetryPolicy(retryPolicy) .build() - assertThat(headers.allHeaders()).containsAllEntriesOf(retryPolicyHeaders) + assertThat(headers.caseSensitiveHeaders()).containsAllEntriesOf(retryPolicyHeaders) } @Test @@ -199,7 +199,7 @@ class RequestHeadersBuilderTest { .build() val headers2 = headers1.toRequestHeadersBuilder().build() - assertThat(headers1.allHeaders()).isEqualTo(headers2.allHeaders()) + assertThat(headers1.caseSensitiveHeaders()).isEqualTo(headers2.caseSensitiveHeaders()) } @Test diff --git a/mobile/test/kotlin/io/envoyproxy/envoymobile/ResponseHeadersTest.kt b/mobile/test/kotlin/io/envoyproxy/envoymobile/ResponseHeadersTest.kt index ec42c5583448..1aaf2e7bc7ff 100644 --- a/mobile/test/kotlin/io/envoyproxy/envoymobile/ResponseHeadersTest.kt +++ b/mobile/test/kotlin/io/envoyproxy/envoymobile/ResponseHeadersTest.kt @@ -43,4 +43,11 @@ class ResponseHeadersTest { .build() assertThat(headers.value(":status")).isNull() } + + @Test + fun `header lookup is a case-insensitive operation`() { + val headers = ResponseHeaders(mapOf("FoO" to listOf("123"))) + assertThat(headers.value("FoO")).isEqualTo(listOf("123")) + assertThat(headers.value("foo")).isEqualTo(listOf("123")) + } } diff --git a/mobile/test/swift/HeadersBuilderTests.swift b/mobile/test/swift/HeadersBuilderTests.swift index a92185ad25b7..6c7108f9774c 100644 --- a/mobile/test/swift/HeadersBuilderTests.swift +++ b/mobile/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") - .allHeaders() + .caseSensitiveHeaders() 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") - .allHeaders() + .caseSensitiveHeaders() 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") - .allHeaders() + .caseSensitiveHeaders() XCTAssertEqual(["x-bar": ["abc"]], headers) } @@ -36,34 +36,34 @@ final class HeadersBuilderTests: XCTestCase { let headers = HeadersBuilder(headers: [:]) .add(name: "x-foo", value: "123") .set(name: "x-foo", value: ["abc"]) - .allHeaders() + .caseSensitiveHeaders() XCTAssertEqual(["x-foo": ["abc"]], headers) } func testInitializationIsCaseInsensitivePreservesCasingAndProcessesInAlphabeticalOrder() { let headers = HeadersBuilder(headers: ["a": ["456"], "A": ["123"]]) - XCTAssertEqual(["A": ["123", "456"]], headers.allHeaders()) + XCTAssertEqual(["A": ["123", "456"]], headers.caseSensitiveHeaders()) } func testAddingHeaderIsCaseInsensitiveAndHeaderCasingIsPreserved() { let headers = HeadersBuilder(headers: [:]) headers.add(name: "fOo", value: "abc") headers.add(name: "foo", value: "123") - XCTAssertEqual(["fOo": ["abc", "123"]], headers.allHeaders()) + XCTAssertEqual(["fOo": ["abc", "123"]], headers.caseSensitiveHeaders()) } func testSettingHeaderIsCaseInsensitiveAndHeaderCasingIsPreserved() { let headers = HeadersBuilder(headers: [:]) headers.set(name: "foo", value: ["123"]) headers.set(name: "fOo", value: ["abc"]) - XCTAssertEqual(["fOo": ["abc"]], headers.allHeaders()) + XCTAssertEqual(["fOo": ["abc"]], headers.caseSensitiveHeaders()) } func testRemovingHeaderIsCaseInsensitive() { let headers = HeadersBuilder(headers: [:]) headers.set(name: "foo", value: ["123"]) headers.remove(name: "fOo") - XCTAssertEqual([:], headers.allHeaders()) + XCTAssertEqual([:], headers.caseSensitiveHeaders()) } func testRestrictedHeadersAreNotSettable() { @@ -72,7 +72,7 @@ final class HeadersBuilderTests: XCTestCase { .add(name: "hostWithSuffix", value: "foo.bar") .set(name: ":scheme", value: ["http"]) .set(name: ":path", value: ["/nope"]) - .allHeaders() + .caseSensitiveHeaders() let expected = [ ":authority": ["example.com"], ":path": ["/"], diff --git a/mobile/test/swift/HeadersContainerTests.swift b/mobile/test/swift/HeadersContainerTests.swift index d7ecd3b97691..d8ff2e4f0aa3 100644 --- a/mobile/test/swift/HeadersContainerTests.swift +++ b/mobile/test/swift/HeadersContainerTests.swift @@ -4,15 +4,15 @@ import XCTest final class HeadersContainerTests: XCTestCase { func testInitializationPreservesAllHeadersFromInputHeadersMap() { let container = HeadersContainer(headers: ["a": ["456"], "b": ["123"]]) - XCTAssertEqual(["a": ["456"], "b": ["123"]], container.allHeaders()) + XCTAssertEqual(["a": ["456"], "b": ["123"]], container.caseSensitiveHeaders()) } func testInitializationIsCaseInsensitivePreservesCasingAndProcessesInAlphabeticalOrder() { let container = HeadersContainer(headers: ["a": ["456"], "A": ["123"]]) - XCTAssertEqual(["A": ["123", "456"]], container.allHeaders()) + XCTAssertEqual(["A": ["123", "456"]], container.caseSensitiveHeaders()) } - func testAddingHeaderValueAddsToListOfHeaders() { + func testAddingHeaderAddsToListOfHeaders() { var container = HeadersContainer() container.add(name: "x-foo", value: "1") container.add(name: "x-foo", value: "2") @@ -20,13 +20,13 @@ final class HeadersContainerTests: XCTestCase { XCTAssertEqual(["1", "2"], container.value(forName: "x-foo")) } - func testAddingHeaderValueIsCaseInsensitiveAndPreservesHeaderNameCasing() { + func testAddingHeaderIsCaseInsensitiveAndPreservesHeaderNameCasing() { 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()) + XCTAssertEqual(["x-FOO": ["1", "2"]], container.caseSensitiveHeaders()) } func testSettingHeaderAddsToListOfHeaders() { @@ -45,20 +45,20 @@ final class HeadersContainerTests: XCTestCase { XCTAssertEqual(["3"], container.value(forName: "x-foo")) } - func testSettingHeaderToNilRemovesAllOfItsValues() { + func testRemovingHeaderRemovesAllOfItsValues() { var container = HeadersContainer() container.add(name: "x-foo", value: "1") container.add(name: "x-foo", value: "2") - container.set(name: "x-foo", value: nil) + container.remove(name: "x-foo") XCTAssertNil(container.value(forName: "x-foo")) } - func testSettingHeaderToNilPerformsCaseInsensitiveHeaderNameLookup() { + func testRemovingHeaderPerformsCaseInsensitiveHeaderNameLookup() { var container = HeadersContainer() container.add(name: "x-FOO", value: "1") container.add(name: "x-foo", value: "2") - container.set(name: "x-foo", value: nil) + container.remove(name: "x-fOo") XCTAssertNil(container.value(forName: "x-foo")) }