mirror of
https://github.com/square/okhttp.git
synced 2026-01-12 10:23:16 +03:00
Headers multiplatform (#6996)
This commit is contained in:
154
okhttp/src/commonMain/kotlin/okhttp3/Headers.kt
Normal file
154
okhttp/src/commonMain/kotlin/okhttp3/Headers.kt
Normal file
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package okhttp3
|
||||
|
||||
import okhttp3.Headers.Builder
|
||||
|
||||
/**
|
||||
* The header fields of a single HTTP message. Values are uninterpreted strings; use `Request` and
|
||||
* `Response` for interpreted headers. This class maintains the order of the header fields within
|
||||
* the HTTP message.
|
||||
*
|
||||
* This class tracks header values line-by-line. A field with multiple comma- separated values on
|
||||
* the same line will be treated as a field with a single value by this class. It is the caller's
|
||||
* responsibility to detect and split on commas if their field permits multiple values. This
|
||||
* simplifies use of single-valued fields whose values routinely contain commas, such as cookies or
|
||||
* dates.
|
||||
*
|
||||
* This class trims whitespace from values. It never returns values with leading or trailing
|
||||
* whitespace.
|
||||
*
|
||||
* Instances of this class are immutable. Use [Builder] to create instances.
|
||||
*/
|
||||
@Suppress("NAME_SHADOWING")
|
||||
expect class Headers internal constructor(
|
||||
namesAndValues: Array<String>
|
||||
) : Iterable<Pair<String, String>> {
|
||||
internal val namesAndValues: Array<String>
|
||||
|
||||
/** Returns the last value corresponding to the specified field, or null. */
|
||||
operator fun get(name: String): String?
|
||||
|
||||
/** Returns the number of field values. */
|
||||
val size: Int
|
||||
|
||||
/** Returns the field at `position`. */
|
||||
fun name(index: Int): String
|
||||
|
||||
/** Returns the value at `index`. */
|
||||
fun value(index: Int): String
|
||||
|
||||
/** Returns an immutable case-insensitive set of header names. */
|
||||
fun names(): Set<String>
|
||||
|
||||
/** Returns an immutable list of the header values for `name`. */
|
||||
fun values(name: String): List<String>
|
||||
|
||||
override operator fun iterator(): Iterator<Pair<String, String>>
|
||||
|
||||
fun newBuilder(): Builder
|
||||
|
||||
/**
|
||||
* Returns true if `other` is a `Headers` object with the same headers, with the same casing, in
|
||||
* the same order. Note that two headers instances may be *semantically* equal but not equal
|
||||
* according to this method. In particular, none of the following sets of headers are equal
|
||||
* according to this method:
|
||||
*
|
||||
* 1. Original
|
||||
* ```
|
||||
* Content-Type: text/html
|
||||
* Content-Length: 50
|
||||
* ```
|
||||
*
|
||||
* 2. Different order
|
||||
*
|
||||
* ```
|
||||
* Content-Length: 50
|
||||
* Content-Type: text/html
|
||||
* ```
|
||||
*
|
||||
* 3. Different case
|
||||
*
|
||||
* ```
|
||||
* content-type: text/html
|
||||
* content-length: 50
|
||||
* ```
|
||||
*
|
||||
* 4. Different values
|
||||
*
|
||||
* ```
|
||||
* Content-Type: text/html
|
||||
* Content-Length: 050
|
||||
* ```
|
||||
*
|
||||
* Applications that require semantically equal headers should convert them into a canonical form
|
||||
* before comparing them for equality.
|
||||
*/
|
||||
override fun equals(other: Any?): Boolean
|
||||
|
||||
/**
|
||||
* Returns header names and values. The names and values are separated by `: ` and each pair is
|
||||
* followed by a newline character `\n`.
|
||||
*
|
||||
* Since OkHttp 5 this redacts these sensitive headers:
|
||||
*
|
||||
* * `Authorization`
|
||||
* * `Cookie`
|
||||
* * `Proxy-Authorization`
|
||||
* * `Set-Cookie`
|
||||
*/
|
||||
override fun toString(): String
|
||||
|
||||
class Builder internal constructor() {
|
||||
internal val namesAndValues: MutableList<String>
|
||||
|
||||
/**
|
||||
* Add a header with the specified name and value. Does validation of header names and values.
|
||||
*/
|
||||
fun add(name: String, value: String): Builder
|
||||
|
||||
/**
|
||||
* Adds all headers from an existing collection.
|
||||
*/
|
||||
fun addAll(headers: Headers): Builder
|
||||
|
||||
fun removeAll(name: String): Builder
|
||||
|
||||
/**
|
||||
* Set a field with the specified value. If the field is not found, it is added. If the field is
|
||||
* found, the existing values are replaced.
|
||||
*/
|
||||
operator fun set(name: String, value: String): Builder
|
||||
|
||||
/** Equivalent to `build().get(name)`, but potentially faster. */
|
||||
operator fun get(name: String): String?
|
||||
|
||||
fun build(): Headers
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Returns headers for the alternating header names and values. There must be an even number of
|
||||
* arguments, and they must alternate between header names and values.
|
||||
*/
|
||||
fun headersOf(vararg namesAndValues: String): Headers
|
||||
|
||||
/** Returns headers for the header names and values in the [Map]. */
|
||||
fun Map<String, String>.toHeaders(): Headers
|
||||
}
|
||||
}
|
||||
191
okhttp/src/commonMain/kotlin/okhttp3/internal/-HeadersCommon.kt
Normal file
191
okhttp/src/commonMain/kotlin/okhttp3/internal/-HeadersCommon.kt
Normal file
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package okhttp3.internal
|
||||
|
||||
import okhttp3.Headers
|
||||
|
||||
internal fun Headers.commonName(index: Int): String = namesAndValues[index * 2]
|
||||
|
||||
internal fun Headers.commonValue(index: Int): String = namesAndValues[index * 2 + 1]
|
||||
|
||||
internal fun Headers.commonValues(name: String): List<String> {
|
||||
var result: MutableList<String>? = null
|
||||
for (i in 0 until size) {
|
||||
if (name.equals(name(i), ignoreCase = true)) {
|
||||
if (result == null) result = ArrayList(2)
|
||||
result.add(value(i))
|
||||
}
|
||||
}
|
||||
return result?.toList().orEmpty()
|
||||
}
|
||||
|
||||
internal fun Headers.commonIterator(): Iterator<Pair<String, String>> {
|
||||
return Array(size) { name(it) to value(it) }.iterator()
|
||||
}
|
||||
|
||||
internal fun Headers.commonNewBuilder(): Headers.Builder {
|
||||
val result = Headers.Builder()
|
||||
result.namesAndValues += namesAndValues
|
||||
return result
|
||||
}
|
||||
|
||||
internal fun Headers.commonEquals(other: Any?): Boolean {
|
||||
return other is Headers && namesAndValues.contentEquals(other.namesAndValues)
|
||||
}
|
||||
|
||||
internal fun Headers.commonHashCode(): Int = namesAndValues.contentHashCode()
|
||||
|
||||
internal fun Headers.commonToString(): String {
|
||||
return buildString {
|
||||
for (i in 0 until size) {
|
||||
val name = name(i)
|
||||
val value = value(i)
|
||||
append(name)
|
||||
append(": ")
|
||||
append(if (isSensitiveHeader(name)) "██" else value)
|
||||
append("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun commonHeadersGet(namesAndValues: Array<String>, name: String): String? {
|
||||
for (i in namesAndValues.size - 2 downTo 0 step 2) {
|
||||
if (name.equals(namesAndValues[i], ignoreCase = true)) {
|
||||
return namesAndValues[i + 1]
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
internal fun Headers.Builder.commonAdd(name: String, value: String) = apply {
|
||||
headersCheckName(name)
|
||||
headersCheckValue(value, name)
|
||||
commonAddLenient(name, value)
|
||||
}
|
||||
|
||||
internal fun Headers.Builder.commonAddAll(headers: Headers) = apply {
|
||||
for (i in 0 until headers.size) {
|
||||
commonAddLenient(headers.name(i), headers.value(i))
|
||||
}
|
||||
}
|
||||
|
||||
internal fun Headers.Builder.commonAddLenient(name: String, value: String) = apply {
|
||||
namesAndValues.add(name)
|
||||
namesAndValues.add(value.trim())
|
||||
}
|
||||
|
||||
internal fun Headers.Builder.commonRemoveAll(name: String) = apply {
|
||||
var i = 0
|
||||
while (i < namesAndValues.size) {
|
||||
if (name.equals(namesAndValues[i], ignoreCase = true)) {
|
||||
namesAndValues.removeAt(i) // name
|
||||
namesAndValues.removeAt(i) // value
|
||||
i -= 2
|
||||
}
|
||||
i += 2
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a field with the specified value. If the field is not found, it is added. If the field is
|
||||
* found, the existing values are replaced.
|
||||
*/
|
||||
internal fun Headers.Builder.commonSet(name: String, value: String) = apply {
|
||||
headersCheckName(name)
|
||||
headersCheckValue(value, name)
|
||||
removeAll(name)
|
||||
commonAddLenient(name, value)
|
||||
}
|
||||
|
||||
/** Equivalent to `build().get(name)`, but potentially faster. */
|
||||
internal fun Headers.Builder.commonGet(name: String): String? {
|
||||
for (i in namesAndValues.size - 2 downTo 0 step 2) {
|
||||
if (name.equals(namesAndValues[i], ignoreCase = true)) {
|
||||
return namesAndValues[i + 1]
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
internal fun Headers.Builder.commonBuild(): Headers = Headers(namesAndValues.toTypedArray())
|
||||
|
||||
internal fun headersCheckName(name: String) {
|
||||
require(name.isNotEmpty()) { "name is empty" }
|
||||
for (i in name.indices) {
|
||||
val c = name[i]
|
||||
require(c in '\u0021'..'\u007e') {
|
||||
"Unexpected char 0x${c.charCode()} at $i in header name: $name"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun headersCheckValue(value: String, name: String) {
|
||||
for (i in value.indices) {
|
||||
val c = value[i]
|
||||
require(c == '\t' || c in '\u0020'..'\u007e') {
|
||||
"Unexpected char 0x${c.charCode()} at $i in $name value" +
|
||||
(if (isSensitiveHeader(name)) "" else ": $value")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Char.charCode() = code.toString(16).let {
|
||||
if (it.length < 2) {
|
||||
"0$it"
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
|
||||
internal fun commonHeadersOf(vararg inputNamesAndValues: String): Headers {
|
||||
require(inputNamesAndValues.size % 2 == 0) { "Expected alternating header names and values" }
|
||||
|
||||
// Make a defensive copy and clean it up.
|
||||
val namesAndValues: Array<String> = arrayOf(*inputNamesAndValues)
|
||||
for (i in namesAndValues.indices) {
|
||||
require(namesAndValues[i] != null) { "Headers cannot be null" }
|
||||
namesAndValues[i] = inputNamesAndValues[i].trim()
|
||||
}
|
||||
|
||||
// Check for malformed headers.
|
||||
for (i in namesAndValues.indices step 2) {
|
||||
val name = namesAndValues[i]
|
||||
val value = namesAndValues[i + 1]
|
||||
headersCheckName(name)
|
||||
headersCheckValue(value, name)
|
||||
}
|
||||
|
||||
return Headers(namesAndValues)
|
||||
}
|
||||
|
||||
internal fun Map<String, String>.commonToHeaders(): Headers {
|
||||
// Make a defensive copy and clean it up.
|
||||
val namesAndValues = arrayOfNulls<String>(size * 2)
|
||||
var i = 0
|
||||
for ((k, v) in this) {
|
||||
val name = k.trim()
|
||||
val value = v.trim()
|
||||
headersCheckName(name)
|
||||
headersCheckValue(value, name)
|
||||
namesAndValues[i] = name
|
||||
namesAndValues[i + 1] = value
|
||||
i += 2
|
||||
}
|
||||
|
||||
return Headers(namesAndValues as Array<String>)
|
||||
}
|
||||
286
okhttp/src/commonTest/kotlin/okhttp3/HeadersTest.kt
Normal file
286
okhttp/src/commonTest/kotlin/okhttp3/HeadersTest.kt
Normal file
@@ -0,0 +1,286 @@
|
||||
/*
|
||||
* Copyright (C) 2012 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package okhttp3
|
||||
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isNotEqualTo
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.fail
|
||||
import okhttp3.Headers.Companion.headersOf
|
||||
import okhttp3.Headers.Companion.toHeaders
|
||||
|
||||
class HeadersTest {
|
||||
@Test fun ofTrims() {
|
||||
val headers = headersOf("\t User-Agent \n", " \r OkHttp ")
|
||||
assertThat(headers.name(0)).isEqualTo("User-Agent")
|
||||
assertThat(headers.value(0)).isEqualTo("OkHttp")
|
||||
}
|
||||
|
||||
@Test fun ofThrowsOddNumberOfHeaders() {
|
||||
try {
|
||||
headersOf("User-Agent", "OkHttp", "Content-Length")
|
||||
fail()
|
||||
} catch (expected: IllegalArgumentException) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun ofThrowsOnEmptyName() {
|
||||
try {
|
||||
headersOf("", "OkHttp")
|
||||
fail()
|
||||
} catch (expected: IllegalArgumentException) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun ofAcceptsEmptyValue() {
|
||||
val headers = headersOf("User-Agent", "")
|
||||
assertThat(headers.value(0)).isEqualTo("")
|
||||
}
|
||||
|
||||
@Test fun ofMakesDefensiveCopy() {
|
||||
val namesAndValues = arrayOf(
|
||||
"User-Agent",
|
||||
"OkHttp"
|
||||
)
|
||||
val headers = headersOf(*namesAndValues)
|
||||
namesAndValues[1] = "Chrome"
|
||||
assertThat(headers.value(0)).isEqualTo("OkHttp")
|
||||
}
|
||||
|
||||
@Test fun ofRejectsNullChar() {
|
||||
try {
|
||||
headersOf("User-Agent", "Square\u0000OkHttp")
|
||||
fail()
|
||||
} catch (expected: IllegalArgumentException) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun ofMapThrowsOnEmptyName() {
|
||||
try {
|
||||
mapOf("" to "OkHttp").toHeaders()
|
||||
fail()
|
||||
} catch (expected: IllegalArgumentException) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun ofMapThrowsOnBlankName() {
|
||||
try {
|
||||
mapOf(" " to "OkHttp").toHeaders()
|
||||
fail()
|
||||
} catch (expected: IllegalArgumentException) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun ofMapAcceptsEmptyValue() {
|
||||
val headers = mapOf("User-Agent" to "").toHeaders()
|
||||
assertThat(headers.value(0)).isEqualTo("")
|
||||
}
|
||||
|
||||
@Test fun ofMapTrimsKey() {
|
||||
val headers = mapOf(" User-Agent " to "OkHttp").toHeaders()
|
||||
assertThat(headers.name(0)).isEqualTo("User-Agent")
|
||||
}
|
||||
|
||||
@Test fun ofMapTrimsValue() {
|
||||
val headers = mapOf("User-Agent" to " OkHttp ").toHeaders()
|
||||
assertThat(headers.value(0)).isEqualTo("OkHttp")
|
||||
}
|
||||
|
||||
@Test fun ofMapMakesDefensiveCopy() {
|
||||
val namesAndValues = mutableMapOf<String, String>()
|
||||
namesAndValues["User-Agent"] = "OkHttp"
|
||||
val headers = namesAndValues.toHeaders()
|
||||
namesAndValues["User-Agent"] = "Chrome"
|
||||
assertThat(headers.value(0)).isEqualTo("OkHttp")
|
||||
}
|
||||
|
||||
@Test fun ofMapRejectsNullCharInName() {
|
||||
try {
|
||||
mapOf("User-\u0000Agent" to "OkHttp").toHeaders()
|
||||
fail()
|
||||
} catch (expected: IllegalArgumentException) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun ofMapRejectsNullCharInValue() {
|
||||
try {
|
||||
mapOf("User-Agent" to "Square\u0000OkHttp").toHeaders()
|
||||
fail()
|
||||
} catch (expected: IllegalArgumentException) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun builderRejectsUnicodeInHeaderName() {
|
||||
try {
|
||||
Headers.Builder().add("héader1", "value1")
|
||||
fail("Should have complained about invalid name")
|
||||
} catch (expected: IllegalArgumentException) {
|
||||
assertThat(expected.message)
|
||||
.isEqualTo("Unexpected char 0xe9 at 1 in header name: héader1")
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun builderRejectsUnicodeInHeaderValue() {
|
||||
try {
|
||||
Headers.Builder().add("header1", "valué1")
|
||||
fail("Should have complained about invalid value")
|
||||
} catch (expected: IllegalArgumentException) {
|
||||
assertThat(expected.message)
|
||||
.isEqualTo("Unexpected char 0xe9 at 4 in header1 value: valué1")
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun varargFactoryRejectsUnicodeInHeaderName() {
|
||||
try {
|
||||
headersOf("héader1", "value1")
|
||||
fail("Should have complained about invalid value")
|
||||
} catch (expected: IllegalArgumentException) {
|
||||
assertThat(expected.message)
|
||||
.isEqualTo("Unexpected char 0xe9 at 1 in header name: héader1")
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun varargFactoryRejectsUnicodeInHeaderValue() {
|
||||
try {
|
||||
headersOf("header1", "valué1")
|
||||
fail("Should have complained about invalid value")
|
||||
} catch (expected: IllegalArgumentException) {
|
||||
assertThat(expected.message)
|
||||
.isEqualTo("Unexpected char 0xe9 at 4 in header1 value: valué1")
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun mapFactoryRejectsUnicodeInHeaderName() {
|
||||
try {
|
||||
mapOf("héader1" to "value1").toHeaders()
|
||||
fail("Should have complained about invalid value")
|
||||
} catch (expected: IllegalArgumentException) {
|
||||
assertThat(expected.message)
|
||||
.isEqualTo("Unexpected char 0xe9 at 1 in header name: héader1")
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun mapFactoryRejectsUnicodeInHeaderValue() {
|
||||
try {
|
||||
mapOf("header1" to "valué1").toHeaders()
|
||||
fail("Should have complained about invalid value")
|
||||
} catch (expected: IllegalArgumentException) {
|
||||
assertThat(expected.message)
|
||||
.isEqualTo("Unexpected char 0xe9 at 4 in header1 value: valué1")
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun sensitiveHeadersNotIncludedInExceptions() {
|
||||
try {
|
||||
Headers.Builder().add("Authorization", "valué1")
|
||||
fail("Should have complained about invalid name")
|
||||
} catch (expected: IllegalArgumentException) {
|
||||
assertThat(expected.message)
|
||||
.isEqualTo("Unexpected char 0xe9 at 4 in Authorization value")
|
||||
}
|
||||
try {
|
||||
Headers.Builder().add("Cookie", "valué1")
|
||||
fail("Should have complained about invalid name")
|
||||
} catch (expected: IllegalArgumentException) {
|
||||
assertThat(expected.message)
|
||||
.isEqualTo("Unexpected char 0xe9 at 4 in Cookie value")
|
||||
}
|
||||
try {
|
||||
Headers.Builder().add("Proxy-Authorization", "valué1")
|
||||
fail("Should have complained about invalid name")
|
||||
} catch (expected: IllegalArgumentException) {
|
||||
assertThat(expected.message)
|
||||
.isEqualTo("Unexpected char 0xe9 at 4 in Proxy-Authorization value")
|
||||
}
|
||||
try {
|
||||
Headers.Builder().add("Set-Cookie", "valué1")
|
||||
fail("Should have complained about invalid name")
|
||||
} catch (expected: IllegalArgumentException) {
|
||||
assertThat(expected.message)
|
||||
.isEqualTo("Unexpected char 0xe9 at 4 in Set-Cookie value")
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun headersEquals() {
|
||||
val headers1 = Headers.Builder()
|
||||
.add("Connection", "close")
|
||||
.add("Transfer-Encoding", "chunked")
|
||||
.build()
|
||||
val headers2 = Headers.Builder()
|
||||
.add("Connection", "close")
|
||||
.add("Transfer-Encoding", "chunked")
|
||||
.build()
|
||||
assertThat(headers2).isEqualTo(headers1)
|
||||
assertThat(headers2.hashCode()).isEqualTo(headers1.hashCode())
|
||||
}
|
||||
|
||||
@Test fun headersNotEquals() {
|
||||
val headers1 = Headers.Builder()
|
||||
.add("Connection", "close")
|
||||
.add("Transfer-Encoding", "chunked")
|
||||
.build()
|
||||
val headers2 = Headers.Builder()
|
||||
.add("Connection", "keep-alive")
|
||||
.add("Transfer-Encoding", "chunked")
|
||||
.build()
|
||||
assertThat(headers2).isNotEqualTo(headers1)
|
||||
assertThat(headers2.hashCode()).isNotEqualTo(headers1.hashCode().toLong())
|
||||
}
|
||||
|
||||
@Test fun headersToString() {
|
||||
val headers = Headers.Builder()
|
||||
.add("A", "a")
|
||||
.add("B", "bb")
|
||||
.build()
|
||||
assertThat(headers.toString()).isEqualTo("A: a\nB: bb\n")
|
||||
}
|
||||
|
||||
@Test fun headersToStringRedactsSensitiveHeaders() {
|
||||
val headers = Headers.Builder()
|
||||
.add("content-length", "99")
|
||||
.add("authorization", "peanutbutter")
|
||||
.add("proxy-authorization", "chocolate")
|
||||
.add("cookie", "drink=coffee")
|
||||
.add("set-cookie", "accessory=sugar")
|
||||
.add("user-agent", "OkHttp")
|
||||
.build()
|
||||
assertThat(headers.toString()).isEqualTo(
|
||||
"""
|
||||
|content-length: 99
|
||||
|authorization: ██
|
||||
|proxy-authorization: ██
|
||||
|cookie: ██
|
||||
|set-cookie: ██
|
||||
|user-agent: OkHttp
|
||||
|""".trimMargin()
|
||||
)
|
||||
}
|
||||
|
||||
@Test fun headersAddAll() {
|
||||
val sourceHeaders = Headers.Builder()
|
||||
.add("A", "aa")
|
||||
.add("a", "aa")
|
||||
.add("B", "bb")
|
||||
.build()
|
||||
val headers = Headers.Builder()
|
||||
.add("A", "a")
|
||||
.addAll(sourceHeaders)
|
||||
.add("C", "c")
|
||||
.build()
|
||||
assertThat(headers.toString()).isEqualTo("A: a\nA: aa\na: aa\nB: bb\nC: c\n")
|
||||
}
|
||||
}
|
||||
41
okhttp/src/jsTest/kotlin/okhttp3/HeadersJsTest.kt
Normal file
41
okhttp/src/jsTest/kotlin/okhttp3/HeadersJsTest.kt
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package okhttp3
|
||||
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import kotlin.test.Test
|
||||
|
||||
class HeadersJsTest {
|
||||
@Test
|
||||
fun nameIndexesAreStrict() {
|
||||
val headers = Headers.headersOf("a", "b", "c", "d")
|
||||
assertThat(headers.name(-1)).isEqualTo(undefined)
|
||||
assertThat(headers.name(0)).isEqualTo("a")
|
||||
assertThat(headers.name(1)).isEqualTo("c")
|
||||
assertThat(headers.name(2)).isEqualTo(undefined)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun valueIndexesAreStrict() {
|
||||
val headers = Headers.headersOf("a", "b", "c", "d")
|
||||
assertThat(headers.value(-1)).isEqualTo(undefined)
|
||||
assertThat(headers.value(0)).isEqualTo("b")
|
||||
assertThat(headers.value(1)).isEqualTo("d")
|
||||
assertThat(headers.value(2)).isEqualTo(undefined)
|
||||
}
|
||||
}
|
||||
@@ -24,35 +24,34 @@ import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.TreeMap
|
||||
import java.util.TreeSet
|
||||
import okhttp3.Headers.Builder
|
||||
import okhttp3.internal.format
|
||||
import okhttp3.internal.commonAdd
|
||||
import okhttp3.internal.commonAddAll
|
||||
import okhttp3.internal.commonAddLenient
|
||||
import okhttp3.internal.commonBuild
|
||||
import okhttp3.internal.commonEquals
|
||||
import okhttp3.internal.commonGet
|
||||
import okhttp3.internal.commonHashCode
|
||||
import okhttp3.internal.commonHeadersGet
|
||||
import okhttp3.internal.commonHeadersOf
|
||||
import okhttp3.internal.commonIterator
|
||||
import okhttp3.internal.commonName
|
||||
import okhttp3.internal.commonNewBuilder
|
||||
import okhttp3.internal.commonRemoveAll
|
||||
import okhttp3.internal.commonSet
|
||||
import okhttp3.internal.commonToHeaders
|
||||
import okhttp3.internal.commonToString
|
||||
import okhttp3.internal.commonValue
|
||||
import okhttp3.internal.commonValues
|
||||
import okhttp3.internal.headersCheckName
|
||||
import okhttp3.internal.http.toHttpDateOrNull
|
||||
import okhttp3.internal.http.toHttpDateString
|
||||
import okhttp3.internal.isSensitiveHeader
|
||||
import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
|
||||
|
||||
/**
|
||||
* The header fields of a single HTTP message. Values are uninterpreted strings; use `Request` and
|
||||
* `Response` for interpreted headers. This class maintains the order of the header fields within
|
||||
* the HTTP message.
|
||||
*
|
||||
* This class tracks header values line-by-line. A field with multiple comma- separated values on
|
||||
* the same line will be treated as a field with a single value by this class. It is the caller's
|
||||
* responsibility to detect and split on commas if their field permits multiple values. This
|
||||
* simplifies use of single-valued fields whose values routinely contain commas, such as cookies or
|
||||
* dates.
|
||||
*
|
||||
* This class trims whitespace from values. It never returns values with leading or trailing
|
||||
* whitespace.
|
||||
*
|
||||
* Instances of this class are immutable. Use [Builder] to create instances.
|
||||
*/
|
||||
@Suppress("NAME_SHADOWING")
|
||||
class Headers private constructor(
|
||||
private val namesAndValues: Array<String>
|
||||
actual class Headers internal actual constructor(
|
||||
internal actual val namesAndValues: Array<String>
|
||||
) : Iterable<Pair<String, String>> {
|
||||
/** Returns the last value corresponding to the specified field, or null. */
|
||||
operator fun get(name: String): String? = get(namesAndValues, name)
|
||||
actual operator fun get(name: String): String? = commonHeadersGet(namesAndValues, name)
|
||||
|
||||
/**
|
||||
* Returns the last value corresponding to the specified field parsed as an HTTP date, or null if
|
||||
@@ -70,8 +69,7 @@ class Headers private constructor(
|
||||
return value?.toInstant()
|
||||
}
|
||||
|
||||
/** Returns the number of field values. */
|
||||
@get:JvmName("size") val size: Int
|
||||
@get:JvmName("size") actual val size: Int
|
||||
get() = namesAndValues.size / 2
|
||||
|
||||
@JvmName("-deprecated_size")
|
||||
@@ -81,14 +79,11 @@ class Headers private constructor(
|
||||
level = DeprecationLevel.ERROR)
|
||||
fun size(): Int = size
|
||||
|
||||
/** Returns the field at `position`. */
|
||||
fun name(index: Int): String = namesAndValues[index * 2]
|
||||
actual fun name(index: Int): String = commonName(index)
|
||||
|
||||
/** Returns the value at `index`. */
|
||||
fun value(index: Int): String = namesAndValues[index * 2 + 1]
|
||||
actual fun value(index: Int): String = commonValue(index)
|
||||
|
||||
/** Returns an immutable case-insensitive set of header names. */
|
||||
fun names(): Set<String> {
|
||||
actual fun names(): Set<String> {
|
||||
val result = TreeSet(String.CASE_INSENSITIVE_ORDER)
|
||||
for (i in 0 until size) {
|
||||
result.add(name(i))
|
||||
@@ -96,21 +91,7 @@ class Headers private constructor(
|
||||
return Collections.unmodifiableSet(result)
|
||||
}
|
||||
|
||||
/** Returns an immutable list of the header values for `name`. */
|
||||
fun values(name: String): List<String> {
|
||||
var result: MutableList<String>? = null
|
||||
for (i in 0 until size) {
|
||||
if (name.equals(name(i), ignoreCase = true)) {
|
||||
if (result == null) result = ArrayList(2)
|
||||
result.add(value(i))
|
||||
}
|
||||
}
|
||||
return if (result != null) {
|
||||
Collections.unmodifiableList(result)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
actual fun values(name: String): List<String> = commonValues(name)
|
||||
|
||||
/**
|
||||
* Returns the number of bytes required to encode these headers using HTTP/1.1. This is also the
|
||||
@@ -129,83 +110,15 @@ class Headers private constructor(
|
||||
return result
|
||||
}
|
||||
|
||||
override operator fun iterator(): Iterator<Pair<String, String>> {
|
||||
return Array(size) { name(it) to value(it) }.iterator()
|
||||
}
|
||||
actual override operator fun iterator(): Iterator<Pair<String, String>> = commonIterator()
|
||||
|
||||
fun newBuilder(): Builder {
|
||||
val result = Builder()
|
||||
result.namesAndValues += namesAndValues
|
||||
return result
|
||||
}
|
||||
actual fun newBuilder(): Builder = commonNewBuilder()
|
||||
|
||||
/**
|
||||
* Returns true if `other` is a `Headers` object with the same headers, with the same casing, in
|
||||
* the same order. Note that two headers instances may be *semantically* equal but not equal
|
||||
* according to this method. In particular, none of the following sets of headers are equal
|
||||
* according to this method:
|
||||
*
|
||||
* 1. Original
|
||||
* ```
|
||||
* Content-Type: text/html
|
||||
* Content-Length: 50
|
||||
* ```
|
||||
*
|
||||
* 2. Different order
|
||||
*
|
||||
* ```
|
||||
* Content-Length: 50
|
||||
* Content-Type: text/html
|
||||
* ```
|
||||
*
|
||||
* 3. Different case
|
||||
*
|
||||
* ```
|
||||
* content-type: text/html
|
||||
* content-length: 50
|
||||
* ```
|
||||
*
|
||||
* 4. Different values
|
||||
*
|
||||
* ```
|
||||
* Content-Type: text/html
|
||||
* Content-Length: 050
|
||||
* ```
|
||||
*
|
||||
* Applications that require semantically equal headers should convert them into a canonical form
|
||||
* before comparing them for equality.
|
||||
*/
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return other is Headers && namesAndValues.contentEquals(other.namesAndValues)
|
||||
}
|
||||
actual override fun equals(other: Any?): Boolean = commonEquals(other)
|
||||
|
||||
override fun hashCode(): Int = namesAndValues.contentHashCode()
|
||||
override fun hashCode(): Int = commonHashCode()
|
||||
|
||||
/**
|
||||
* Returns header names and values. The names and values are separated by `: ` and each pair is
|
||||
* followed by a newline character `\n`.
|
||||
*
|
||||
* Since OkHttp 5 this redacts these sensitive headers:
|
||||
*
|
||||
* * `Authorization`
|
||||
* * `Cookie`
|
||||
* * `Proxy-Authorization`
|
||||
* * `Set-Cookie`
|
||||
*
|
||||
* To get all headers as a human-readable string use `toMultimap().toString()`.
|
||||
*/
|
||||
override fun toString(): String {
|
||||
return buildString {
|
||||
for (i in 0 until size) {
|
||||
val name = name(i)
|
||||
val value = value(i)
|
||||
append(name)
|
||||
append(": ")
|
||||
append(if (isSensitiveHeader(name)) "██" else value)
|
||||
append("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
actual override fun toString(): String = commonToString()
|
||||
|
||||
fun toMultimap(): Map<String, List<String>> {
|
||||
val result = TreeMap<String, MutableList<String>>(String.CASE_INSENSITIVE_ORDER)
|
||||
@@ -221,8 +134,8 @@ class Headers private constructor(
|
||||
return result
|
||||
}
|
||||
|
||||
class Builder {
|
||||
internal val namesAndValues: MutableList<String> = ArrayList(20)
|
||||
actual class Builder {
|
||||
internal actual val namesAndValues: MutableList<String> = ArrayList(20)
|
||||
|
||||
/**
|
||||
* Add a header line without any validation. Only appropriate for headers from the remote peer
|
||||
@@ -253,32 +166,18 @@ class Headers private constructor(
|
||||
add(line.substring(0, index).trim(), line.substring(index + 1))
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a header with the specified name and value. Does validation of header names and values.
|
||||
*/
|
||||
fun add(name: String, value: String) = apply {
|
||||
checkName(name)
|
||||
checkValue(value, name)
|
||||
addLenient(name, value)
|
||||
}
|
||||
actual fun add(name: String, value: String) = commonAdd(name, value)
|
||||
|
||||
/**
|
||||
* Add a header with the specified name and value. Does validation of header names, allowing
|
||||
* non-ASCII values.
|
||||
*/
|
||||
fun addUnsafeNonAscii(name: String, value: String) = apply {
|
||||
checkName(name)
|
||||
headersCheckName(name)
|
||||
addLenient(name, value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds all headers from an existing collection.
|
||||
*/
|
||||
fun addAll(headers: Headers) = apply {
|
||||
for (i in 0 until headers.size) {
|
||||
addLenient(headers.name(i), headers.value(i))
|
||||
}
|
||||
}
|
||||
actual fun addAll(headers: Headers) = commonAddAll(headers)
|
||||
|
||||
/**
|
||||
* Add a header with the specified name and formatted date. Does validation of header names and
|
||||
@@ -318,83 +217,26 @@ class Headers private constructor(
|
||||
* Add a field with the specified value without any validation. Only appropriate for headers
|
||||
* from the remote peer or cache.
|
||||
*/
|
||||
internal fun addLenient(name: String, value: String) = apply {
|
||||
namesAndValues.add(name)
|
||||
namesAndValues.add(value.trim())
|
||||
}
|
||||
internal fun addLenient(name: String, value: String) = commonAddLenient(name, value)
|
||||
|
||||
fun removeAll(name: String) = apply {
|
||||
var i = 0
|
||||
while (i < namesAndValues.size) {
|
||||
if (name.equals(namesAndValues[i], ignoreCase = true)) {
|
||||
namesAndValues.removeAt(i) // name
|
||||
namesAndValues.removeAt(i) // value
|
||||
i -= 2
|
||||
}
|
||||
i += 2
|
||||
}
|
||||
}
|
||||
actual fun removeAll(name: String) = commonRemoveAll(name)
|
||||
|
||||
/**
|
||||
* Set a field with the specified value. If the field is not found, it is added. If the field is
|
||||
* found, the existing values are replaced.
|
||||
*/
|
||||
operator fun set(name: String, value: String) = apply {
|
||||
checkName(name)
|
||||
checkValue(value, name)
|
||||
removeAll(name)
|
||||
addLenient(name, value)
|
||||
}
|
||||
actual operator fun set(name: String, value: String) = commonSet(name, value)
|
||||
|
||||
/** Equivalent to `build().get(name)`, but potentially faster. */
|
||||
operator fun get(name: String): String? {
|
||||
for (i in namesAndValues.size - 2 downTo 0 step 2) {
|
||||
if (name.equals(namesAndValues[i], ignoreCase = true)) {
|
||||
return namesAndValues[i + 1]
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
actual operator fun get(name: String): String? = commonGet(name)
|
||||
|
||||
fun build(): Headers = Headers(namesAndValues.toTypedArray())
|
||||
actual fun build(): Headers = commonBuild()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun get(namesAndValues: Array<String>, name: String): String? {
|
||||
for (i in namesAndValues.size - 2 downTo 0 step 2) {
|
||||
if (name.equals(namesAndValues[i], ignoreCase = true)) {
|
||||
return namesAndValues[i + 1]
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns headers for the alternating header names and values. There must be an even number of
|
||||
* arguments, and they must alternate between header names and values.
|
||||
*/
|
||||
actual companion object {
|
||||
@JvmStatic
|
||||
@JvmName("of")
|
||||
fun headersOf(vararg namesAndValues: String): Headers {
|
||||
require(namesAndValues.size % 2 == 0) { "Expected alternating header names and values" }
|
||||
|
||||
// Make a defensive copy and clean it up.
|
||||
val namesAndValues: Array<String> = namesAndValues.clone() as Array<String>
|
||||
for (i in namesAndValues.indices) {
|
||||
require(namesAndValues[i] != null) { "Headers cannot be null" }
|
||||
namesAndValues[i] = namesAndValues[i].trim()
|
||||
}
|
||||
|
||||
// Check for malformed headers.
|
||||
for (i in namesAndValues.indices step 2) {
|
||||
val name = namesAndValues[i]
|
||||
val value = namesAndValues[i + 1]
|
||||
checkName(name)
|
||||
checkValue(value, name)
|
||||
}
|
||||
|
||||
return Headers(namesAndValues)
|
||||
}
|
||||
actual fun headersOf(vararg namesAndValues: String): Headers = commonHeadersOf(*namesAndValues)
|
||||
|
||||
@JvmName("-deprecated_of")
|
||||
@Deprecated(
|
||||
@@ -405,25 +247,9 @@ class Headers private constructor(
|
||||
return headersOf(*namesAndValues)
|
||||
}
|
||||
|
||||
/** Returns headers for the header names and values in the [Map]. */
|
||||
@JvmStatic
|
||||
@JvmName("of")
|
||||
fun Map<String, String>.toHeaders(): Headers {
|
||||
// Make a defensive copy and clean it up.
|
||||
val namesAndValues = arrayOfNulls<String>(size * 2)
|
||||
var i = 0
|
||||
for ((k, v) in this) {
|
||||
val name = k.trim()
|
||||
val value = v.trim()
|
||||
checkName(name)
|
||||
checkValue(value, name)
|
||||
namesAndValues[i] = name
|
||||
namesAndValues[i + 1] = value
|
||||
i += 2
|
||||
}
|
||||
|
||||
return Headers(namesAndValues as Array<String>)
|
||||
}
|
||||
actual fun Map<String, String>.toHeaders(): Headers = commonToHeaders()
|
||||
|
||||
@JvmName("-deprecated_of")
|
||||
@Deprecated(
|
||||
@@ -433,25 +259,5 @@ class Headers private constructor(
|
||||
fun of(headers: Map<String, String>): Headers {
|
||||
return headers.toHeaders()
|
||||
}
|
||||
|
||||
private fun checkName(name: String) {
|
||||
require(name.isNotEmpty()) { "name is empty" }
|
||||
for (i in name.indices) {
|
||||
val c = name[i]
|
||||
require(c in '\u0021'..'\u007e') {
|
||||
format("Unexpected char %#04x at %d in header name: %s", c.code, i, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkValue(value: String, name: String) {
|
||||
for (i in value.indices) {
|
||||
val c = value[i]
|
||||
require(c == '\t' || c in '\u0020'..'\u007e') {
|
||||
format("Unexpected char %#04x at %d in %s value", c.code, i, name) +
|
||||
(if (isSensitiveHeader(name)) "" else ": $value")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,466 +15,19 @@
|
||||
*/
|
||||
package okhttp3
|
||||
|
||||
import java.time.Instant
|
||||
import java.util.Date
|
||||
import okhttp3.Headers.Companion.headersOf
|
||||
import okhttp3.Headers.Companion.toHeaders
|
||||
import okhttp3.TestUtil.headerEntries
|
||||
import okhttp3.internal.EMPTY_HEADERS
|
||||
import okhttp3.internal.http.parseChallenges
|
||||
import okhttp3.internal.http2.Http2ExchangeCodec.Companion.http2HeadersList
|
||||
import okhttp3.internal.http2.Http2ExchangeCodec.Companion.readHttp2HeadersList
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.Assertions.fail
|
||||
import org.junit.jupiter.api.Disabled
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class HeadersTest {
|
||||
@Test fun readNameValueBlockDropsForbiddenHeadersHttp2() {
|
||||
val headerBlock = headersOf(
|
||||
":status", "200 OK",
|
||||
":version", "HTTP/1.1",
|
||||
"connection", "close"
|
||||
)
|
||||
val request = Request.Builder().url("http://square.com/").build()
|
||||
val response = readHttp2HeadersList(headerBlock, Protocol.HTTP_2).request(request).build()
|
||||
val headers = response.headers
|
||||
assertThat(headers.size).isEqualTo(1)
|
||||
assertThat(headers.name(0)).isEqualTo(":version")
|
||||
assertThat(headers.value(0)).isEqualTo("HTTP/1.1")
|
||||
}
|
||||
|
||||
@Test fun http2HeadersListDropsForbiddenHeadersHttp2() {
|
||||
val request = Request.Builder()
|
||||
.url("http://square.com/")
|
||||
.header("Connection", "upgrade")
|
||||
.header("Upgrade", "websocket")
|
||||
.header("Host", "square.com")
|
||||
.header("TE", "gzip")
|
||||
.build()
|
||||
val expected = headerEntries(
|
||||
":method", "GET",
|
||||
":path", "/",
|
||||
":authority", "square.com",
|
||||
":scheme", "http"
|
||||
)
|
||||
assertThat(http2HeadersList(request)).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test fun http2HeadersListDontDropTeIfTrailersHttp2() {
|
||||
val request = Request.Builder()
|
||||
.url("http://square.com/")
|
||||
.header("TE", "trailers")
|
||||
.build()
|
||||
val expected = headerEntries(
|
||||
":method", "GET",
|
||||
":path", "/",
|
||||
":scheme", "http",
|
||||
"te", "trailers"
|
||||
)
|
||||
assertThat(http2HeadersList(request)).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test fun ofTrims() {
|
||||
val headers = headersOf("\t User-Agent \n", " \r OkHttp ")
|
||||
assertThat(headers.name(0)).isEqualTo("User-Agent")
|
||||
assertThat(headers.value(0)).isEqualTo("OkHttp")
|
||||
}
|
||||
|
||||
@Test fun addParsing() {
|
||||
val headers = Headers.Builder()
|
||||
.add("foo: bar")
|
||||
.add(" foo: baz") // Name leading whitespace is trimmed.
|
||||
.add("foo : bak") // Name trailing whitespace is trimmed.
|
||||
.add("\tkey\t:\tvalue\t") // '\t' also counts as whitespace
|
||||
.add("ping: pong ") // Value whitespace is trimmed.
|
||||
.add("kit:kat") // Space after colon is not required.
|
||||
.build()
|
||||
assertThat(headers.values("foo")).containsExactly("bar", "baz", "bak")
|
||||
assertThat(headers.values("key")).containsExactly("value")
|
||||
assertThat(headers.values("ping")).containsExactly("pong")
|
||||
assertThat(headers.values("kit")).containsExactly("kat")
|
||||
}
|
||||
|
||||
@Test fun addThrowsOnEmptyName() {
|
||||
try {
|
||||
Headers.Builder().add(": bar")
|
||||
fail<Any>()
|
||||
} catch (expected: IllegalArgumentException) {
|
||||
}
|
||||
try {
|
||||
Headers.Builder().add(" : bar")
|
||||
fail<Any>()
|
||||
} catch (expected: IllegalArgumentException) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun addThrowsOnNoColon() {
|
||||
try {
|
||||
Headers.Builder().add("foo bar")
|
||||
fail<Any>()
|
||||
} catch (expected: IllegalArgumentException) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun addThrowsOnMultiColon() {
|
||||
try {
|
||||
Headers.Builder().add(":status: 200 OK")
|
||||
fail<Any>()
|
||||
} catch (expected: IllegalArgumentException) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun addUnsafeNonAsciiRejectsUnicodeName() {
|
||||
try {
|
||||
Headers.Builder()
|
||||
.addUnsafeNonAscii("héader1", "value1")
|
||||
.build()
|
||||
fail<Any>("Should have complained about invalid value")
|
||||
} catch (expected: IllegalArgumentException) {
|
||||
assertThat(expected.message).isEqualTo("Unexpected char 0xe9 at 1 in header name: héader1")
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun addUnsafeNonAsciiAcceptsUnicodeValue() {
|
||||
val headers = Headers.Builder()
|
||||
.addUnsafeNonAscii("header1", "valué1")
|
||||
.build()
|
||||
assertThat(headers.toString()).isEqualTo("header1: valué1\n")
|
||||
}
|
||||
|
||||
@Test fun ofThrowsOddNumberOfHeaders() {
|
||||
try {
|
||||
headersOf("User-Agent", "OkHttp", "Content-Length")
|
||||
fail<Any>()
|
||||
} catch (expected: IllegalArgumentException) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun ofThrowsOnEmptyName() {
|
||||
try {
|
||||
headersOf("", "OkHttp")
|
||||
fail<Any>()
|
||||
} catch (expected: IllegalArgumentException) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun ofAcceptsEmptyValue() {
|
||||
val headers = headersOf("User-Agent", "")
|
||||
assertThat(headers.value(0)).isEqualTo("")
|
||||
}
|
||||
|
||||
@Test fun ofMakesDefensiveCopy() {
|
||||
val namesAndValues = arrayOf(
|
||||
"User-Agent",
|
||||
"OkHttp"
|
||||
)
|
||||
val headers = headersOf(*namesAndValues)
|
||||
namesAndValues[1] = "Chrome"
|
||||
assertThat(headers.value(0)).isEqualTo("OkHttp")
|
||||
}
|
||||
|
||||
@Test fun ofRejectsNullChar() {
|
||||
try {
|
||||
headersOf("User-Agent", "Square\u0000OkHttp")
|
||||
fail<Any>()
|
||||
} catch (expected: IllegalArgumentException) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun ofMapThrowsOnNull() {
|
||||
try {
|
||||
(mapOf("User-Agent" to null) as Map<String, String>).toHeaders()
|
||||
fail<Any>()
|
||||
} catch (expected: NullPointerException) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun ofMapThrowsOnEmptyName() {
|
||||
try {
|
||||
mapOf("" to "OkHttp").toHeaders()
|
||||
fail<Any>()
|
||||
} catch (expected: IllegalArgumentException) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun ofMapThrowsOnBlankName() {
|
||||
try {
|
||||
mapOf(" " to "OkHttp").toHeaders()
|
||||
fail<Any>()
|
||||
} catch (expected: IllegalArgumentException) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun ofMapAcceptsEmptyValue() {
|
||||
val headers = mapOf("User-Agent" to "").toHeaders()
|
||||
assertThat(headers.value(0)).isEqualTo("")
|
||||
}
|
||||
|
||||
@Test fun ofMapTrimsKey() {
|
||||
val headers = mapOf(" User-Agent " to "OkHttp").toHeaders()
|
||||
assertThat(headers.name(0)).isEqualTo("User-Agent")
|
||||
}
|
||||
|
||||
@Test fun ofMapTrimsValue() {
|
||||
val headers = mapOf("User-Agent" to " OkHttp ").toHeaders()
|
||||
assertThat(headers.value(0)).isEqualTo("OkHttp")
|
||||
}
|
||||
|
||||
@Test fun ofMapMakesDefensiveCopy() {
|
||||
val namesAndValues = mutableMapOf<String, String>()
|
||||
namesAndValues["User-Agent"] = "OkHttp"
|
||||
val headers = namesAndValues.toHeaders()
|
||||
namesAndValues["User-Agent"] = "Chrome"
|
||||
assertThat(headers.value(0)).isEqualTo("OkHttp")
|
||||
}
|
||||
|
||||
@Test fun ofMapRejectsNullCharInName() {
|
||||
try {
|
||||
mapOf("User-\u0000Agent" to "OkHttp").toHeaders()
|
||||
fail<Any>()
|
||||
} catch (expected: IllegalArgumentException) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun ofMapRejectsNullCharInValue() {
|
||||
try {
|
||||
mapOf("User-Agent" to "Square\u0000OkHttp").toHeaders()
|
||||
fail<Any>()
|
||||
} catch (expected: IllegalArgumentException) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun toMultimapGroupsHeaders() {
|
||||
val headers = headersOf(
|
||||
"cache-control", "no-cache",
|
||||
"cache-control", "no-store",
|
||||
"user-agent", "OkHttp"
|
||||
)
|
||||
val headerMap = headers.toMultimap()
|
||||
assertThat(headerMap["cache-control"]!!.size).isEqualTo(2)
|
||||
assertThat(headerMap["user-agent"]!!.size).isEqualTo(1)
|
||||
}
|
||||
|
||||
@Test fun toMultimapUsesCanonicalCase() {
|
||||
val headers = headersOf(
|
||||
"cache-control", "no-store",
|
||||
"Cache-Control", "no-cache",
|
||||
"User-Agent", "OkHttp"
|
||||
)
|
||||
val headerMap = headers.toMultimap()
|
||||
assertThat(headerMap["cache-control"]!!.size).isEqualTo(2)
|
||||
assertThat(headerMap["user-agent"]!!.size).isEqualTo(1)
|
||||
}
|
||||
|
||||
@Test fun toMultimapAllowsCaseInsensitiveGet() {
|
||||
val headers = headersOf(
|
||||
"cache-control", "no-store",
|
||||
"Cache-Control", "no-cache"
|
||||
)
|
||||
val headerMap = headers.toMultimap()
|
||||
assertThat(headerMap["cache-control"]!!.size).isEqualTo(2)
|
||||
assertThat(headerMap["Cache-Control"]!!.size).isEqualTo(2)
|
||||
}
|
||||
|
||||
@Test fun nameIndexesAreStrict() {
|
||||
val headers = headersOf("a", "b", "c", "d")
|
||||
try {
|
||||
headers.name(-1)
|
||||
fail<Any>()
|
||||
} catch (expected: IndexOutOfBoundsException) {
|
||||
}
|
||||
assertThat(headers.name(0)).isEqualTo("a")
|
||||
assertThat(headers.name(1)).isEqualTo("c")
|
||||
try {
|
||||
headers.name(2)
|
||||
fail<Any>()
|
||||
} catch (expected: IndexOutOfBoundsException) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun valueIndexesAreStrict() {
|
||||
val headers = headersOf("a", "b", "c", "d")
|
||||
try {
|
||||
headers.value(-1)
|
||||
fail<Any>()
|
||||
} catch (expected: IndexOutOfBoundsException) {
|
||||
}
|
||||
assertThat(headers.value(0)).isEqualTo("b")
|
||||
assertThat(headers.value(1)).isEqualTo("d")
|
||||
try {
|
||||
headers.value(2)
|
||||
fail<Any>()
|
||||
} catch (expected: IndexOutOfBoundsException) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun builderRejectsUnicodeInHeaderName() {
|
||||
try {
|
||||
Headers.Builder().add("héader1", "value1")
|
||||
fail<Any>("Should have complained about invalid name")
|
||||
} catch (expected: IllegalArgumentException) {
|
||||
assertThat(expected.message)
|
||||
.isEqualTo("Unexpected char 0xe9 at 1 in header name: héader1")
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun builderRejectsUnicodeInHeaderValue() {
|
||||
try {
|
||||
Headers.Builder().add("header1", "valué1")
|
||||
fail<Any>("Should have complained about invalid value")
|
||||
} catch (expected: IllegalArgumentException) {
|
||||
assertThat(expected.message)
|
||||
.isEqualTo("Unexpected char 0xe9 at 4 in header1 value: valué1")
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun varargFactoryRejectsUnicodeInHeaderName() {
|
||||
try {
|
||||
headersOf("héader1", "value1")
|
||||
fail<Any>("Should have complained about invalid value")
|
||||
} catch (expected: IllegalArgumentException) {
|
||||
assertThat(expected.message)
|
||||
.isEqualTo("Unexpected char 0xe9 at 1 in header name: héader1")
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun varargFactoryRejectsUnicodeInHeaderValue() {
|
||||
try {
|
||||
headersOf("header1", "valué1")
|
||||
fail<Any>("Should have complained about invalid value")
|
||||
} catch (expected: IllegalArgumentException) {
|
||||
assertThat(expected.message)
|
||||
.isEqualTo("Unexpected char 0xe9 at 4 in header1 value: valué1")
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun mapFactoryRejectsUnicodeInHeaderName() {
|
||||
try {
|
||||
mapOf("héader1" to "value1").toHeaders()
|
||||
fail<Any>("Should have complained about invalid value")
|
||||
} catch (expected: IllegalArgumentException) {
|
||||
assertThat(expected.message)
|
||||
.isEqualTo("Unexpected char 0xe9 at 1 in header name: héader1")
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun mapFactoryRejectsUnicodeInHeaderValue() {
|
||||
try {
|
||||
mapOf("header1" to "valué1").toHeaders()
|
||||
fail<Any>("Should have complained about invalid value")
|
||||
} catch (expected: IllegalArgumentException) {
|
||||
assertThat(expected.message)
|
||||
.isEqualTo("Unexpected char 0xe9 at 4 in header1 value: valué1")
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun sensitiveHeadersNotIncludedInExceptions() {
|
||||
try {
|
||||
Headers.Builder().add("Authorization", "valué1")
|
||||
fail<Any>("Should have complained about invalid name")
|
||||
} catch (expected: IllegalArgumentException) {
|
||||
assertThat(expected.message)
|
||||
.isEqualTo("Unexpected char 0xe9 at 4 in Authorization value")
|
||||
}
|
||||
try {
|
||||
Headers.Builder().add("Cookie", "valué1")
|
||||
fail<Any>("Should have complained about invalid name")
|
||||
} catch (expected: IllegalArgumentException) {
|
||||
assertThat(expected.message)
|
||||
.isEqualTo("Unexpected char 0xe9 at 4 in Cookie value")
|
||||
}
|
||||
try {
|
||||
Headers.Builder().add("Proxy-Authorization", "valué1")
|
||||
fail<Any>("Should have complained about invalid name")
|
||||
} catch (expected: IllegalArgumentException) {
|
||||
assertThat(expected.message)
|
||||
.isEqualTo("Unexpected char 0xe9 at 4 in Proxy-Authorization value")
|
||||
}
|
||||
try {
|
||||
Headers.Builder().add("Set-Cookie", "valué1")
|
||||
fail<Any>("Should have complained about invalid name")
|
||||
} catch (expected: IllegalArgumentException) {
|
||||
assertThat(expected.message)
|
||||
.isEqualTo("Unexpected char 0xe9 at 4 in Set-Cookie value")
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun headersEquals() {
|
||||
val headers1 = Headers.Builder()
|
||||
.add("Connection", "close")
|
||||
.add("Transfer-Encoding", "chunked")
|
||||
.build()
|
||||
val headers2 = Headers.Builder()
|
||||
.add("Connection", "close")
|
||||
.add("Transfer-Encoding", "chunked")
|
||||
.build()
|
||||
assertThat(headers2).isEqualTo(headers1)
|
||||
assertThat(headers2.hashCode()).isEqualTo(headers1.hashCode())
|
||||
}
|
||||
|
||||
@Test fun headersNotEquals() {
|
||||
val headers1 = Headers.Builder()
|
||||
.add("Connection", "close")
|
||||
.add("Transfer-Encoding", "chunked")
|
||||
.build()
|
||||
val headers2 = Headers.Builder()
|
||||
.add("Connection", "keep-alive")
|
||||
.add("Transfer-Encoding", "chunked")
|
||||
.build()
|
||||
assertThat(headers2).isNotEqualTo(headers1)
|
||||
assertThat(headers2.hashCode()).isNotEqualTo(headers1.hashCode().toLong())
|
||||
}
|
||||
|
||||
@Test fun headersToString() {
|
||||
val headers = Headers.Builder()
|
||||
.add("A", "a")
|
||||
.add("B", "bb")
|
||||
.build()
|
||||
assertThat(headers.toString()).isEqualTo("A: a\nB: bb\n")
|
||||
}
|
||||
|
||||
@Test fun headersToStringRedactsSensitiveHeaders() {
|
||||
val headers = Headers.Builder()
|
||||
.add("content-length", "99")
|
||||
.add("authorization", "peanutbutter")
|
||||
.add("proxy-authorization", "chocolate")
|
||||
.add("cookie", "drink=coffee")
|
||||
.add("set-cookie", "accessory=sugar")
|
||||
.add("user-agent", "OkHttp")
|
||||
.build()
|
||||
assertThat(headers.toString()).isEqualTo(
|
||||
"""
|
||||
|content-length: 99
|
||||
|authorization: ██
|
||||
|proxy-authorization: ██
|
||||
|cookie: ██
|
||||
|set-cookie: ██
|
||||
|user-agent: OkHttp
|
||||
|""".trimMargin()
|
||||
)
|
||||
}
|
||||
|
||||
@Test fun headersAddAll() {
|
||||
val sourceHeaders = Headers.Builder()
|
||||
.add("A", "aa")
|
||||
.add("a", "aa")
|
||||
.add("B", "bb")
|
||||
.build()
|
||||
val headers = Headers.Builder()
|
||||
.add("A", "a")
|
||||
.addAll(sourceHeaders)
|
||||
.add("C", "c")
|
||||
.build()
|
||||
assertThat(headers.toString()).isEqualTo("A: a\nA: aa\na: aa\nB: bb\nC: c\n")
|
||||
}
|
||||
class HeadersChallengesTest {
|
||||
|
||||
/** See https://github.com/square/okhttp/issues/2780. */
|
||||
@Test fun testDigestChallengeWithStrictRfc2617Header() {
|
||||
val headers = Headers.Builder()
|
||||
.add(
|
||||
"WWW-Authenticate", "Digest realm=\"myrealm\", nonce=\"fjalskdflwejrlaskdfjlaskdjflaks"
|
||||
+ "jdflkasdf\", qop=\"auth\", stale=\"FALSE\""
|
||||
+ "jdflkasdf\", qop=\"auth\", stale=\"FALSE\""
|
||||
)
|
||||
.build()
|
||||
val challenges = headers.parseChallenges("WWW-Authenticate")
|
||||
@@ -493,7 +46,7 @@ class HeadersTest {
|
||||
val headers = Headers.Builder()
|
||||
.add(
|
||||
"WWW-Authenticate", "Digest qop=\"auth\", realm=\"myrealm\", nonce=\"fjalskdflwejrlask"
|
||||
+ "dfjlaskdjflaksjdflkasdf\", stale=\"FALSE\""
|
||||
+ "dfjlaskdjflaksjdflkasdf\", stale=\"FALSE\""
|
||||
)
|
||||
.build()
|
||||
val challenges = headers.parseChallenges("WWW-Authenticate")
|
||||
@@ -512,7 +65,7 @@ class HeadersTest {
|
||||
val headers = Headers.Builder()
|
||||
.add(
|
||||
"WWW-Authenticate", "Digest qop=\"auth\", nonce=\"fjalskdflwejrlaskdfjlaskdjflaksjdflk"
|
||||
+ "asdf\", realm=\"myrealm\", stale=\"FALSE\""
|
||||
+ "asdf\", realm=\"myrealm\", stale=\"FALSE\""
|
||||
)
|
||||
.build()
|
||||
val challenges = headers.parseChallenges("WWW-Authenticate")
|
||||
@@ -531,7 +84,7 @@ class HeadersTest {
|
||||
val headers = Headers.Builder()
|
||||
.add(
|
||||
"WWW-Authenticate", "Digest qop=\"auth\", underrealm=\"myrealm\", nonce=\"fjalskdflwej"
|
||||
+ "rlaskdfjlaskdjflaksjdflkasdf\", stale=\"FALSE\""
|
||||
+ "rlaskdfjlaskdjflaksjdflkasdf\", stale=\"FALSE\""
|
||||
)
|
||||
.build()
|
||||
val challenges = headers.parseChallenges("WWW-Authenticate")
|
||||
@@ -550,7 +103,7 @@ class HeadersTest {
|
||||
val headers = Headers.Builder()
|
||||
.add(
|
||||
"WWW-Authenticate", "Digest qop=\"auth\", realm=\"myrealm\", nonce=\"fjalskdflwejrl"
|
||||
+ "askdfjlaskdjflaksjdflkasdf\", stale=\"FALSE\""
|
||||
+ "askdfjlaskdjflaksjdflkasdf\", stale=\"FALSE\""
|
||||
)
|
||||
.build()
|
||||
val challenges = headers.parseChallenges("WWW-Authenticate")
|
||||
@@ -569,7 +122,7 @@ class HeadersTest {
|
||||
val headers = Headers.Builder()
|
||||
.add(
|
||||
"WWW-Authenticate", "Digest realm=\"myrealm\", nonce=\"fjalskdflwejrlaskdfjlaskdjfl"
|
||||
+ "aksjdflkasdf\", qop=\"auth\", stale=\"FALSE\""
|
||||
+ "aksjdflkasdf\", qop=\"auth\", stale=\"FALSE\""
|
||||
)
|
||||
.build()
|
||||
val challenges = headers.parseChallenges("WWW-Authenticate")
|
||||
@@ -588,7 +141,7 @@ class HeadersTest {
|
||||
val headers = Headers.Builder()
|
||||
.add(
|
||||
"WWW-Authenticate", "DiGeSt qop=\"auth\", rEaLm=\"myrealm\", nonce=\"fjalskdflwejrlask"
|
||||
+ "dfjlaskdjflaksjdflkasdf\", stale=\"FALSE\""
|
||||
+ "dfjlaskdjflaksjdflkasdf\", stale=\"FALSE\""
|
||||
)
|
||||
.build()
|
||||
val challenges = headers.parseChallenges("WWW-Authenticate")
|
||||
@@ -608,7 +161,7 @@ class HeadersTest {
|
||||
val headers = Headers.Builder()
|
||||
.add(
|
||||
"WWW-Authenticate", "DIgEsT rEaLm=\"myrealm\", nonce=\"fjalskdflwejrlaskdfjlaskdjflaks"
|
||||
+ "jdflkasdf\", qop=\"auth\", stale=\"FALSE\""
|
||||
+ "jdflkasdf\", qop=\"auth\", stale=\"FALSE\""
|
||||
)
|
||||
.build()
|
||||
val challenges = headers.parseChallenges("WWW-Authenticate")
|
||||
@@ -808,7 +361,7 @@ class HeadersTest {
|
||||
.build()
|
||||
assertThat(headers.parseChallenges("WWW-Authenticate"))
|
||||
.isEqualTo(listOf(Challenge("Other", mapOf(null to "abc==")))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test fun token68AndAuthParams() {
|
||||
@@ -859,59 +412,4 @@ class HeadersTest {
|
||||
Challenge("Basic", mapOf("realm" to "myotherrealm"))
|
||||
)
|
||||
}
|
||||
|
||||
@Test fun byteCount() {
|
||||
assertThat(EMPTY_HEADERS.byteCount()).isEqualTo(0L)
|
||||
assertThat(
|
||||
Headers.Builder()
|
||||
.add("abc", "def")
|
||||
.build()
|
||||
.byteCount()
|
||||
).isEqualTo(10L)
|
||||
assertThat(
|
||||
Headers.Builder()
|
||||
.add("abc", "def")
|
||||
.add("ghi", "jkl")
|
||||
.build()
|
||||
.byteCount()
|
||||
).isEqualTo(20L)
|
||||
}
|
||||
|
||||
@Test fun addDate() {
|
||||
val expected = Date(0L)
|
||||
val headers = Headers.Builder()
|
||||
.add("testDate", expected)
|
||||
.build()
|
||||
assertThat(headers["testDate"]).isEqualTo("Thu, 01 Jan 1970 00:00:00 GMT")
|
||||
assertThat(headers.getDate("testDate")).isEqualTo(Date(0L))
|
||||
}
|
||||
|
||||
@Test fun addInstant() {
|
||||
val expected = Instant.ofEpochMilli(0L)
|
||||
val headers = Headers.Builder()
|
||||
.add("Test-Instant", expected)
|
||||
.build()
|
||||
assertThat(headers["Test-Instant"]).isEqualTo("Thu, 01 Jan 1970 00:00:00 GMT")
|
||||
assertThat(headers.getInstant("Test-Instant")).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test fun setDate() {
|
||||
val expected = Date(1000)
|
||||
val headers = Headers.Builder()
|
||||
.add("testDate", Date(0L))
|
||||
.set("testDate", expected)
|
||||
.build()
|
||||
assertThat(headers["testDate"]).isEqualTo("Thu, 01 Jan 1970 00:00:01 GMT")
|
||||
assertThat(headers.getDate("testDate")).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test fun setInstant() {
|
||||
val expected = Instant.ofEpochMilli(1000L)
|
||||
val headers = Headers.Builder()
|
||||
.add("Test-Instant", Instant.ofEpochMilli(0L))
|
||||
.set("Test-Instant", expected)
|
||||
.build()
|
||||
assertThat(headers["Test-Instant"]).isEqualTo("Thu, 01 Jan 1970 00:00:01 GMT")
|
||||
assertThat(headers.getInstant("Test-Instant")).isEqualTo(expected)
|
||||
}
|
||||
}
|
||||
217
okhttp/src/jvmTest/java/okhttp3/HeadersJvmTest.kt
Normal file
217
okhttp/src/jvmTest/java/okhttp3/HeadersJvmTest.kt
Normal file
@@ -0,0 +1,217 @@
|
||||
/*
|
||||
* Copyright (C) 2012 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package okhttp3
|
||||
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import kotlin.test.fail
|
||||
import okhttp3.Headers.Companion.toHeaders
|
||||
import okhttp3.internal.EMPTY_HEADERS
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class HeadersJvmTest {
|
||||
|
||||
@Test fun byteCount() {
|
||||
assertThat(EMPTY_HEADERS.byteCount()).isEqualTo(0L)
|
||||
assertThat(
|
||||
Headers.Builder()
|
||||
.add("abc", "def")
|
||||
.build()
|
||||
.byteCount()
|
||||
).isEqualTo(10L)
|
||||
assertThat(
|
||||
Headers.Builder()
|
||||
.add("abc", "def")
|
||||
.add("ghi", "jkl")
|
||||
.build()
|
||||
.byteCount()
|
||||
).isEqualTo(20L)
|
||||
}
|
||||
|
||||
@Test fun addDate() {
|
||||
val expected = Date(0L)
|
||||
val headers = Headers.Builder()
|
||||
.add("testDate", expected)
|
||||
.build()
|
||||
assertThat(headers["testDate"]).isEqualTo("Thu, 01 Jan 1970 00:00:00 GMT")
|
||||
assertThat(headers.getDate("testDate")).isEqualTo(Date(0L))
|
||||
}
|
||||
|
||||
@Test fun addInstant() {
|
||||
val expected = Instant.ofEpochMilli(0L)
|
||||
val headers = Headers.Builder()
|
||||
.add("Test-Instant", expected)
|
||||
.build()
|
||||
assertThat(headers["Test-Instant"]).isEqualTo("Thu, 01 Jan 1970 00:00:00 GMT")
|
||||
assertThat(headers.getInstant("Test-Instant")).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test fun setDate() {
|
||||
val expected = Date(1000)
|
||||
val headers = Headers.Builder()
|
||||
.add("testDate", Date(0L))
|
||||
.set("testDate", expected)
|
||||
.build()
|
||||
assertThat(headers["testDate"]).isEqualTo("Thu, 01 Jan 1970 00:00:01 GMT")
|
||||
assertThat(headers.getDate("testDate")).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test fun setInstant() {
|
||||
val expected = Instant.ofEpochMilli(1000L)
|
||||
val headers = Headers.Builder()
|
||||
.add("Test-Instant", Instant.ofEpochMilli(0L))
|
||||
.set("Test-Instant", expected)
|
||||
.build()
|
||||
assertThat(headers["Test-Instant"]).isEqualTo("Thu, 01 Jan 1970 00:00:01 GMT")
|
||||
assertThat(headers.getInstant("Test-Instant")).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test fun addParsing() {
|
||||
val headers = Headers.Builder()
|
||||
.add("foo: bar")
|
||||
.add(" foo: baz") // Name leading whitespace is trimmed.
|
||||
.add("foo : bak") // Name trailing whitespace is trimmed.
|
||||
.add("\tkey\t:\tvalue\t") // '\t' also counts as whitespace
|
||||
.add("ping: pong ") // Value whitespace is trimmed.
|
||||
.add("kit:kat") // Space after colon is not required.
|
||||
.build()
|
||||
assertThat(headers.values("foo")).containsExactly("bar", "baz", "bak")
|
||||
assertThat(headers.values("key")).containsExactly("value")
|
||||
assertThat(headers.values("ping")).containsExactly("pong")
|
||||
assertThat(headers.values("kit")).containsExactly("kat")
|
||||
}
|
||||
|
||||
@Test fun addThrowsOnEmptyName() {
|
||||
try {
|
||||
Headers.Builder().add(": bar")
|
||||
fail()
|
||||
} catch (expected: IllegalArgumentException) {
|
||||
}
|
||||
try {
|
||||
Headers.Builder().add(" : bar")
|
||||
fail()
|
||||
} catch (expected: IllegalArgumentException) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun addThrowsOnNoColon() {
|
||||
try {
|
||||
Headers.Builder().add("foo bar")
|
||||
fail()
|
||||
} catch (expected: IllegalArgumentException) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun addThrowsOnMultiColon() {
|
||||
try {
|
||||
Headers.Builder().add(":status: 200 OK")
|
||||
fail()
|
||||
} catch (expected: IllegalArgumentException) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun addUnsafeNonAsciiRejectsUnicodeName() {
|
||||
try {
|
||||
Headers.Builder()
|
||||
.addUnsafeNonAscii("héader1", "value1")
|
||||
.build()
|
||||
fail("Should have complained about invalid value")
|
||||
} catch (expected: IllegalArgumentException) {
|
||||
assertThat(expected.message).isEqualTo("Unexpected char 0xe9 at 1 in header name: héader1")
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun addUnsafeNonAsciiAcceptsUnicodeValue() {
|
||||
val headers = Headers.Builder()
|
||||
.addUnsafeNonAscii("header1", "valué1")
|
||||
.build()
|
||||
assertThat(headers.toString()).isEqualTo("header1: valué1\n")
|
||||
}
|
||||
|
||||
// Fails on JS, ClassCastException: Illegal cast
|
||||
@Test fun ofMapThrowsOnNull() {
|
||||
try {
|
||||
(mapOf("User-Agent" to null) as Map<String, String>).toHeaders()
|
||||
fail()
|
||||
} catch (expected: NullPointerException) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun toMultimapGroupsHeaders() {
|
||||
val headers = Headers.headersOf(
|
||||
"cache-control", "no-cache",
|
||||
"cache-control", "no-store",
|
||||
"user-agent", "OkHttp"
|
||||
)
|
||||
val headerMap = headers.toMultimap()
|
||||
assertThat(headerMap["cache-control"]!!.size).isEqualTo(2)
|
||||
assertThat(headerMap["user-agent"]!!.size).isEqualTo(1)
|
||||
}
|
||||
|
||||
@Test fun toMultimapUsesCanonicalCase() {
|
||||
val headers = Headers.headersOf(
|
||||
"cache-control", "no-store",
|
||||
"Cache-Control", "no-cache",
|
||||
"User-Agent", "OkHttp"
|
||||
)
|
||||
val headerMap = headers.toMultimap()
|
||||
assertThat(headerMap["cache-control"]!!.size).isEqualTo(2)
|
||||
assertThat(headerMap["user-agent"]!!.size).isEqualTo(1)
|
||||
}
|
||||
|
||||
@Test fun toMultimapAllowsCaseInsensitiveGet() {
|
||||
val headers = Headers.headersOf(
|
||||
"cache-control", "no-store",
|
||||
"Cache-Control", "no-cache"
|
||||
)
|
||||
val headerMap = headers.toMultimap()
|
||||
assertThat(headerMap["cache-control"]!!.size).isEqualTo(2)
|
||||
assertThat(headerMap["Cache-Control"]!!.size).isEqualTo(2)
|
||||
}
|
||||
|
||||
@Test fun nameIndexesAreStrict() {
|
||||
val headers = Headers.headersOf("a", "b", "c", "d")
|
||||
try {
|
||||
headers.name(-1)
|
||||
fail()
|
||||
} catch (expected: IndexOutOfBoundsException) {
|
||||
}
|
||||
assertThat(headers.name(0)).isEqualTo("a")
|
||||
assertThat(headers.name(1)).isEqualTo("c")
|
||||
try {
|
||||
headers.name(2)
|
||||
fail()
|
||||
} catch (expected: IndexOutOfBoundsException) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun valueIndexesAreStrict() {
|
||||
val headers = Headers.headersOf("a", "b", "c", "d")
|
||||
try {
|
||||
headers.value(-1)
|
||||
fail()
|
||||
} catch (expected: IndexOutOfBoundsException) {
|
||||
}
|
||||
assertThat(headers.value(0)).isEqualTo("b")
|
||||
assertThat(headers.value(1)).isEqualTo("d")
|
||||
try {
|
||||
headers.value(2)
|
||||
fail()
|
||||
} catch (expected: IndexOutOfBoundsException) {
|
||||
}
|
||||
}
|
||||
}
|
||||
70
okhttp/src/jvmTest/java/okhttp3/HeadersRequestTest.kt
Normal file
70
okhttp/src/jvmTest/java/okhttp3/HeadersRequestTest.kt
Normal file
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright (C) 2012 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package okhttp3
|
||||
|
||||
import okhttp3.Headers.Companion.headersOf
|
||||
import okhttp3.TestUtil.headerEntries
|
||||
import okhttp3.internal.http2.Http2ExchangeCodec.Companion.http2HeadersList
|
||||
import okhttp3.internal.http2.Http2ExchangeCodec.Companion.readHttp2HeadersList
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class HeadersRequestTest {
|
||||
@Test fun readNameValueBlockDropsForbiddenHeadersHttp2() {
|
||||
val headerBlock = headersOf(
|
||||
":status", "200 OK",
|
||||
":version", "HTTP/1.1",
|
||||
"connection", "close"
|
||||
)
|
||||
val request = Request.Builder().url("http://square.com/").build()
|
||||
val response = readHttp2HeadersList(headerBlock, Protocol.HTTP_2).request(request).build()
|
||||
val headers = response.headers
|
||||
assertThat(headers.size).isEqualTo(1)
|
||||
assertThat(headers.name(0)).isEqualTo(":version")
|
||||
assertThat(headers.value(0)).isEqualTo("HTTP/1.1")
|
||||
}
|
||||
|
||||
@Test fun http2HeadersListDropsForbiddenHeadersHttp2() {
|
||||
val request = Request.Builder()
|
||||
.url("http://square.com/")
|
||||
.header("Connection", "upgrade")
|
||||
.header("Upgrade", "websocket")
|
||||
.header("Host", "square.com")
|
||||
.header("TE", "gzip")
|
||||
.build()
|
||||
val expected = headerEntries(
|
||||
":method", "GET",
|
||||
":path", "/",
|
||||
":authority", "square.com",
|
||||
":scheme", "http"
|
||||
)
|
||||
assertThat(http2HeadersList(request)).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test fun http2HeadersListDontDropTeIfTrailersHttp2() {
|
||||
val request = Request.Builder()
|
||||
.url("http://square.com/")
|
||||
.header("TE", "trailers")
|
||||
.build()
|
||||
val expected = headerEntries(
|
||||
":method", "GET",
|
||||
":path", "/",
|
||||
":scheme", "http",
|
||||
"te", "trailers"
|
||||
)
|
||||
assertThat(http2HeadersList(request)).isEqualTo(expected)
|
||||
}
|
||||
}
|
||||
111
okhttp/src/nonJvmMain/kotlin/okhttp3/Headers.kt
Normal file
111
okhttp/src/nonJvmMain/kotlin/okhttp3/Headers.kt
Normal file
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package okhttp3
|
||||
|
||||
import okhttp3.internal.commonAdd
|
||||
import okhttp3.internal.commonAddAll
|
||||
import okhttp3.internal.commonAddLenient
|
||||
import okhttp3.internal.commonBuild
|
||||
import okhttp3.internal.commonEquals
|
||||
import okhttp3.internal.commonGet
|
||||
import okhttp3.internal.commonHashCode
|
||||
import okhttp3.internal.commonHeadersGet
|
||||
import okhttp3.internal.commonHeadersOf
|
||||
import okhttp3.internal.commonIterator
|
||||
import okhttp3.internal.commonName
|
||||
import okhttp3.internal.commonNewBuilder
|
||||
import okhttp3.internal.commonRemoveAll
|
||||
import okhttp3.internal.commonSet
|
||||
import okhttp3.internal.commonToHeaders
|
||||
import okhttp3.internal.commonToString
|
||||
import okhttp3.internal.commonValue
|
||||
import okhttp3.internal.commonValues
|
||||
|
||||
@Suppress("NAME_SHADOWING")
|
||||
actual class Headers internal actual constructor(
|
||||
internal actual val namesAndValues: Array<String>
|
||||
) : Iterable<Pair<String, String>> {
|
||||
actual operator fun get(name: String): String? = commonHeadersGet(namesAndValues, name)
|
||||
|
||||
actual val size: Int
|
||||
get() = namesAndValues.size / 2
|
||||
|
||||
actual fun name(index: Int): String = commonName(index)
|
||||
|
||||
actual fun value(index: Int): String = commonValue(index)
|
||||
|
||||
actual fun names(): Set<String> {
|
||||
return (0 until size step 2).map { name(it) }.distinctBy { it.lowercase() }.toSet()
|
||||
}
|
||||
|
||||
actual fun values(name: String): List<String> = commonValues(name)
|
||||
|
||||
/**
|
||||
* Returns the number of bytes required to encode these headers using HTTP/1.1. This is also the
|
||||
* approximate size of HTTP/2 headers before they are compressed with HPACK. This value is
|
||||
* intended to be used as a metric: smaller headers are more efficient to encode and transmit.
|
||||
*/
|
||||
fun byteCount(): Long {
|
||||
// Each header name has 2 bytes of overhead for ': ' and every header value has 2 bytes of
|
||||
// overhead for '\r\n'.
|
||||
var result = (namesAndValues.size * 2).toLong()
|
||||
|
||||
for (i in 0 until namesAndValues.size) {
|
||||
result += namesAndValues[i].length.toLong()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
actual override operator fun iterator(): Iterator<Pair<String, String>> = commonIterator()
|
||||
|
||||
actual fun newBuilder(): Builder = commonNewBuilder()
|
||||
|
||||
actual override fun equals(other: Any?): Boolean = commonEquals(other)
|
||||
|
||||
override fun hashCode(): Int = commonHashCode()
|
||||
|
||||
actual override fun toString(): String = commonToString()
|
||||
|
||||
actual class Builder {
|
||||
internal actual val namesAndValues: MutableList<String> = ArrayList(20)
|
||||
|
||||
actual fun add(name: String, value: String) = commonAdd(name, value)
|
||||
|
||||
actual fun addAll(headers: Headers) = commonAddAll(headers)
|
||||
|
||||
actual fun removeAll(name: String) = commonRemoveAll(name)
|
||||
|
||||
/**
|
||||
* Set a field with the specified value. If the field is not found, it is added. If the field is
|
||||
* found, the existing values are replaced.
|
||||
*/
|
||||
actual operator fun set(name: String, value: String) = commonSet(name, value)
|
||||
|
||||
/** Equivalent to `build().get(name)`, but potentially faster. */
|
||||
actual operator fun get(name: String): String? = commonGet(name)
|
||||
|
||||
actual fun build(): Headers = commonBuild()
|
||||
}
|
||||
|
||||
actual companion object {
|
||||
actual fun headersOf(vararg namesAndValues: String): Headers = commonHeadersOf(*namesAndValues)
|
||||
|
||||
actual fun Map<String, String>.toHeaders(): Headers = commonToHeaders()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user