From 22ebeccb5499da266d333968c494cd6ed2cfa362 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Quenaudon?= Date: Mon, 18 Dec 2023 15:25:02 -0500 Subject: [PATCH] Port all common documentations to jvm (#8146) --- .../jvmMain/kotlin/okhttp3/CacheControl.kt | 53 ++ okhttp/src/jvmMain/kotlin/okhttp3/Call.kt | 26 +- .../src/jvmMain/kotlin/okhttp3/Challenge.kt | 11 + okhttp/src/jvmMain/kotlin/okhttp3/Headers.kt | 80 +++ okhttp/src/jvmMain/kotlin/okhttp3/HttpUrl.kt | 530 ++++++++++++++++++ .../src/jvmMain/kotlin/okhttp3/MediaType.kt | 27 + okhttp/src/jvmMain/kotlin/okhttp3/Request.kt | 49 ++ .../src/jvmMain/kotlin/okhttp3/RequestBody.kt | 56 ++ okhttp/src/jvmMain/kotlin/okhttp3/Response.kt | 82 +++ .../jvmMain/kotlin/okhttp3/ResponseBody.kt | 113 ++++ 10 files changed, 1026 insertions(+), 1 deletion(-) diff --git a/okhttp/src/jvmMain/kotlin/okhttp3/CacheControl.kt b/okhttp/src/jvmMain/kotlin/okhttp3/CacheControl.kt index bbd45b5e5..e7a0547e4 100644 --- a/okhttp/src/jvmMain/kotlin/okhttp3/CacheControl.kt +++ b/okhttp/src/jvmMain/kotlin/okhttp3/CacheControl.kt @@ -32,13 +32,32 @@ import okhttp3.internal.commonOnlyIfCached import okhttp3.internal.commonParse import okhttp3.internal.commonToString +/** + * A Cache-Control header with cache directives from a server or client. These directives set policy + * on what responses can be stored, and which requests can be satisfied by those stored responses. + * + * See [RFC 7234, 5.2](https://tools.ietf.org/html/rfc7234#section-5.2). + */ actual class CacheControl internal actual constructor( + /** + * In a response, this field's name "no-cache" is misleading. It doesn't prevent us from caching + * the response; it only means we have to validate the response with the origin server before + * returning it. We can do this with a conditional GET. + * + * In a request, it means do not use a cache to satisfy the request. + */ @get:JvmName("noCache") actual val noCache: Boolean, + /** If true, this response should not be cached. */ @get:JvmName("noStore") actual val noStore: Boolean, + /** The duration past the response's served date that it can be served without validation. */ @get:JvmName("maxAgeSeconds") actual val maxAgeSeconds: Int, + /** + * The "s-maxage" directive is the max age for shared caches. Not to be confused with "max-age" + * for non-shared caches, As in Firefox and Chrome, this directive is not honored by this cache. + */ @get:JvmName("sMaxAgeSeconds") actual val sMaxAgeSeconds: Int, actual val isPrivate: Boolean, @@ -50,6 +69,12 @@ actual class CacheControl internal actual constructor( @get:JvmName("minFreshSeconds") actual val minFreshSeconds: Int, + /** + * This field's name "only-if-cached" is misleading. It actually means "do not use the network". + * It is set by a client who only wants to make a request if it can be fully satisfied by the + * cache. Cached responses that would require validation (ie. conditional gets) are not permitted + * if this header is set. + */ @get:JvmName("onlyIfCached") actual val onlyIfCached: Boolean, @get:JvmName("noTransform") actual val noTransform: Boolean, @@ -130,6 +155,7 @@ actual class CacheControl internal actual constructor( actual override fun toString(): String = commonToString() + /** Builds a `Cache-Control` request header. */ actual class Builder { internal actual var noCache: Boolean = false internal actual var noStore: Boolean = false @@ -140,16 +166,30 @@ actual class CacheControl internal actual constructor( internal actual var noTransform: Boolean = false internal actual var immutable: Boolean = false + /** Don't accept an unvalidated cached response. */ actual fun noCache() = commonNoCache() + /** Don't store the server's response in any cache. */ actual fun noStore() = commonNoStore() + /** + * Only accept the response if it is in the cache. If the response isn't cached, a `504 + * Unsatisfiable Request` response will be returned. + */ actual fun onlyIfCached() = commonOnlyIfCached() + /** Don't accept a transformed response. */ actual fun noTransform() = commonNoTransform() actual fun immutable() = commonImmutable() + /** + * Sets the maximum age of a cached response. If the cache response's age exceeds [maxAge], it + * will not be used and a network request will be made. + * + * @param maxAge a non-negative integer. This is stored and transmitted with [TimeUnit.SECONDS] + * precision; finer precision will be lost. + */ actual fun maxAge(maxAge: Int, timeUnit: DurationUnit) = commonMaxAge(maxAge, timeUnit) actual fun maxStale(maxStale: Int, timeUnit: DurationUnit) = commonMaxStale(maxStale, timeUnit) @@ -200,12 +240,25 @@ actual class CacheControl internal actual constructor( } actual companion object { + /** + * Cache control request directives that require network validation of responses. Note that such + * requests may be assisted by the cache via conditional GET requests. + */ @JvmField actual val FORCE_NETWORK = commonForceNetwork() + /** + * Cache control request directives that uses the cache only, even if the cached response is + * stale. If the response isn't available in the cache or requires server validation, the call + * will fail with a `504 Unsatisfiable Request`. + */ @JvmField actual val FORCE_CACHE = commonForceCache() + /** + * Returns the cache directives of [headers]. This honors both Cache-Control and Pragma headers + * if they are present. + */ @JvmStatic actual fun parse(headers: Headers): CacheControl = commonParse(headers) } diff --git a/okhttp/src/jvmMain/kotlin/okhttp3/Call.kt b/okhttp/src/jvmMain/kotlin/okhttp3/Call.kt index 0070bc486..d3953bdc0 100644 --- a/okhttp/src/jvmMain/kotlin/okhttp3/Call.kt +++ b/okhttp/src/jvmMain/kotlin/okhttp3/Call.kt @@ -12,14 +12,18 @@ * 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 okio.IOException import okio.Timeout +/** + * A call is a request that has been prepared for execution. A call can be canceled. As this object + * represents a single request/response pair (stream), it cannot be executed twice. + */ actual interface Call : Cloneable { + /** Returns the original request that initiated this call. */ actual fun request(): Request /** @@ -50,10 +54,26 @@ actual interface Call : Cloneable { @Throws(IOException::class) fun execute(): Response + /** + * Schedules the request to be executed at some point in the future. + * + * The [dispatcher][OkHttpClient.dispatcher] defines when the request will run: usually + * immediately unless there are several other requests currently being executed. + * + * This client will later call back `responseCallback` with either an HTTP response or a failure + * exception. + * + * @throws IllegalStateException when the call has already been executed. + */ actual fun enqueue(responseCallback: Callback) + /** Cancels the request, if possible. Requests that are already complete cannot be canceled. */ actual fun cancel() + /** + * Returns true if this call has been either [executed][execute] or [enqueued][enqueue]. It is an + * error to execute a call more than once. + */ actual fun isExecuted(): Boolean actual fun isCanceled(): Boolean @@ -67,6 +87,10 @@ actual interface Call : Cloneable { */ fun timeout(): Timeout + /** + * Create a new, identical call to this one which can be enqueued or executed even if this call + * has already been. + */ public actual override fun clone(): Call actual fun interface Factory { diff --git a/okhttp/src/jvmMain/kotlin/okhttp3/Challenge.kt b/okhttp/src/jvmMain/kotlin/okhttp3/Challenge.kt index d2d9593af..eca4372ba 100644 --- a/okhttp/src/jvmMain/kotlin/okhttp3/Challenge.kt +++ b/okhttp/src/jvmMain/kotlin/okhttp3/Challenge.kt @@ -24,13 +24,24 @@ import okhttp3.internal.commonEquals import okhttp3.internal.commonHashCode import okhttp3.internal.commonToString +/** + * An [RFC 7235][rfc_7235] challenge. + * + * [rfc_7235]: https://tools.ietf.org/html/rfc7235 + */ actual class Challenge actual constructor( + /** Returns the authentication scheme, like `Basic`. */ @get:JvmName("scheme") actual val scheme: String, authParams: Map ) { + /** + * Returns the auth params, including [realm] and [charset] if present, but as + * strings. The map's keys are lowercase and should be treated case-insensitively. + */ @get:JvmName("authParams") actual val authParams: Map + /** Returns the protection space. */ @get:JvmName("realm") actual val realm: String? get() = authParams["realm"] diff --git a/okhttp/src/jvmMain/kotlin/okhttp3/Headers.kt b/okhttp/src/jvmMain/kotlin/okhttp3/Headers.kt index 187d7fcdf..71ba54992 100644 --- a/okhttp/src/jvmMain/kotlin/okhttp3/Headers.kt +++ b/okhttp/src/jvmMain/kotlin/okhttp3/Headers.kt @@ -46,10 +46,27 @@ import okhttp3.internal.http.toHttpDateOrNull import okhttp3.internal.http.toHttpDateString 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") actual class Headers internal actual constructor( internal actual val namesAndValues: Array ) : Iterable> { + /** Returns the last value corresponding to the specified field, or null. */ actual operator fun get(name: String): String? = commonHeadersGet(namesAndValues, name) /** @@ -67,6 +84,7 @@ actual class Headers internal actual constructor( return getDate(name)?.toInstant() } + /** Returns the number of field values. */ @get:JvmName("size") actual val size: Int get() = namesAndValues.size / 2 @@ -77,10 +95,13 @@ actual class Headers internal actual constructor( level = DeprecationLevel.ERROR) fun size(): Int = size + /** Returns the field at `position`. */ actual fun name(index: Int): String = commonName(index) + /** Returns the value at `index`. */ actual fun value(index: Int): String = commonValue(index) + /** Returns an immutable case-insensitive set of header names. */ actual fun names(): Set { val result = TreeSet(String.CASE_INSENSITIVE_ORDER) for (i in 0 until size) { @@ -89,6 +110,7 @@ actual class Headers internal actual constructor( return Collections.unmodifiableSet(result) } + /** Returns an immutable list of the header values for `name`. */ actual fun values(name: String): List = commonValues(name) /** @@ -112,10 +134,57 @@ actual class Headers internal actual constructor( 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. + */ actual override fun equals(other: Any?): Boolean = commonEquals(other) 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` + */ actual override fun toString(): String = commonToString() fun toMultimap(): Map> { @@ -164,6 +233,9 @@ actual class Headers internal actual 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. + */ actual fun add(name: String, value: String) = commonAdd(name, value) /** @@ -175,6 +247,9 @@ actual class Headers internal actual constructor( addLenient(name, value) } + /** + * Adds all headers from an existing collection. + */ actual fun addAll(headers: Headers) = commonAddAll(headers) /** @@ -224,6 +299,10 @@ actual class Headers internal actual constructor( } actual 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. + */ @JvmStatic @JvmName("of") actual fun headersOf(vararg namesAndValues: String): Headers = commonHeadersOf(*namesAndValues) @@ -237,6 +316,7 @@ actual class Headers internal actual constructor( return headersOf(*namesAndValues) } + /** Returns headers for the header names and values in the [Map]. */ @JvmStatic @JvmName("of") actual fun Map.toHeaders(): Headers = commonToHeaders() diff --git a/okhttp/src/jvmMain/kotlin/okhttp3/HttpUrl.kt b/okhttp/src/jvmMain/kotlin/okhttp3/HttpUrl.kt index 34de98f44..7fef4e460 100644 --- a/okhttp/src/jvmMain/kotlin/okhttp3/HttpUrl.kt +++ b/okhttp/src/jvmMain/kotlin/okhttp3/HttpUrl.kt @@ -70,21 +70,347 @@ import okhttp3.internal.HttpUrlCommon.canonicalize import okhttp3.internal.canParseAsIpAddress import okhttp3.internal.publicsuffix.PublicSuffixDatabase +/** + * A uniform resource locator (URL) with a scheme of either `http` or `https`. Use this class to + * compose and decompose Internet addresses. For example, this code will compose and print a URL for + * Google search: + * + * ```java + * HttpUrl url = new HttpUrl.Builder() + * .scheme("https") + * .host("www.google.com") + * .addPathSegment("search") + * .addQueryParameter("q", "polar bears") + * .build(); + * System.out.println(url); + * ``` + * + * which prints: + * + * ``` + * https://www.google.com/search?q=polar%20bears + * ``` + * + * As another example, this code prints the human-readable query parameters of a Twitter search: + * + * ```java + * HttpUrl url = HttpUrl.parse("https://twitter.com/search?q=cute%20%23puppies&f=images"); + * for (int i = 0, size = url.querySize(); i < size; i++) { + * System.out.println(url.queryParameterName(i) + ": " + url.queryParameterValue(i)); + * } + * ``` + * + * which prints: + * + * ``` + * q: cute #puppies + * f: images + * ``` + * + * In addition to composing URLs from their component parts and decomposing URLs into their + * component parts, this class implements relative URL resolution: what address you'd reach by + * clicking a relative link on a specified page. For example: + * + * ```java + * HttpUrl base = HttpUrl.parse("https://www.youtube.com/user/WatchTheDaily/videos"); + * HttpUrl link = base.resolve("../../watch?v=cbP2N1BQdYc"); + * System.out.println(link); + * ``` + * + * which prints: + * + * ``` + * https://www.youtube.com/watch?v=cbP2N1BQdYc + * ``` + * + * ## What's in a URL? + * + * A URL has several components. + * + * ### Scheme + * + * Sometimes referred to as *protocol*, A URL's scheme describes what mechanism should be used to + * retrieve the resource. Although URLs have many schemes (`mailto`, `file`, `ftp`), this class only + * supports `http` and `https`. Use [java.net.URI][URI] for URLs with arbitrary schemes. + * + * ### Username and Password + * + * Username and password are either present, or the empty string `""` if absent. This class offers + * no mechanism to differentiate empty from absent. Neither of these components are popular in + * practice. Typically HTTP applications use other mechanisms for user identification and + * authentication. + * + * ### Host + * + * The host identifies the webserver that serves the URL's resource. It is either a hostname like + * `square.com` or `localhost`, an IPv4 address like `192.168.0.1`, or an IPv6 address like `::1`. + * + * Usually a webserver is reachable with multiple identifiers: its IP addresses, registered + * domain names, and even `localhost` when connecting from the server itself. Each of a web server's + * names is a distinct URL and they are not interchangeable. For example, even if + * `http://square.github.io/dagger` and `http://google.github.io/dagger` are served by the same IP + * address, the two URLs identify different resources. + * + * ### Port + * + * The port used to connect to the web server. By default this is 80 for HTTP and 443 for HTTPS. + * This class never returns -1 for the port: if no port is explicitly specified in the URL then the + * scheme's default is used. + * + * ### Path + * + * The path identifies a specific resource on the host. Paths have a hierarchical structure like + * "/square/okhttp/issues/1486" and decompose into a list of segments like `["square", "okhttp", + * "issues", "1486"]`. + * + * This class offers methods to compose and decompose paths by segment. It composes each path + * from a list of segments by alternating between "/" and the encoded segment. For example the + * segments `["a", "b"]` build "/a/b" and the segments `["a", "b", ""]` build "/a/b/". + * + * If a path's last segment is the empty string then the path ends with "/". This class always + * builds non-empty paths: if the path is omitted it defaults to "/". The default path's segment + * list is a single empty string: `[""]`. + * + * ### Query + * + * The query is optional: it can be null, empty, or non-empty. For many HTTP URLs the query string + * is subdivided into a collection of name-value parameters. This class offers methods to set the + * query as the single string, or as individual name-value parameters. With name-value parameters + * the values are optional and names may be repeated. + * + * ### Fragment + * + * The fragment is optional: it can be null, empty, or non-empty. Unlike host, port, path, and + * query the fragment is not sent to the webserver: it's private to the client. + * + * ## Encoding + * + * Each component must be encoded before it is embedded in the complete URL. As we saw above, the + * string `cute #puppies` is encoded as `cute%20%23puppies` when used as a query parameter value. + * + * ### Percent encoding + * + * Percent encoding replaces a character (like `\ud83c\udf69`) with its UTF-8 hex bytes (like + * `%F0%9F%8D%A9`). This approach works for whitespace characters, control characters, non-ASCII + * characters, and characters that already have another meaning in a particular context. + * + * Percent encoding is used in every URL component except for the hostname. But the set of + * characters that need to be encoded is different for each component. For example, the path + * component must escape all of its `?` characters, otherwise it could be interpreted as the + * start of the URL's query. But within the query and fragment components, the `?` character + * doesn't delimit anything and doesn't need to be escaped. + * + * ```java + * HttpUrl url = HttpUrl.parse("http://who-let-the-dogs.out").newBuilder() + * .addPathSegment("_Who?_") + * .query("_Who?_") + * .fragment("_Who?_") + * .build(); + * System.out.println(url); + * ``` + * + * This prints: + * + * ``` + * http://who-let-the-dogs.out/_Who%3F_?_Who?_#_Who?_ + * ``` + * + * When parsing URLs that lack percent encoding where it is required, this class will percent encode + * the offending characters. + * + * ### IDNA Mapping and Punycode encoding + * + * Hostnames have different requirements and use a different encoding scheme. It consists of IDNA + * mapping and Punycode encoding. + * + * In order to avoid confusion and discourage phishing attacks, [IDNA Mapping][idna] transforms + * names to avoid confusing characters. This includes basic case folding: transforming shouting + * `SQUARE.COM` into cool and casual `square.com`. It also handles more exotic characters. For + * example, the Unicode trademark sign (™) could be confused for the letters "TM" in + * `http://ho™ail.com`. To mitigate this, the single character (™) maps to the string (tm). There + * is similar policy for all of the 1.1 million Unicode code points. Note that some code points such + * as "\ud83c\udf69" are not mapped and cannot be used in a hostname. + * + * [Punycode](http://ietf.org/rfc/rfc3492.txt) converts a Unicode string to an ASCII string to make + * international domain names work everywhere. For example, "σ" encodes as "xn--4xa". The encoded + * string is not human readable, but can be used with classes like [InetAddress] to establish + * connections. + * + * ## Why another URL model? + * + * Java includes both [java.net.URL][URL] and [java.net.URI][URI]. We offer a new URL + * model to address problems that the others don't. + * + * ### Different URLs should be different + * + * Although they have different content, `java.net.URL` considers the following two URLs + * equal, and the [equals()][Object.equals] method between them returns true: + * + * * https://example.net/ + * + * * https://example.com/ + * + * This is because those two hosts share the same IP address. This is an old, bad design decision + * that makes `java.net.URL` unusable for many things. It shouldn't be used as a [Map] key or in a + * [Set]. Doing so is both inefficient because equality may require a DNS lookup, and incorrect + * because unequal URLs may be equal because of how they are hosted. + * + * ### Equal URLs should be equal + * + * These two URLs are semantically identical, but `java.net.URI` disagrees: + * + * * http://host:80/ + * + * * http://host + * + * Both the unnecessary port specification (`:80`) and the absent trailing slash (`/`) cause URI to + * bucket the two URLs separately. This harms URI's usefulness in collections. Any application that + * stores information-per-URL will need to either canonicalize manually, or suffer unnecessary + * redundancy for such URLs. + * + * Because they don't attempt canonical form, these classes are surprisingly difficult to use + * securely. Suppose you're building a webservice that checks that incoming paths are prefixed + * "/static/images/" before serving the corresponding assets from the filesystem. + * + * ```java + * String attack = "http://example.com/static/images/../../../../../etc/passwd"; + * System.out.println(new URL(attack).getPath()); + * System.out.println(new URI(attack).getPath()); + * System.out.println(HttpUrl.parse(attack).encodedPath()); + * ``` + * + * By canonicalizing the input paths, they are complicit in directory traversal attacks. Code that + * checks only the path prefix may suffer! + * + * ``` + * /static/images/../../../../../etc/passwd + * /static/images/../../../../../etc/passwd + * /etc/passwd + * ``` + * + * ### If it works on the web, it should work in your application + * + * The `java.net.URI` class is strict around what URLs it accepts. It rejects URLs like + * `http://example.com/abc|def` because the `|` character is unsupported. This class is more + * forgiving: it will automatically percent-encode the `|'` yielding `http://example.com/abc%7Cdef`. + * This kind behavior is consistent with web browsers. `HttpUrl` prefers consistency with major web + * browsers over consistency with obsolete specifications. + * + * ### Paths and Queries should decompose + * + * Neither of the built-in URL models offer direct access to path segments or query parameters. + * Manually using `StringBuilder` to assemble these components is cumbersome: do '+' characters get + * silently replaced with spaces? If a query parameter contains a '&', does that get escaped? + * By offering methods to read and write individual query parameters directly, application + * developers are saved from the hassles of encoding and decoding. + * + * ### Plus a modern API + * + * The URL (JDK1.0) and URI (Java 1.4) classes predate builders and instead use telescoping + * constructors. For example, there's no API to compose a URI with a custom port without also + * providing a query and fragment. + * + * Instances of [HttpUrl] are well-formed and always have a scheme, host, and path. With + * `java.net.URL` it's possible to create an awkward URL like `http:/` with scheme and path but no + * hostname. Building APIs that consume such malformed values is difficult! + * + * This class has a modern API. It avoids punitive checked exceptions: [toHttpUrl] throws + * [IllegalArgumentException] on invalid input or [toHttpUrlOrNull] returns null if the input is an + * invalid URL. You can even be explicit about whether each component has been encoded already. + * + * [idna]: http://www.unicode.org/reports/tr46/#ToASCII + */ actual class HttpUrl internal actual constructor( + /** Either "http" or "https". */ @get:JvmName("scheme") actual val scheme: String, + /** + * The decoded username, or an empty string if none is present. + * + * | URL | `username()` | + * | :------------------------------- | :----------- | + * | `http://host/` | `""` | + * | `http://username@host/` | `"username"` | + * | `http://username:password@host/` | `"username"` | + * | `http://a%20b:c%20d@host/` | `"a b"` | + */ @get:JvmName("username") actual val username: String, + /** + * Returns the decoded password, or an empty string if none is present. + * + * | URL | `password()` | + * | :------------------------------- | :----------- | + * | `http://host/` | `""` | + * | `http://username@host/` | `""` | + * | `http://username:password@host/` | `"password"` | + * | `http://a%20b:c%20d@host/` | `"c d"` | + */ @get:JvmName("password") actual val password: String, + /** + * The host address suitable for use with [InetAddress.getAllByName]. May be: + * + * * A regular host name, like `android.com`. + * + * * An IPv4 address, like `127.0.0.1`. + * + * * An IPv6 address, like `::1`. Note that there are no square braces. + * + * * An encoded IDN, like `xn--n3h.net`. + * + * | URL | `host()` | + * | :-------------------- | :-------------- | + * | `http://android.com/` | `"android.com"` | + * | `http://127.0.0.1/` | `"127.0.0.1"` | + * | `http://[::1]/` | `"::1"` | + * | `http://xn--n3h.net/` | `"xn--n3h.net"` | + */ @get:JvmName("host") actual val host: String, + /** + * The explicitly-specified port if one was provided, or the default port for this URL's scheme. + * For example, this returns 8443 for `https://square.com:8443/` and 443 for + * `https://square.com/`. The result is in `[1..65535]`. + * + * | URL | `port()` | + * | :------------------ | :------- | + * | `http://host/` | `80` | + * | `http://host:8000/` | `8000` | + * | `https://host/` | `443` | + */ @get:JvmName("port") actual val port: Int, + /** + * A list of path segments like `["a", "b", "c"]` for the URL `http://host/a/b/c`. This list is + * never empty though it may contain a single empty string. + * + * | URL | `pathSegments()` | + * | :----------------------- | :------------------ | + * | `http://host/` | `[""]` | + * | `http://host/a/b/c"` | `["a", "b", "c"]` | + * | `http://host/a/b%20c/d"` | `["a", "b c", "d"]` | + */ @get:JvmName("pathSegments") actual val pathSegments: List, + /** + * Alternating, decoded query names and values, or null for no query. Names may be empty or + * non-empty, but never null. Values are null if the name has no corresponding '=' separator, or + * empty, or non-empty. + */ internal actual val queryNamesAndValues: List?, + /** + * This URL's fragment, like `"abc"` for `http://host/#abc`. This is null if the URL has no + * fragment. + * + * | URL | `fragment()` | + * | :--------------------- | :----------- | + * | `http://host/` | null | + * | `http://host/#` | `""` | + * | `http://host/#abc` | `"abc"` | + * | `http://host/#abc|def` | `"abc|def"` | + */ @get:JvmName("fragment") actual val fragment: String?, /** Canonical URL. */ @@ -130,52 +456,230 @@ actual class HttpUrl internal actual constructor( } } + /** + * The username, or an empty string if none is set. + * + * | URL | `encodedUsername()` | + * | :------------------------------- | :------------------ | + * | `http://host/` | `""` | + * | `http://username@host/` | `"username"` | + * | `http://username:password@host/` | `"username"` | + * | `http://a%20b:c%20d@host/` | `"a%20b"` | + */ @get:JvmName("encodedUsername") actual val encodedUsername: String get() = commonEncodedUsername + /** + * The password, or an empty string if none is set. + * + * | URL | `encodedPassword()` | + * | :--------------------------------| :------------------ | + * | `http://host/` | `""` | + * | `http://username@host/` | `""` | + * | `http://username:password@host/` | `"password"` | + * | `http://a%20b:c%20d@host/` | `"c%20d"` | + */ @get:JvmName("encodedPassword") actual val encodedPassword: String get() = commonEncodedPassword + /** + * The number of segments in this URL's path. This is also the number of slashes in this URL's + * path, like 3 in `http://host/a/b/c`. This is always at least 1. + * + * | URL | `pathSize()` | + * | :------------------- | :----------- | + * | `http://host/` | `1` | + * | `http://host/a/b/c` | `3` | + * | `http://host/a/b/c/` | `4` | + */ @get:JvmName("pathSize") actual val pathSize: Int get() = commonPathSize + /** + * The entire path of this URL encoded for use in HTTP resource resolution. The returned path will + * start with `"/"`. + * + * | URL | `encodedPath()` | + * | :---------------------- | :-------------- | + * | `http://host/` | `"/"` | + * | `http://host/a/b/c` | `"/a/b/c"` | + * | `http://host/a/b%20c/d` | `"/a/b%20c/d"` | + */ @get:JvmName("encodedPath") actual val encodedPath: String get() = commonEncodedPath + /** + * A list of encoded path segments like `["a", "b", "c"]` for the URL `http://host/a/b/c`. This + * list is never empty though it may contain a single empty string. + * + * | URL | `encodedPathSegments()` | + * | :---------------------- | :---------------------- | + * | `http://host/` | `[""]` | + * | `http://host/a/b/c` | `["a", "b", "c"]` | + * | `http://host/a/b%20c/d` | `["a", "b%20c", "d"]` | + */ @get:JvmName("encodedPathSegments") actual val encodedPathSegments: List get() = commonEncodedPathSegments + /** + * The query of this URL, encoded for use in HTTP resource resolution. This string may be null + * (for URLs with no query), empty (for URLs with an empty query) or non-empty (all other URLs). + * + * | URL | `encodedQuery()` | + * | :-------------------------------- | :--------------------- | + * | `http://host/` | null | + * | `http://host/?` | `""` | + * | `http://host/?a=apple&k=key+lime` | `"a=apple&k=key+lime"` | + * | `http://host/?a=apple&a=apricot` | `"a=apple&a=apricot"` | + * | `http://host/?a=apple&b` | `"a=apple&b"` | + */ @get:JvmName("encodedQuery") actual val encodedQuery: String? get() = commonEncodedQuery + /** + * This URL's query, like `"abc"` for `http://host/?abc`. Most callers should prefer + * [queryParameterName] and [queryParameterValue] because these methods offer direct access to + * individual query parameters. + * + * | URL | `query()` | + * | :-------------------------------- | :--------------------- | + * | `http://host/` | null | + * | `http://host/?` | `""` | + * | `http://host/?a=apple&k=key+lime` | `"a=apple&k=key lime"` | + * | `http://host/?a=apple&a=apricot` | `"a=apple&a=apricot"` | + * | `http://host/?a=apple&b` | `"a=apple&b"` | + */ @get:JvmName("query") actual val query: String? get() = commonQuery + /** + * The number of query parameters in this URL, like 2 for `http://host/?a=apple&b=banana`. If this + * URL has no query this is 0. Otherwise it is one more than the number of `"&"` separators in the + * query. + * + * | URL | `querySize()` | + * | :-------------------------------- | :------------ | + * | `http://host/` | `0` | + * | `http://host/?` | `1` | + * | `http://host/?a=apple&k=key+lime` | `2` | + * | `http://host/?a=apple&a=apricot` | `2` | + * | `http://host/?a=apple&b` | `2` | + */ @get:JvmName("querySize") actual val querySize: Int get() = commonQuerySize + /** + * The first query parameter named `name` decoded using UTF-8, or null if there is no such query + * parameter. + * + * | URL | `queryParameter("a")` | + * | :-------------------------------- | :-------------------- | + * | `http://host/` | null | + * | `http://host/?` | null | + * | `http://host/?a=apple&k=key+lime` | `"apple"` | + * | `http://host/?a=apple&a=apricot` | `"apple"` | + * | `http://host/?a=apple&b` | `"apple"` | + */ actual fun queryParameter(name: String): String? = commonQueryParameter(name) + /** + * The distinct query parameter names in this URL, like `["a", "b"]` for + * `http://host/?a=apple&b=banana`. If this URL has no query this is the empty set. + * + * | URL | `queryParameterNames()` | + * | :-------------------------------- | :---------------------- | + * | `http://host/` | `[]` | + * | `http://host/?` | `[""]` | + * | `http://host/?a=apple&k=key+lime` | `["a", "k"]` | + * | `http://host/?a=apple&a=apricot` | `["a"]` | + * | `http://host/?a=apple&b` | `["a", "b"]` | + */ actual @get:JvmName("queryParameterNames") val queryParameterNames: Set get() = commonQueryParameterNames + /** + * Returns all values for the query parameter `name` ordered by their appearance in this + * URL. For example this returns `["banana"]` for `queryParameterValue("b")` on + * `http://host/?a=apple&b=banana`. + * + * | URL | `queryParameterValues("a")` | `queryParameterValues("b")` | + * | :-------------------------------- | :-------------------------- | :-------------------------- | + * | `http://host/` | `[]` | `[]` | + * | `http://host/?` | `[]` | `[]` | + * | `http://host/?a=apple&k=key+lime` | `["apple"]` | `[]` | + * | `http://host/?a=apple&a=apricot` | `["apple", "apricot"]` | `[]` | + * | `http://host/?a=apple&b` | `["apple"]` | `[null]` | + */ actual fun queryParameterValues(name: String): List = commonQueryParameterValues(name) + /** + * Returns the name of the query parameter at `index`. For example this returns `"a"` + * for `queryParameterName(0)` on `http://host/?a=apple&b=banana`. This throws if + * `index` is not less than the [query size][querySize]. + * + * | URL | `queryParameterName(0)` | `queryParameterName(1)` | + * | :-------------------------------- | :---------------------- | :---------------------- | + * | `http://host/` | exception | exception | + * | `http://host/?` | `""` | exception | + * | `http://host/?a=apple&k=key+lime` | `"a"` | `"k"` | + * | `http://host/?a=apple&a=apricot` | `"a"` | `"a"` | + * | `http://host/?a=apple&b` | `"a"` | `"b"` | + */ actual fun queryParameterName(index: Int): String = commonQueryParameterName(index) + /** + * Returns the value of the query parameter at `index`. For example this returns `"apple"` for + * `queryParameterName(0)` on `http://host/?a=apple&b=banana`. This throws if `index` is not less + * than the [query size][querySize]. + * + * | URL | `queryParameterValue(0)` | `queryParameterValue(1)` | + * | :-------------------------------- | :----------------------- | :----------------------- | + * | `http://host/` | exception | exception | + * | `http://host/?` | null | exception | + * | `http://host/?a=apple&k=key+lime` | `"apple"` | `"key lime"` | + * | `http://host/?a=apple&a=apricot` | `"apple"` | `"apricot"` | + * | `http://host/?a=apple&b` | `"apple"` | null | + */ actual fun queryParameterValue(index: Int): String? = commonQueryParameterValue(index) + /** + * This URL's encoded fragment, like `"abc"` for `http://host/#abc`. This is null if the URL has + * no fragment. + * + * | URL | `encodedFragment()` | + * | :--------------------- | :------------------ | + * | `http://host/` | null | + * | `http://host/#` | `""` | + * | `http://host/#abc` | `"abc"` | + * | `http://host/#abc|def` | `"abc|def"` | + */ @get:JvmName("encodedFragment") actual val encodedFragment: String? get() = commonEncodedFragment + /** + * Returns a string with containing this URL with its username, password, query, and fragment + * stripped, and its path replaced with `/...`. For example, redacting + * `http://username:password@example.com/path` returns `http://example.com/...`. + */ actual fun redact(): String = commonRedact() + /** + * Returns the URL that would be retrieved by following `link` from this URL, or null if the + * resulting URL is not well-formed. + */ actual fun resolve(link: String): HttpUrl? = commonResolve(link) + /** + * Returns a builder based on this URL. + */ actual fun newBuilder(): Builder = commonNewBuilder() + /** + * Returns a builder for the URL that would be retrieved by following `link` from this URL, + * or null if the resulting URL is not well-formed. + */ actual fun newBuilder(link: String): Builder? = commonNewBuilder(link) override fun equals(other: Any?): Boolean = commonEquals(other) @@ -352,6 +856,9 @@ actual class HttpUrl internal actual constructor( internal actual var encodedQueryNamesAndValues: MutableList? = null internal actual var encodedFragment: String? = null + /** + * @param scheme either "http" or "https". + */ actual fun scheme(scheme: String) = commonScheme(scheme) actual fun username(username: String) = commonUsername(username) @@ -362,16 +869,28 @@ actual class HttpUrl internal actual constructor( actual fun encodedPassword(encodedPassword: String) = commonEncodedPassword(encodedPassword) + /** + * @param host either a regular hostname, International Domain Name, IPv4 address, or IPv6 + * address. + */ actual fun host(host: String) = commonHost(host) actual fun port(port: Int) = commonPort(port) actual fun addPathSegment(pathSegment: String) = commonAddPathSegment(pathSegment) + /** + * Adds a set of path segments separated by a slash (either `\` or `/`). If `pathSegments` + * starts with a slash, the resulting URL will have empty path segment. + */ actual fun addPathSegments(pathSegments: String): Builder = commonAddPathSegments(pathSegments) actual fun addEncodedPathSegment(encodedPathSegment: String) = commonAddEncodedPathSegment(encodedPathSegment) + /** + * Adds a set of encoded path segments separated by a slash (either `\` or `/`). If + * `encodedPathSegments` starts with a slash, the resulting URL will have empty path segment. + */ actual fun addEncodedPathSegments(encodedPathSegments: String): Builder = commonAddEncodedPathSegments(encodedPathSegments) actual fun setPathSegment(index: Int, pathSegment: String) = commonSetPathSegment(index, pathSegment) @@ -386,8 +905,10 @@ actual class HttpUrl internal actual constructor( actual fun encodedQuery(encodedQuery: String?) = commonEncodedQuery(encodedQuery) + /** Encodes the query parameter using UTF-8 and adds it to this URL's query string. */ actual fun addQueryParameter(name: String, value: String?) = commonAddQueryParameter(name, value) + /** Adds the pre-encoded query parameter to this URL's query string. */ actual fun addEncodedQueryParameter(encodedName: String, encodedValue: String?) = commonAddEncodedQueryParameter(encodedName, encodedValue) actual fun setQueryParameter(name: String, value: String?) = commonSetQueryParameter(name, value) @@ -448,9 +969,18 @@ actual class HttpUrl internal actual constructor( @JvmStatic actual fun defaultPort(scheme: String): Int = commonDefaultPort(scheme) + /** + * Returns a new [HttpUrl] representing this. + * + * @throws IllegalArgumentException If this is not a well-formed HTTP or HTTPS URL. + */ @JvmStatic @JvmName("get") actual fun String.toHttpUrl(): HttpUrl = commonToHttpUrl() + /** + * Returns a new `HttpUrl` representing `url` if it is a well-formed HTTP or HTTPS URL, or null + * if it isn't. + */ @JvmStatic @JvmName("parse") actual fun String.toHttpUrlOrNull(): HttpUrl? = commonToHttpUrlOrNull() diff --git a/okhttp/src/jvmMain/kotlin/okhttp3/MediaType.kt b/okhttp/src/jvmMain/kotlin/okhttp3/MediaType.kt index 805c3f810..20bca3d15 100644 --- a/okhttp/src/jvmMain/kotlin/okhttp3/MediaType.kt +++ b/okhttp/src/jvmMain/kotlin/okhttp3/MediaType.kt @@ -23,13 +23,26 @@ import okhttp3.internal.commonToMediaType import okhttp3.internal.commonToMediaTypeOrNull import okhttp3.internal.commonToString +/** + * An [RFC 2045][rfc_2045] Media Type, appropriate to describe the content type of an HTTP request + * or response body. + * + * [rfc_2045]: http://tools.ietf.org/html/rfc2045 + */ actual class MediaType internal actual constructor( internal actual val mediaType: String, + /** + * Returns the high-level media type, such as "text", "image", "audio", "video", or "application". + */ @get:JvmName("type") actual val type: String, + /** + * Returns a specific media subtype, such as "plain" or "png", "mpeg", "mp4" or "xml". + */ @get:JvmName("subtype") actual val subtype: String, + /** Alternating parameter names with their values, like `["charset", "utf-8"]`. */ internal actual val parameterNamesAndValues: Array ) { @@ -47,6 +60,10 @@ actual class MediaType internal actual constructor( } } + /** + * Returns the parameter [name] of this media type, or null if this media type does not define + * such a parameter. + */ actual fun parameter(name: String): String? = commonParameter(name) @JvmName("-deprecated_type") @@ -65,6 +82,10 @@ actual class MediaType internal actual constructor( ) fun subtype(): String = subtype + /** + * Returns the encoded media type, like "text/plain; charset=utf-8", appropriate for use in a + * Content-Type header. + */ actual override fun toString(): String = commonToString() override fun equals(other: Any?): Boolean = commonEquals(other) @@ -72,10 +93,16 @@ actual class MediaType internal actual constructor( override fun hashCode(): Int = commonHashCode() actual companion object { + /** + * Returns a media type for this string. + * + * @throws IllegalArgumentException if this is not a well-formed media type. + */ @JvmStatic @JvmName("get") actual fun String.toMediaType(): MediaType = commonToMediaType() + /** Returns a media type for this, or null if this is not a well-formed media type. */ @JvmStatic @JvmName("parse") actual fun String.toMediaTypeOrNull(): MediaType? = commonToMediaTypeOrNull() diff --git a/okhttp/src/jvmMain/kotlin/okhttp3/Request.kt b/okhttp/src/jvmMain/kotlin/okhttp3/Request.kt index 88e219aff..355776851 100644 --- a/okhttp/src/jvmMain/kotlin/okhttp3/Request.kt +++ b/okhttp/src/jvmMain/kotlin/okhttp3/Request.kt @@ -35,6 +35,10 @@ import okhttp3.internal.commonRemoveHeader import okhttp3.internal.commonTag import okhttp3.internal.commonToString +/** + * An HTTP request. Instances of this class are immutable if their [body] is null or itself + * immutable. + */ actual class Request internal actual constructor(builder: Builder) { @get:JvmName("url") actual val url: HttpUrl = checkNotNull(builder.url) { "url == null" } @@ -85,9 +89,11 @@ actual class Request internal actual constructor(builder: Builder) { actual fun headers(name: String): List = commonHeaders(name) + /** Returns the tag attached with [T] as a key, or null if no tag is attached with that key. */ @JvmName("reifiedTag") actual inline fun tag(): T? = tag(T::class) + /** Returns the tag attached with [type] as a key, or null if no tag is attached with that key. */ actual fun tag(type: KClass): T? = type.java.cast(tags[type]) /** @@ -110,6 +116,10 @@ actual class Request internal actual constructor(builder: Builder) { actual fun newBuilder(): Builder = Builder(this) + /** + * Returns the cache control directives for this response. This is never null, even if this + * response contains no `Cache-Control` header. + */ @get:JvmName("cacheControl") actual val cacheControl: CacheControl get() { var result = lazyCacheControl @@ -162,6 +172,7 @@ actual class Request internal actual constructor(builder: Builder) { internal actual var method: String internal actual var headers: Headers.Builder internal actual var body: RequestBody? = null + /** A mutable map of tags, or an immutable empty map if we don't have any. */ internal actual var tags = mapOf, Any>() actual constructor() { @@ -184,6 +195,12 @@ actual class Request internal actual constructor(builder: Builder) { this.url = url } + /** + * Sets the URL target of this request. + * + * @throws IllegalArgumentException if [url] is not a valid HTTP or HTTPS URL. Avoid this + * exception by calling [HttpUrl.parse]; it returns null for invalid URLs. + */ actual open fun url(url: String): Builder { return url(canonicalUrl(url).toHttpUrl()) } @@ -195,14 +212,32 @@ actual class Request internal actual constructor(builder: Builder) { */ open fun url(url: URL) = url(url.toString().toHttpUrl()) + /** + * Sets the header named [name] to [value]. If this request already has any headers + * with that name, they are all replaced. + */ actual open fun header(name: String, value: String) = commonHeader(name, value) + /** + * Adds a header with [name] and [value]. Prefer this method for multiply-valued + * headers like "Cookie". + * + * Note that for some headers including `Content-Length` and `Content-Encoding`, + * OkHttp may replace [value] with a header derived from the request body. + */ actual open fun addHeader(name: String, value: String) = commonAddHeader(name, value) + /** Removes all headers named [name] on this builder. */ actual open fun removeHeader(name: String) = commonRemoveHeader(name) + /** Removes all headers on this builder and adds [headers]. */ actual open fun headers(headers: Headers) = commonHeaders(headers) + /** + * Sets this request's `Cache-Control` header, replacing any cache control headers already + * present. If [cacheControl] doesn't define any directives, this clears this request's + * cache-control headers. + */ actual open fun cacheControl(cacheControl: CacheControl): Builder = commonCacheControl(cacheControl) actual open fun get(): Builder = commonGet() @@ -220,9 +255,23 @@ actual class Request internal actual constructor(builder: Builder) { actual open fun method(method: String, body: RequestBody?): Builder = commonMethod(method, body) + /** + * Attaches [tag] to the request using [T] as a key. Tags can be read from a request using + * [Request.tag]. Use null to remove any existing tag assigned for [T]. + * + * Use this API to attach timing, debugging, or other application data to a request so that + * you may read it in interceptors, event listeners, or callbacks. + */ @JvmName("reifiedTag") actual inline fun tag(tag: T?): Builder = tag(T::class, tag) + /** + * Attaches [tag] to the request using [type] as a key. Tags can be read from a request using + * [Request.tag]. Use null to remove any existing tag assigned for [type]. + * + * Use this API to attach timing, debugging, or other application data to a request so that + * you may read it in interceptors, event listeners, or callbacks. + */ actual fun tag(type: KClass, tag: T?): Builder = commonTag(type, type.cast(tag)) /** Attaches [tag] to the request using `Object.class` as a key. */ diff --git a/okhttp/src/jvmMain/kotlin/okhttp3/RequestBody.kt b/okhttp/src/jvmMain/kotlin/okhttp3/RequestBody.kt index a397c4487..c2ceee2ed 100644 --- a/okhttp/src/jvmMain/kotlin/okhttp3/RequestBody.kt +++ b/okhttp/src/jvmMain/kotlin/okhttp3/RequestBody.kt @@ -34,16 +34,71 @@ import okio.source actual abstract class RequestBody { + /** Returns the Content-Type header for this body. */ actual abstract fun contentType(): MediaType? + /** + * Returns the number of bytes that will be written to sink in a call to [writeTo], + * or -1 if that count is unknown. + */ @Throws(IOException::class) actual open fun contentLength(): Long = commonContentLength() + /** Writes the content of this request to [sink]. */ @Throws(IOException::class) actual abstract fun writeTo(sink: BufferedSink) + /** + * A duplex request body is special in how it is **transmitted** on the network and + * in the **API contract** between OkHttp and the application. + * + * This method returns false unless it is overridden by a subclass. + * + * ### Duplex Transmission + * + * With regular HTTP calls the request always completes sending before the response may begin + * receiving. With duplex the request and response may be interleaved! That is, request body bytes + * may be sent after response headers or body bytes have been received. + * + * Though any call may be initiated as a duplex call, only web servers that are specially + * designed for this nonstandard interaction will use it. As of 2019-01, the only widely-used + * implementation of this pattern is [gRPC][grpc]. + * + * Because the encoding of interleaved data is not well-defined for HTTP/1, duplex request + * bodies may only be used with HTTP/2. Calls to HTTP/1 servers will fail before the HTTP request + * is transmitted. If you cannot ensure that your client and server both support HTTP/2, do not + * use this feature. + * + * ### Duplex APIs + * + * With regular request bodies it is not legal to write bytes to the sink passed to + * [RequestBody.writeTo] after that method returns. For duplex requests bodies that condition is + * lifted. Such writes occur on an application-provided thread and may occur concurrently with + * reads of the [ResponseBody]. For duplex request bodies, [writeTo] should return + * quickly, possibly by handing off the provided request body to another thread to perform + * writing. + * + * [grpc]: https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md + */ actual open fun isDuplex(): Boolean = commonIsDuplex() + /** + * Returns true if this body expects at most one call to [writeTo] and can be transmitted + * at most once. This is typically used when writing the request body is destructive and it is not + * possible to recreate the request body after it has been sent. + * + * This method returns false unless it is overridden by a subclass. + * + * By default OkHttp will attempt to retransmit request bodies when the original request fails + * due to any of: + * + * * A stale connection. The request was made on a reused connection and that reused connection + * has since been closed by the server. + * * A client timeout (HTTP 408). + * * A authorization challenge (HTTP 401 and 407) that is satisfied by the [Authenticator]. + * * A retryable server failure (HTTP 503 with a `Retry-After: 0` response header). + * * A misdirected request (HTTP 421) on a coalesced connection. + */ actual open fun isOneShot(): Boolean = commonIsOneShot() actual companion object { @@ -81,6 +136,7 @@ actual abstract class RequestBody { } } + /** Returns a new request body that transmits this. */ @JvmOverloads @JvmStatic @JvmName("create") diff --git a/okhttp/src/jvmMain/kotlin/okhttp3/Response.kt b/okhttp/src/jvmMain/kotlin/okhttp3/Response.kt index 2805a4068..d75fb2efb 100644 --- a/okhttp/src/jvmMain/kotlin/okhttp3/Response.kt +++ b/okhttp/src/jvmMain/kotlin/okhttp3/Response.kt @@ -44,13 +44,36 @@ import okhttp3.internal.connection.Exchange import okhttp3.internal.http.parseChallenges import okio.Buffer +/** + * An HTTP response. Instances of this class are not immutable: the response body is a one-shot + * value that may be consumed only once and then closed. All other properties are immutable. + * + * This class implements [Closeable]. Closing it simply closes its response body. See + * [ResponseBody] for an explanation and examples. + */ actual class Response internal constructor( + /** + * The request that initiated this HTTP response. This is not necessarily the same request issued + * by the application: + * + * * It may be transformed by the user's interceptors. For example, an application interceptor + * may add headers like `User-Agent`. + * * It may be the request generated in response to an HTTP redirect or authentication + * challenge. In this case the request URL may be different than the initial request URL. + * + * Use the `request` of the [networkResponse] field to get the wire-level request that was + * transmitted. In the case of follow-ups and redirects, also look at the `request` of the + * [priorResponse] objects, which have its own [priorResponse]. + */ @get:JvmName("request") actual val request: Request, + /** Returns the HTTP protocol, such as [Protocol.HTTP_1_1] or [Protocol.HTTP_1_0]. */ @get:JvmName("protocol") actual val protocol: Protocol, + /** Returns the HTTP status message. */ @get:JvmName("message") actual val message: String, + /** Returns the HTTP status code. */ @get:JvmName("code") actual val code: Int, /** @@ -62,12 +85,36 @@ actual class Response internal constructor( /** Returns the HTTP headers. */ @get:JvmName("headers") actual val headers: Headers, + /** + * Returns a non-null value if this response was passed to [Callback.onResponse] or returned + * from [Call.execute]. Response bodies must be [closed][ResponseBody] and may + * be consumed only once. + * + * This always returns null on responses returned from [cacheResponse], [networkResponse], + * and [priorResponse]. + */ @get:JvmName("body") actual val body: ResponseBody, + /** + * Returns the raw response received from the network. Will be null if this response didn't use + * the network, such as when the response is fully cached. The body of the returned response + * should not be read. + */ @get:JvmName("networkResponse") actual val networkResponse: Response?, + /** + * Returns the raw response received from the cache. Will be null if this response didn't use + * the cache. For conditional get requests the cache response and network response may both be + * non-null. The body of the returned response should not be read. + */ @get:JvmName("cacheResponse") actual val cacheResponse: Response?, + /** + * Returns the response for the HTTP redirect or authorization challenge that triggered this + * response, or null if this response wasn't triggered by an automatic retry. The body of the + * returned response should not be read because it has already been consumed by the redirecting + * client. + */ @get:JvmName("priorResponse") actual val priorResponse: Response?, /** @@ -112,6 +159,10 @@ actual class Response internal constructor( level = DeprecationLevel.ERROR) fun code(): Int = code + /** + * Returns true if the code is in [200..300), which means the request was successfully received, + * understood, and accepted. + */ actual val isSuccessful: Boolean = commonIsSuccessful @JvmName("-deprecated_message") @@ -147,6 +198,17 @@ actual class Response internal constructor( @Throws(IOException::class) actual fun trailers(): Headers = trailersFn() + /** + * Peeks up to [byteCount] bytes from the response body and returns them as a new response + * body. If fewer than [byteCount] bytes are in the response body, the full response body is + * returned. If more than [byteCount] bytes are in the response body, the returned value + * will be truncated to [byteCount] bytes. + * + * It is an error to call this method after the body has been consumed. + * + * **Warning:** this method loads the requested bytes into memory. Most applications should set + * a modest limit on `byteCount`, such as 1 MiB. + */ @Throws(IOException::class) actual fun peekBody(byteCount: Long): ResponseBody { val peeked = body.source().peek() @@ -210,6 +272,10 @@ actual class Response internal constructor( ) } + /** + * Returns the cache control directives for this response. This is never null, even if this + * response contains no `Cache-Control` header. + */ @get:JvmName("cacheControl") actual val cacheControl: CacheControl get() = commonCacheControl @@ -234,6 +300,12 @@ actual class Response internal constructor( level = DeprecationLevel.ERROR) fun receivedResponseAtMillis(): Long = receivedResponseAtMillis + /** + * Closes the response body. Equivalent to `body().close()`. + * + * Prior to OkHttp 5.0, it was an error to close a response that is not eligible for a body. This + * includes the responses returned from [cacheResponse], [networkResponse], and [priorResponse]. + */ actual override fun close() = commonClose() actual override fun toString(): String = commonToString() @@ -287,12 +359,22 @@ actual class Response internal constructor( this.handshake = handshake } + /** + * Sets the header named [name] to [value]. If this request already has any headers + * with that name, they are all replaced. + */ actual open fun header(name: String, value: String) = commonHeader(name, value) + /** + * Adds a header with [name] to [value]. Prefer this method for multiply-valued + * headers like "Set-Cookie". + */ actual open fun addHeader(name: String, value: String) = commonAddHeader(name, value) + /** Removes all headers named [name] on this builder. */ actual open fun removeHeader(name: String) = commonRemoveHeader(name) + /** Removes all headers on this builder and adds [headers]. */ actual open fun headers(headers: Headers) = commonHeaders(headers) actual open fun body(body: ResponseBody) = commonBody(body) diff --git a/okhttp/src/jvmMain/kotlin/okhttp3/ResponseBody.kt b/okhttp/src/jvmMain/kotlin/okhttp3/ResponseBody.kt index e6aca3c75..cecf237a2 100644 --- a/okhttp/src/jvmMain/kotlin/okhttp3/ResponseBody.kt +++ b/okhttp/src/jvmMain/kotlin/okhttp3/ResponseBody.kt @@ -33,21 +33,107 @@ import okio.Buffer import okio.BufferedSource import okio.ByteString +/** + * A one-shot stream from the origin server to the client application with the raw bytes of the + * response body. Each response body is supported by an active connection to the webserver. This + * imposes both obligations and limits on the client application. + * + * ### The response body must be closed. + * + * Each response body is backed by a limited resource like a socket (live network responses) or + * an open file (for cached responses). Failing to close the response body will leak resources and + * may ultimately cause the application to slow down or crash. + * + * Both this class and [Response] implement [Closeable]. Closing a response simply + * closes its response body. If you invoke [Call.execute] or implement [Callback.onResponse] you + * must close this body by calling any of the following methods: + * + * * `Response.close()` + * * `Response.body().close()` + * * `Response.body().source().close()` + * * `Response.body().charStream().close()` + * * `Response.body().byteStream().close()` + * * `Response.body().bytes()` + * * `Response.body().string()` + * + * There is no benefit to invoking multiple `close()` methods for the same response body. + * + * For synchronous calls, the easiest way to make sure a response body is closed is with a `try` + * block. With this structure the compiler inserts an implicit `finally` clause that calls + * [close()][Response.close] for you. + * + * ```java + * Call call = client.newCall(request); + * try (Response response = call.execute()) { + * ... // Use the response. + * } + * ``` + * + * You can use a similar block for asynchronous calls: + * + * ```java + * Call call = client.newCall(request); + * call.enqueue(new Callback() { + * public void onResponse(Call call, Response response) throws IOException { + * try (ResponseBody responseBody = response.body()) { + * ... // Use the response. + * } + * } + * + * public void onFailure(Call call, IOException e) { + * ... // Handle the failure. + * } + * }); + * ``` + * + * These examples will not work if you're consuming the response body on another thread. In such + * cases the consuming thread must call [close] when it has finished reading the response + * body. + * + * ### The response body can be consumed only once. + * + * This class may be used to stream very large responses. For example, it is possible to use this + * class to read a response that is larger than the entire memory allocated to the current process. + * It can even stream a response larger than the total storage on the current device, which is a + * common requirement for video streaming applications. + * + * Because this class does not buffer the full response in memory, the application may not + * re-read the bytes of the response. Use this one shot to read the entire response into memory with + * [bytes] or [string]. Or stream the response with either [source], [byteStream], or [charStream]. + */ actual abstract class ResponseBody : Closeable { /** Multiple calls to [charStream] must return the same instance. */ private var reader: Reader? = null actual abstract fun contentType(): MediaType? + /** + * Returns the number of bytes in that will returned by [bytes], or [byteStream], or -1 if + * unknown. + */ actual abstract fun contentLength(): Long fun byteStream(): InputStream = source().inputStream() actual abstract fun source(): BufferedSource + /** + * Returns the response as a byte array. + * + * This method loads entire response body into memory. If the response body is very large this + * may trigger an [OutOfMemoryError]. Prefer to stream the response body if this is a + * possibility for your response. + */ @Throws(IOException::class) actual fun bytes() = commonBytes() + /** + * Returns the response as a [ByteString]. + * + * This method loads entire response body into memory. If the response body is very large this + * may trigger an [OutOfMemoryError]. Prefer to stream the response body if this is a + * possibility for your response. + */ @Throws(IOException::class) actual fun byteString() = commonByteString() @@ -67,6 +153,22 @@ actual abstract class ResponseBody : Closeable { reader = it } + /** + * Returns the response as a string. + * + * If the response starts with a + * [Byte Order Mark (BOM)](https://en.wikipedia.org/wiki/Byte_order_mark), it is consumed and + * used to determine the charset of the response bytes. + * + * Otherwise if the response has a `Content-Type` header that specifies a charset, that is used + * to determine the charset of the response bytes. + * + * Otherwise the response bytes are decoded as UTF-8. + * + * This method loads entire response body into memory. If the response body is very large this + * may trigger an [OutOfMemoryError]. Prefer to stream the response body if this is a + * possibility for your response. + */ @Throws(IOException::class) actual fun string(): String = source().use { source -> source.readString(charset = source.readBomAsCharset(charset())) @@ -104,6 +206,14 @@ actual abstract class ResponseBody : Closeable { } actual companion object { + /** + * Returns a new response body that transmits this string. If [contentType] is non-null and + * has a charset other than utf-8 the behaviour differs by platform. + * + * On the JVM the encoding will be used instead of utf-8. + * + * On non JVM platforms, this method will fail for encodings other than utf-8. + */ @JvmStatic @JvmName("create") actual fun String.toResponseBody(contentType: MediaType?): ResponseBody { @@ -112,14 +222,17 @@ actual abstract class ResponseBody : Closeable { return buffer.asResponseBody(finalContentType, buffer.size) } + /** Returns a new response body that transmits this byte array. */ @JvmStatic @JvmName("create") actual fun ByteArray.toResponseBody(contentType: MediaType?): ResponseBody = commonToResponseBody(contentType) + /** Returns a new response body that transmits this byte string. */ @JvmStatic @JvmName("create") actual fun ByteString.toResponseBody(contentType: MediaType?): ResponseBody = commonToResponseBody(contentType) + /** Returns a new response body that transmits this source. */ @JvmStatic @JvmName("create") actual fun BufferedSource.asResponseBody(