1
0
mirror of https://github.com/square/okhttp.git synced 2026-01-25 16:01:38 +03:00

Merge pull request #394 from square/jwilson_0102_headers_apis

Another round of header APIs cleanup.
This commit is contained in:
Jesse Wilson
2014-01-03 14:35:41 -08:00
18 changed files with 301 additions and 353 deletions

View File

@@ -38,9 +38,9 @@ import static java.net.HttpURLConnection.HTTP_OK;
import static java.net.HttpURLConnection.HTTP_PROXY_AUTH;
/**
* Holds the sockets and streams of an HTTP, HTTPS, or HTTPS+SPDY connection,
* which may be used for multiple HTTP request/response exchanges. Connections
* may be direct to the origin server or via a proxy.
* The sockets and streams of an HTTP, HTTPS, or HTTPS+SPDY connection. May be
* used for multiple HTTP request/response exchanges. Connections may be direct
* to the origin server or via a proxy.
*
* <p>Typically instances of this class are created, connected and exercised
* automatically by the HTTP client. Applications may use this class to monitor
@@ -53,10 +53,10 @@ import static java.net.HttpURLConnection.HTTP_PROXY_AUTH;
* There are tradeoffs when selecting which options to include when negotiating
* a secure connection to a remote host. Newer TLS options are quite useful:
* <ul>
* <li>Server Name Indication (SNI) enables one IP address to negotiate secure
* connections for multiple domain names.
* <li>Next Protocol Negotiation (NPN) enables the HTTPS port (443) to be used
* for both HTTP and SPDY transports.
* <li>Server Name Indication (SNI) enables one IP address to negotiate secure
* connections for multiple domain names.
* <li>Next Protocol Negotiation (NPN) enables the HTTPS port (443) to be used
* for both HTTP and SPDY transports.
* </ul>
* Unfortunately, older HTTPS servers refuse to connect when such options are
* presented. Rather than avoiding these options entirely, this class allows a
@@ -223,9 +223,7 @@ public final class Connection implements Closeable {
}
public void resetIdleStartTime() {
if (spdyConnection != null) {
throw new IllegalStateException("spdyConnection != null");
}
if (spdyConnection != null) throw new IllegalStateException("spdyConnection != null");
this.idleStartTimeNs = System.nanoTime();
}

View File

@@ -15,8 +15,6 @@
*/
package com.squareup.okhttp;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
@@ -55,31 +53,4 @@ final class Dispatcher {
List<Job> jobs = enqueuedJobs.get(job.tag());
if (jobs != null) jobs.remove(job);
}
static class RealResponseBody extends Response.Body {
private final Response response;
private final InputStream in;
RealResponseBody(Response response, InputStream in) {
this.response = response;
this.in = in;
}
@Override public boolean ready() throws IOException {
return true;
}
@Override public MediaType contentType() {
String contentType = response.getContentType();
return contentType != null ? MediaType.parse(contentType) : null;
}
@Override public long contentLength() {
return response.getContentLength();
}
@Override public InputStream byteStream() {
return in;
}
}
}

View File

@@ -15,27 +15,21 @@
* limitations under the License.
*/
package com.squareup.okhttp.internal.http;
package com.squareup.okhttp;
import com.squareup.okhttp.internal.Util;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
/**
* The HTTP status and unparsed header fields of a single HTTP message. Values
* are represented as uninterpreted strings; use {@code Request} and
* {@code Response} for interpreted headers. This class maintains the
* order of the header fields within the HTTP message.
* The header fields of a single HTTP message. Values are uninterpreted strings;
* use {@code Request} and {@code Response} for interpreted headers. This class
* maintains the order of the header fields within the HTTP message.
*
* <p>This class tracks fields line-by-line. A field with multiple comma-
* <p>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
@@ -44,29 +38,22 @@ import java.util.TreeSet;
*
* <p>This class trims whitespace from values. It never returns values with
* leading or trailing whitespace.
*
* <p>Instances of this class are immutable. Use {@link Builder} to create
* instances.
*/
public final class Headers {
private static final Comparator<String> FIELD_NAME_COMPARATOR = new Comparator<String>() {
// @FindBugsSuppressWarnings("ES_COMPARING_PARAMETER_STRING_WITH_EQ")
@Override public int compare(String a, String b) {
if (a == b) {
return 0;
} else if (a == null) {
return -1;
} else if (b == null) {
return 1;
} else {
return String.CASE_INSENSITIVE_ORDER.compare(a, b);
}
}
};
private final List<String> namesAndValues;
private Headers(Builder builder) {
this.namesAndValues = Util.immutableList(builder.namesAndValues);
}
/** Returns the last value corresponding to the specified field, or null. */
public String get(String fieldName) {
return get(namesAndValues, fieldName);
}
/** Returns the number of field values. */
public int size() {
return namesAndValues.size() / 2;
@@ -81,15 +68,6 @@ public final class Headers {
return namesAndValues.get(fieldNameIndex);
}
/** Returns an immutable case-insensitive set of header names. */
public Set<String> names() {
TreeSet<String> result = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
for (int i = 0; i < size(); i++) {
result.add(name(i));
}
return Collections.unmodifiableSet(result);
}
/** Returns the value at {@code index} or null if that is out of range. */
public String value(int index) {
int valueIndex = index * 2 + 1;
@@ -99,9 +77,13 @@ public final class Headers {
return namesAndValues.get(valueIndex);
}
/** Returns the last value corresponding to the specified field, or null. */
public String get(String fieldName) {
return get(namesAndValues, fieldName);
/** Returns an immutable case-insensitive set of header names. */
public Set<String> names() {
TreeSet<String> result = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
for (int i = 0; i < size(); i++) {
result.add(name(i));
}
return Collections.unmodifiableSet(result);
}
/** Returns an immutable list of the header values for {@code name}. */
@@ -119,6 +101,7 @@ public final class Headers {
}
/** @param fieldNames a case-insensitive set of HTTP header field names. */
// TODO: it is very weird to request a case-insensitive set as a parameter.
public Headers getAll(Set<String> fieldNames) {
Builder result = new Builder();
for (int i = 0; i < namesAndValues.size(); i += 2) {
@@ -130,32 +113,6 @@ public final class Headers {
return result.build();
}
/**
* Returns an immutable map containing each field to its list of values.
*
* @param valueForNullKey the request line for requests, or the status line
* for responses. If non-null, this value is mapped to the null key.
*/
public Map<String, List<String>> toMultimap(String valueForNullKey) {
Map<String, List<String>> result = new TreeMap<String, List<String>>(FIELD_NAME_COMPARATOR);
for (int i = 0; i < namesAndValues.size(); i += 2) {
String fieldName = namesAndValues.get(i);
String value = namesAndValues.get(i + 1);
List<String> allValues = new ArrayList<String>();
List<String> otherValues = result.get(fieldName);
if (otherValues != null) {
allValues.addAll(otherValues);
}
allValues.add(value);
result.put(fieldName, Collections.unmodifiableList(allValues));
}
if (valueForNullKey != null) {
result.put(null, Collections.unmodifiableList(Collections.singletonList(valueForNullKey)));
}
return Collections.unmodifiableMap(result);
}
public Builder newBuilder() {
Builder result = new Builder();
result.namesAndValues.addAll(namesAndValues);
@@ -174,21 +131,14 @@ public final class Headers {
public static class Builder {
private final List<String> namesAndValues = new ArrayList<String>(20);
/** Equivalent to {@code build().get(fieldName)}, but potentially faster. */
public String get(String fieldName) {
return Headers.get(namesAndValues, fieldName);
}
/**
* Add an HTTP header line containing a field name, a literal colon, and a
* value. This works around empty header names and header names that start
* with a colon (created by old broken SPDY versions of the response cache).
*/
/** Add an header line containing a field name, a literal colon, and a value. */
public Builder addLine(String line) {
int index = line.indexOf(":", 1);
if (index != -1) {
return addLenient(line.substring(0, index), line.substring(index + 1));
} else if (line.startsWith(":")) {
// Work around empty header names and header names that start with a
// colon (created by old broken SPDY versions of the response cache).
return addLenient("", line.substring(1)); // Empty header name.
} else {
return addLenient("", line); // No header name.
@@ -235,13 +185,9 @@ public final class Headers {
return this;
}
/** Reads headers or trailers into {@code out}. */
public Builder readHeaders(InputStream in) throws IOException {
// parse the result headers until the first blank line
for (String line; (line = Util.readAsciiLine(in)).length() != 0; ) {
addLine(line);
}
return this;
/** Equivalent to {@code build().get(fieldName)}, but potentially faster. */
public String get(String fieldName) {
return Headers.get(namesAndValues, fieldName);
}
public Headers build() {

View File

@@ -20,7 +20,6 @@ import com.squareup.okhttp.internal.Base64;
import com.squareup.okhttp.internal.DiskLruCache;
import com.squareup.okhttp.internal.StrictLineReader;
import com.squareup.okhttp.internal.Util;
import com.squareup.okhttp.internal.http.Headers;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.File;

View File

@@ -17,7 +17,9 @@ package com.squareup.okhttp;
import com.squareup.okhttp.internal.http.HttpAuthenticator;
import com.squareup.okhttp.internal.http.HttpEngine;
import com.squareup.okhttp.internal.http.OkHeaders;
import java.io.IOException;
import java.io.InputStream;
import java.net.ProtocolException;
import java.net.Proxy;
import java.net.URL;
@@ -82,7 +84,7 @@ final class Job implements Runnable {
long contentLength = body.contentLength();
if (contentLength != -1) {
requestBuilder.setContentLength(contentLength);
requestBuilder.header("Content-Length", Long.toString(contentLength));
requestBuilder.removeHeader("Transfer-Encoding");
} else {
requestBuilder.header("Transfer-Encoding", "chunked");
@@ -107,7 +109,7 @@ final class Job implements Runnable {
if (redirect == null) {
engine.automaticallyReleaseConnectionToPool();
return response.newBuilder()
.body(new Dispatcher.RealResponseBody(response, engine.getResponseBody()))
.body(new RealResponseBody(response, engine.getResponseBody()))
.redirectedBy(redirectedBy)
.build();
}
@@ -183,4 +185,31 @@ final class Job implements Runnable {
&& getEffectivePort(a.url()) == getEffectivePort(b.url())
&& a.url().getProtocol().equals(b.url().getProtocol());
}
static class RealResponseBody extends Response.Body {
private final Response response;
private final InputStream in;
RealResponseBody(Response response, InputStream in) {
this.response = response;
this.in = in;
}
@Override public boolean ready() throws IOException {
return true;
}
@Override public MediaType contentType() {
String contentType = response.header("Content-Type");
return contentType != null ? MediaType.parse(contentType) : null;
}
@Override public long contentLength() {
return OkHeaders.contentLength(response);
}
@Override public InputStream byteStream() {
return in;
}
}
}

View File

@@ -18,7 +18,6 @@ package com.squareup.okhttp;
import com.squareup.okhttp.internal.Platform;
import com.squareup.okhttp.internal.Util;
import com.squareup.okhttp.internal.http.HeaderParser;
import com.squareup.okhttp.internal.http.Headers;
import com.squareup.okhttp.internal.http.HttpDate;
import java.io.File;
import java.io.FileInputStream;
@@ -32,8 +31,6 @@ import java.net.URISyntaxException;
import java.net.URL;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* An HTTP request. Instances of this class are immutable if their {@link #body}
@@ -81,6 +78,10 @@ public final class Request {
return method;
}
public Headers headers() {
return headers;
}
public String header(String name) {
return headers.get(name);
}
@@ -89,26 +90,6 @@ public final class Request {
return headers.values(name);
}
public Set<String> headerNames() {
return headers.names();
}
Headers headers() {
return headers;
}
public int headerCount() {
return headers.size();
}
public String headerName(int index) {
return headers.name(index);
}
public String headerValue(int index) {
return headers.value(index);
}
public Body body() {
return body;
}
@@ -145,20 +126,10 @@ public final class Request {
return parsedHeaders().onlyIfCached;
}
// TODO: Make non-public. This conflicts with the Body's content length!
public long getContentLength() {
return parsedHeaders().contentLength;
}
public String getUserAgent() {
return parsedHeaders().userAgent;
}
// TODO: Make non-public. This conflicts with the Body's content type!
public String getContentType() {
return parsedHeaders().contentType;
}
public String getProxyAuthorization() {
return parsedHeaders().proxyAuthorization;
}
@@ -189,9 +160,7 @@ public final class Request {
*/
private boolean onlyIfCached;
private long contentLength = -1;
private String userAgent;
private String contentType;
private String proxyAuthorization;
public ParsedHeaders(Headers headers) {
@@ -220,15 +189,8 @@ public final class Request {
if ("no-cache".equalsIgnoreCase(value)) {
noCache = true;
}
} else if ("Content-Length".equalsIgnoreCase(fieldName)) {
try {
contentLength = Long.parseLong(value);
} catch (NumberFormatException ignored) {
}
} else if ("User-Agent".equalsIgnoreCase(fieldName)) {
userAgent = value;
} else if ("Content-Type".equalsIgnoreCase(fieldName)) {
contentType = value;
} else if ("Proxy-Authorization".equalsIgnoreCase(fieldName)) {
proxyAuthorization = value;
}
@@ -323,7 +285,7 @@ public final class Request {
public static class Builder {
private URL url;
private String method;
private final Headers.Builder headers;
private Headers.Builder headers;
private Body body;
private Object tag;
@@ -377,51 +339,22 @@ public final class Request {
return this;
}
// TODO: conflict's with the body's content type.
public Builder setContentLength(long contentLength) {
headers.set("Content-Length", Long.toString(contentLength));
/** Removes all headers on this builder and adds {@code headers}. */
public Builder headers(Headers headers) {
this.headers = headers.newBuilder();
return this;
}
public void setUserAgent(String userAgent) {
headers.set("User-Agent", userAgent);
public Builder setUserAgent(String userAgent) {
return header("User-Agent", userAgent);
}
// TODO: conflict's with the body's content type.
public void setContentType(String contentType) {
headers.set("Content-Type", contentType);
public Builder setIfModifiedSince(Date date) {
return header("If-Modified-Since", HttpDate.format(date));
}
public void setIfModifiedSince(Date date) {
headers.set("If-Modified-Since", HttpDate.format(date));
}
public void setIfNoneMatch(String ifNoneMatch) {
headers.set("If-None-Match", ifNoneMatch);
}
public void addCookies(Map<String, List<String>> cookieHeaders) {
for (Map.Entry<String, List<String>> entry : cookieHeaders.entrySet()) {
String key = entry.getKey();
if (("Cookie".equalsIgnoreCase(key) || "Cookie2".equalsIgnoreCase(key))
&& !entry.getValue().isEmpty()) {
headers.add(key, buildCookieHeader(entry.getValue()));
}
}
}
/**
* Send all cookies in one big header, as recommended by
* <a href="http://tools.ietf.org/html/rfc6265#section-4.2.1">RFC 6265</a>.
*/
private String buildCookieHeader(List<String> cookies) {
if (cookies.size() == 1) return cookies.get(0);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < cookies.size(); i++) {
if (i > 0) sb.append("; ");
sb.append(cookies.get(i));
}
return sb.toString();
public Builder setIfNoneMatch(String ifNoneMatch) {
return header("If-None-Match", ifNoneMatch);
}
public Builder get() {

View File

@@ -17,10 +17,9 @@ package com.squareup.okhttp;
import com.squareup.okhttp.internal.Util;
import com.squareup.okhttp.internal.http.HeaderParser;
import com.squareup.okhttp.internal.http.Headers;
import com.squareup.okhttp.internal.http.HttpDate;
import com.squareup.okhttp.internal.http.OkHeaders;
import com.squareup.okhttp.internal.http.StatusLine;
import com.squareup.okhttp.internal.http.SyntheticHeaders;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException;
@@ -105,6 +104,10 @@ public final class Response {
return handshake;
}
public List<String> headers(String name) {
return headers.values(name);
}
public String header(String name) {
return header(name, null);
}
@@ -114,31 +117,10 @@ public final class Response {
return result != null ? result : defaultValue;
}
public List<String> headers(String name) {
return headers.values(name);
}
public Set<String> headerNames() {
return headers.names();
}
public int headerCount() {
return headers.size();
}
public String headerName(int index) {
return headers.name(index);
}
// TODO: this shouldn't be public?
public Headers headers() {
return headers;
}
public String headerValue(int index) {
return headers.value(index);
}
public Body body() {
return body;
}
@@ -201,16 +183,6 @@ public final class Response {
return parsedHeaders().varyFields;
}
// TODO: this shouldn't be public.
public long getContentLength() {
return parsedHeaders().contentLength;
}
// TODO: this shouldn't be public.
public String getContentType() {
return parsedHeaders().contentType;
}
/**
* Returns true if a Vary header contains an asterisk. Such responses cannot
* be cached.
@@ -471,9 +443,9 @@ public final class Response {
}
} else if ("Content-Type".equalsIgnoreCase(fieldName)) {
contentType = value;
} else if (SyntheticHeaders.SENT_MILLIS.equalsIgnoreCase(fieldName)) {
} else if (OkHeaders.SENT_MILLIS.equalsIgnoreCase(fieldName)) {
sentRequestMillis = Long.parseLong(value);
} else if (SyntheticHeaders.RECEIVED_MILLIS.equalsIgnoreCase(fieldName)) {
} else if (OkHeaders.RECEIVED_MILLIS.equalsIgnoreCase(fieldName)) {
receivedResponseMillis = Long.parseLong(value);
}
}
@@ -589,7 +561,7 @@ public final class Response {
return this;
}
// TODO: this shouldn't be public?
/** Removes all headers on this builder and adds {@code headers}. */
public Builder headers(Headers headers) {
this.headers = headers.newBuilder();
return this;
@@ -602,8 +574,7 @@ public final class Response {
// TODO: this shouldn't be public.
public Builder setResponseSource(ResponseSource responseSource) {
headers.set(SyntheticHeaders.RESPONSE_SOURCE, responseSource + " " + statusLine.code());
return this;
return header(OkHeaders.RESPONSE_SOURCE, responseSource + " " + statusLine.code());
}
public Builder redirectedBy(Response redirectedBy) {

View File

@@ -18,7 +18,21 @@ package com.squareup.okhttp;
import java.net.InetSocketAddress;
import java.net.Proxy;
/** Represents the route used by a connection to reach an endpoint. */
/**
* The concrete route used by a connection to reach an abstract origin server.
* When creating a connection the client has many options:
* <ul>
* <li><strong>HTTP proxy:</strong> a proxy server may be explicitly
* configured for the client. Otherwise the {@link java.net.ProxySelector
* proxy selector} is used. It may return multiple proxies to attempt.
* <li><strong>IP address:</strong> whether connecting directly to an origin
* server or a proxy, opening a socket requires an IP address. The DNS
* server may return multiple IP addresses to attempt.
* <li><strong>Modern TLS:</strong> whether to include advanced TLS options
* when attempting a HTTPS connection.
* </ul>
* Each route is a specific selection of these options.
*/
public class Route {
final Address address;
final Proxy proxy;
@@ -44,11 +58,8 @@ public class Route {
/**
* Returns the {@link Proxy} of this route.
*
* <strong>Warning:</strong> This may be different than the proxy returned
* by {@link #getAddress}! That is the proxy that the user asked to be
* connected to; this returns the proxy that they were actually connected
* to. The two may disagree when a proxy selector selects a different proxy
* for a connection.
* <strong>Warning:</strong> This may disagree with {@link Address#getProxy}
* is null. When the address's proxy is null, the proxy selector will be used.
*/
public Proxy getProxy() {
return proxy;

View File

@@ -16,6 +16,7 @@
*/
package com.squareup.okhttp.internal.http;
import com.squareup.okhttp.Headers;
import com.squareup.okhttp.OkAuthenticator;
import com.squareup.okhttp.OkAuthenticator.Challenge;
import com.squareup.okhttp.Request;

View File

@@ -19,7 +19,7 @@ package com.squareup.okhttp.internal.http;
import com.squareup.okhttp.Address;
import com.squareup.okhttp.Connection;
import com.squareup.okhttp.MediaType;
import com.squareup.okhttp.Headers;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.OkResponseCache;
import com.squareup.okhttp.Request;
@@ -35,6 +35,8 @@ import java.net.CacheRequest;
import java.net.CookieHandler;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.List;
import java.util.Map;
import java.util.zip.GZIPInputStream;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLSocketFactory;
@@ -206,10 +208,7 @@ public class HttpEngine {
private Response cacheableResponse() {
// Use an unreadable response body when offering the response to the cache.
// The cache isn't allowed to consume the response body bytes!
return response.newBuilder()
.body(new UnreadableResponseBody(response.getContentType(),
response.getContentLength()))
.build();
return response.newBuilder().body(null).build();
}
/** Connect to the origin server either directly or via a proxy. */
@@ -400,7 +399,7 @@ public class HttpEngine {
// If the Content-Length or Transfer-Encoding headers disagree with the
// response code, the response is malformed. For best compatibility, we
// honor the headers.
if (response.getContentLength() != -1
if (OkHeaders.contentLength(response) != -1
|| "chunked".equalsIgnoreCase(response.header("Transfer-Encoding"))) {
return true;
}
@@ -435,13 +434,15 @@ public class HttpEngine {
result.header("Accept-Encoding", "gzip");
}
if (hasRequestBody() && request.getContentType() == null) {
result.setContentType("application/x-www-form-urlencoded");
if (hasRequestBody() && request.header("Content-Type") == null) {
result.header("Content-Type", "application/x-www-form-urlencoded");
}
CookieHandler cookieHandler = client.getCookieHandler();
if (cookieHandler != null) {
result.addCookies(cookieHandler.get(request.uri(), request.getHeaders().toMultimap(null)));
Map<String, List<String>> cookies = cookieHandler.get(
request.uri(), OkHeaders.toMultimap(request.getHeaders(), null));
OkHeaders.addCookies(result, cookies);
}
request = result.build();
@@ -468,11 +469,13 @@ public class HttpEngine {
if (!responseSource.requiresConnection()) return;
if (sentRequestMillis == -1) {
if (request.getContentLength() == -1
if (OkHeaders.contentLength(request) == -1
&& requestBodyOut instanceof RetryableOutputStream) {
// We might not learn the Content-Length until the request body has been buffered.
int contentLength = ((RetryableOutputStream) requestBodyOut).contentLength();
request = request.newBuilder().setContentLength(contentLength).build();
long contentLength = ((RetryableOutputStream) requestBodyOut).contentLength();
request = request.newBuilder()
.header("Content-Length", Long.toString(contentLength))
.build();
}
transport.writeRequestHeaders(request);
}
@@ -489,8 +492,8 @@ public class HttpEngine {
response = transport.readResponseHeaders()
.request(request)
.handshake(connection.getHandshake())
.header(SyntheticHeaders.SENT_MILLIS, Long.toString(sentRequestMillis))
.header(SyntheticHeaders.RECEIVED_MILLIS, Long.toString(System.currentTimeMillis()))
.header(OkHeaders.SENT_MILLIS, Long.toString(sentRequestMillis))
.header(OkHeaders.RECEIVED_MILLIS, Long.toString(System.currentTimeMillis()))
.setResponseSource(responseSource)
.build();
connection.setHttpMinorVersion(response.httpMinorVersion());
@@ -530,9 +533,10 @@ public class HttpEngine {
private static Response combine(Response cached, Response network) throws IOException {
Headers.Builder result = new Headers.Builder();
for (int i = 0; i < cached.headerCount(); i++) {
String fieldName = cached.headerName(i);
String value = cached.headerValue(i);
Headers cachedHeaders = cached.headers();
for (int i = 0; i < cachedHeaders.size(); i++) {
String fieldName = cachedHeaders.name(i);
String value = cachedHeaders.value(i);
if ("Warning".equals(fieldName) && value.startsWith("1")) {
continue; // drop 100-level freshness warnings
}
@@ -541,10 +545,11 @@ public class HttpEngine {
}
}
for (int i = 0; i < network.headerCount(); i++) {
String fieldName = network.headerName(i);
Headers networkHeaders = network.headers();
for (int i = 0; i < networkHeaders.size(); i++) {
String fieldName = networkHeaders.name(i);
if (isEndToEnd(fieldName)) {
result.add(fieldName, network.headerValue(i));
result.add(fieldName, networkHeaders.value(i));
}
}
@@ -580,33 +585,7 @@ public class HttpEngine {
public void receiveHeaders(Headers headers) throws IOException {
CookieHandler cookieHandler = client.getCookieHandler();
if (cookieHandler != null) {
cookieHandler.put(request.uri(), headers.toMultimap(null));
}
}
static class UnreadableResponseBody extends Response.Body {
private final String contentType;
private final long contentLength;
public UnreadableResponseBody(String contentType, long contentLength) {
this.contentType = contentType;
this.contentLength = contentLength;
}
@Override public boolean ready() throws IOException {
throw new IllegalStateException("It is an error to read this response body at this time.");
}
@Override public MediaType contentType() {
return contentType != null ? MediaType.parse(contentType) : null;
}
@Override public long contentLength() {
return contentLength;
}
@Override public InputStream byteStream() {
throw new IllegalStateException("It is an error to read this response body at this time.");
cookieHandler.put(request.uri(), OkHeaders.toMultimap(headers, null));
}
}
}

View File

@@ -17,6 +17,7 @@
package com.squareup.okhttp.internal.http;
import com.squareup.okhttp.Connection;
import com.squareup.okhttp.Headers;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;
import com.squareup.okhttp.internal.AbstractOutputStream;
@@ -62,7 +63,7 @@ public final class HttpTransport implements Transport {
}
@Override public OutputStream createRequestBody(Request request) throws IOException {
long contentLength = request.getContentLength();
long contentLength = OkHeaders.contentLength(request);
if (httpEngine.bufferRequestBody) {
if (contentLength > Integer.MAX_VALUE) {
@@ -154,10 +155,10 @@ public final class HttpTransport implements Transport {
Response.Builder responseBuilder = new Response.Builder()
.statusLine(statusLine)
.header(SyntheticHeaders.SELECTED_TRANSPORT, "http/1.1");
.header(OkHeaders.SELECTED_TRANSPORT, "http/1.1");
Headers.Builder headersBuilder = new Headers.Builder();
headersBuilder.readHeaders(in);
OkHeaders.readHeaders(headersBuilder, in);
responseBuilder.headers(headersBuilder.build());
if (statusLine.code() != HTTP_CONTINUE) return responseBuilder;
@@ -234,9 +235,9 @@ public final class HttpTransport implements Transport {
return new ChunkedInputStream(socketIn, cacheRequest, this);
}
if (httpEngine.getResponse().getContentLength() != -1) {
return new FixedLengthInputStream(socketIn, cacheRequest, httpEngine,
httpEngine.getResponse().getContentLength());
long contentLength = OkHeaders.contentLength(httpEngine.getResponse());
if (contentLength != -1) {
return new FixedLengthInputStream(socketIn, cacheRequest, httpEngine, contentLength);
}
// Wrap the input stream from the connection (rather than just returning
@@ -443,14 +444,12 @@ public final class HttpTransport implements Transport {
/** An HTTP body with alternating chunk sizes and chunk bodies. */
private static class ChunkedInputStream extends AbstractHttpInputStream {
private static final int NO_CHUNK_YET = -1;
private final HttpTransport transport;
private int bytesRemainingInChunk = NO_CHUNK_YET;
private boolean hasMoreChunks = true;
ChunkedInputStream(InputStream is, CacheRequest cacheRequest, HttpTransport transport)
throws IOException {
super(is, transport.httpEngine, cacheRequest);
this.transport = transport;
}
@Override public int read(byte[] buffer, int offset, int count) throws IOException {
@@ -493,10 +492,9 @@ public final class HttpTransport implements Transport {
}
if (bytesRemainingInChunk == 0) {
hasMoreChunks = false;
Headers trailers = new Headers.Builder()
.readHeaders(transport.socketIn)
.build();
httpEngine.receiveHeaders(trailers);
Headers.Builder trailersBuilder = new Headers.Builder();
OkHeaders.readHeaders(trailersBuilder, in);
httpEngine.receiveHeaders(trailersBuilder.build());
endOfInput();
}
}

View File

@@ -18,6 +18,7 @@
package com.squareup.okhttp.internal.http;
import com.squareup.okhttp.Connection;
import com.squareup.okhttp.Headers;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;
@@ -165,7 +166,7 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
@Override public final Map<String, List<String>> getHeaderFields() {
try {
Response response = getResponse().getResponse();
return response.headers().toMultimap(response.statusLine());
return OkHeaders.toMultimap(response.headers(), response.statusLine());
} catch (IOException e) {
return Collections.emptyMap();
}
@@ -180,7 +181,7 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
// For the request line property assigned to the null key, just use no proxy and HTTP 1.1.
Request request = new Request.Builder().url(getURL()).method(method, null).build();
String requestLine = RequestLine.get(request, null, 1);
return requestHeaders.build().toMultimap(requestLine);
return OkHeaders.toMultimap(requestHeaders.build(), requestLine);
}
@Override public final InputStream getInputStream() throws IOException {

View File

@@ -0,0 +1,134 @@
package com.squareup.okhttp.internal.http;
import com.squareup.okhttp.Headers;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;
import com.squareup.okhttp.internal.Platform;
import com.squareup.okhttp.internal.Util;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
/** Headers and utilities for internal use by OkHttp. */
public final class OkHeaders {
private static final Comparator<String> FIELD_NAME_COMPARATOR = new Comparator<String>() {
// @FindBugsSuppressWarnings("ES_COMPARING_PARAMETER_STRING_WITH_EQ")
@Override public int compare(String a, String b) {
if (a == b) {
return 0;
} else if (a == null) {
return -1;
} else if (b == null) {
return 1;
} else {
return String.CASE_INSENSITIVE_ORDER.compare(a, b);
}
}
};
static final String PREFIX = Platform.get().getPrefix();
/**
* Synthetic response header: the local time when the request was sent.
*/
public static final String SENT_MILLIS = PREFIX + "-Sent-Millis";
/**
* Synthetic response header: the local time when the response was received.
*/
public static final String RECEIVED_MILLIS = PREFIX + "-Received-Millis";
/**
* Synthetic response header: the response source and status code like
* "CONDITIONAL_CACHE 304".
*/
public static final String RESPONSE_SOURCE = PREFIX + "-Response-Source";
/**
* Synthetic response header: the selected transport ("spdy/3", "http/1.1", etc).
*/
public static final String SELECTED_TRANSPORT = PREFIX + "-Selected-Transport";
private OkHeaders() {
}
public static long contentLength(Request request) {
return stringToLong(request.header("Content-Length"));
}
public static long contentLength(Response response) {
return stringToLong(response.header("Content-Length"));
}
private static long stringToLong(String s) {
if (s == null) return -1;
try {
return Long.parseLong(s);
} catch (NumberFormatException e) {
return -1;
}
}
/**
* Returns an immutable map containing each field to its list of values.
*
* @param valueForNullKey the request line for requests, or the status line
* for responses. If non-null, this value is mapped to the null key.
*/
public static Map<String, List<String>> toMultimap(Headers headers, String valueForNullKey) {
Map<String, List<String>> result = new TreeMap<String, List<String>>(FIELD_NAME_COMPARATOR);
for (int i = 0; i < headers.size(); i++) {
String fieldName = headers.name(i);
String value = headers.value(i);
List<String> allValues = new ArrayList<String>();
List<String> otherValues = result.get(fieldName);
if (otherValues != null) {
allValues.addAll(otherValues);
}
allValues.add(value);
result.put(fieldName, Collections.unmodifiableList(allValues));
}
if (valueForNullKey != null) {
result.put(null, Collections.unmodifiableList(Collections.singletonList(valueForNullKey)));
}
return Collections.unmodifiableMap(result);
}
public static void addCookies(Request.Builder builder, Map<String, List<String>> cookieHeaders) {
for (Map.Entry<String, List<String>> entry : cookieHeaders.entrySet()) {
String key = entry.getKey();
if (("Cookie".equalsIgnoreCase(key) || "Cookie2".equalsIgnoreCase(key))
&& !entry.getValue().isEmpty()) {
builder.addHeader(key, buildCookieHeader(entry.getValue()));
}
}
}
/**
* Send all cookies in one big header, as recommended by
* <a href="http://tools.ietf.org/html/rfc6265#section-4.2.1">RFC 6265</a>.
*/
private static String buildCookieHeader(List<String> cookies) {
if (cookies.size() == 1) return cookies.get(0);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < cookies.size(); i++) {
if (i > 0) sb.append("; ");
sb.append(cookies.get(i));
}
return sb.toString();
}
/** Reads headers or trailers into {@code builder}. */
public static void readHeaders(Headers.Builder builder, InputStream in) throws IOException {
// parse the result headers until the first blank line
for (String line; (line = Util.readAsciiLine(in)).length() != 0; ) {
builder.addLine(line);
}
}
}

View File

@@ -16,6 +16,7 @@
package com.squareup.okhttp.internal.http;
import com.squareup.okhttp.Headers;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;
import com.squareup.okhttp.internal.spdy.ErrorCode;
@@ -78,7 +79,8 @@ public final class SpdyTransport implements Transport {
* values, they are concatenated using "\0" as a delimiter.
*/
public static List<String> writeNameValueBlock(Request request, String version) {
List<String> result = new ArrayList<String>(request.headerCount() + 10);
Headers headers = request.headers();
List<String> result = new ArrayList<String>(headers.size() + 10);
result.add(":method");
result.add(request.method());
result.add(":path");
@@ -91,9 +93,9 @@ public final class SpdyTransport implements Transport {
result.add(request.url().getProtocol());
Set<String> names = new LinkedHashSet<String>();
for (int i = 0; i < request.headerCount(); i++) {
String name = request.headerName(i).toLowerCase(Locale.US);
String value = request.headerValue(i);
for (int i = 0; i < headers.size(); i++) {
String name = headers.name(i).toLowerCase(Locale.US);
String value = headers.value(i);
// Drop headers that are forbidden when layering HTTP over SPDY.
if (name.equals("connection")
@@ -141,7 +143,7 @@ public final class SpdyTransport implements Transport {
String version = null;
Headers.Builder headersBuilder = new Headers.Builder();
headersBuilder.set(SyntheticHeaders.SELECTED_TRANSPORT, "spdy/3");
headersBuilder.set(OkHeaders.SELECTED_TRANSPORT, "spdy/3");
for (int i = 0; i < nameValueBlock.size(); i += 2) {
String name = nameValueBlock.get(i);
String values = nameValueBlock.get(i + 1);

View File

@@ -1,23 +0,0 @@
package com.squareup.okhttp.internal.http;
import com.squareup.okhttp.internal.Platform;
/** Headers added to the HTTP response for internal use by OkHttp. */
public final class SyntheticHeaders {
static final String PREFIX = Platform.get().getPrefix();
/** The local time when the request was sent. */
public static final String SENT_MILLIS = PREFIX + "-Sent-Millis";
/** The local time when the response was received. */
public static final String RECEIVED_MILLIS = PREFIX + "-Received-Millis";
/** The response source. */
public static final String RESPONSE_SOURCE = PREFIX + "-Response-Source";
/** The selected transport (spdy/3, http/1.1, etc). */
public static final String SELECTED_TRANSPORT = PREFIX + "-Selected-Transport";
private SyntheticHeaders() {
}
}

View File

@@ -47,8 +47,9 @@ public class RecordedResponse {
public RecordedResponse assertContainsHeaders(String... expectedHeaders) {
List<String> actualHeaders = new ArrayList<String>();
for (int i = 0; i < response.headerCount(); i++) {
actualHeaders.add(response.headerName(i) + ": " + response.headerValue(i));
Headers headers = response.headers();
for (int i = 0; i < headers.size(); i++) {
actualHeaders.add(headers.name(i) + ": " + headers.value(i));
}
if (!actualHeaders.containsAll(Arrays.asList(expectedHeaders))) {
fail("Expected: " + actualHeaders + "\nto contain: " + Arrays.toString(expectedHeaders));

View File

@@ -15,6 +15,7 @@
*/
package com.squareup.okhttp.internal.http;
import com.squareup.okhttp.Headers;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;
import java.io.IOException;
@@ -39,8 +40,8 @@ public final class HeadersTest {
assertEquals("HTTP/1.1 200 OK", response.statusLine());
assertEquals("no-cache, no-store", headers.get("cache-control"));
assertEquals("Cookie2", headers.get("set-cookie"));
assertEquals("spdy/3", headers.get(SyntheticHeaders.SELECTED_TRANSPORT));
assertEquals(SyntheticHeaders.SELECTED_TRANSPORT, headers.name(0));
assertEquals("spdy/3", headers.get(OkHeaders.SELECTED_TRANSPORT));
assertEquals(OkHeaders.SELECTED_TRANSPORT, headers.name(0));
assertEquals("spdy/3", headers.value(0));
assertEquals("cache-control", headers.name(1));
assertEquals("no-cache, no-store", headers.value(1));

View File

@@ -233,14 +233,10 @@ public final class HttpResponseCacheTest {
@Override public CacheRequest put(Response response) throws IOException {
assertEquals(server.getUrl("/"), response.request().url());
assertEquals(200, response.code());
assertEquals(body.length(), response.body().contentLength());
assertEquals("text/plain", response.body().contentType().toString());
assertNull(response.body());
assertEquals("5", response.header("Content-Length"));
assertEquals("text/plain", response.header("Content-Type"));
assertEquals("ijk", response.header("fgh"));
try {
response.body().byteStream(); // the RI doesn't forbid this, but it should
fail();
} catch (IllegalStateException expected) {
}
cacheCount.incrementAndGet();
return null;
}
@@ -1696,7 +1692,7 @@ public final class HttpResponseCacheTest {
connection.addRequestProperty("Cache-Control", "only-if-cached");
assertEquals("A", readAscii(connection));
String source = connection.getHeaderField(SyntheticHeaders.RESPONSE_SOURCE);
String source = connection.getHeaderField(OkHeaders.RESPONSE_SOURCE);
assertEquals(ResponseSource.CACHE + " 200", source);
}
@@ -1713,7 +1709,7 @@ public final class HttpResponseCacheTest {
HttpURLConnection connection = openConnection(server.getUrl("/"));
assertEquals("B", readAscii(connection));
String source = connection.getHeaderField(SyntheticHeaders.RESPONSE_SOURCE);
String source = connection.getHeaderField(OkHeaders.RESPONSE_SOURCE);
assertEquals(ResponseSource.CONDITIONAL_CACHE + " 200", source);
}
@@ -1728,7 +1724,7 @@ public final class HttpResponseCacheTest {
HttpURLConnection connection = openConnection(server.getUrl("/"));
assertEquals("A", readAscii(connection));
String source = connection.getHeaderField(SyntheticHeaders.RESPONSE_SOURCE);
String source = connection.getHeaderField(OkHeaders.RESPONSE_SOURCE);
assertEquals(ResponseSource.CONDITIONAL_CACHE + " 304", source);
}
@@ -1739,7 +1735,7 @@ public final class HttpResponseCacheTest {
URLConnection connection = openConnection(server.getUrl("/"));
assertEquals("A", readAscii(connection));
String source = connection.getHeaderField(SyntheticHeaders.RESPONSE_SOURCE);
String source = connection.getHeaderField(OkHeaders.RESPONSE_SOURCE);
assertEquals(ResponseSource.NETWORK + " 200", source);
}
@@ -1956,7 +1952,7 @@ public final class HttpResponseCacheTest {
assertEquals(504, connection.getResponseCode());
assertEquals(-1, connection.getErrorStream().read());
assertEquals(ResponseSource.NONE + " 504",
connection.getHeaderField(SyntheticHeaders.RESPONSE_SOURCE));
connection.getHeaderField(OkHeaders.RESPONSE_SOURCE));
}
enum TransferKind {