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

Make RawHeaders, RequestHeaders and ResponseHeaders immutable.

This introduces a new, poorly-named class ResponseStrategy
that pulls some code out of ResponseHeaders. That was necessary
because the old method mutated itself and its parameters in
place.

Obvious follow-up for this is to combine ResponseHeaders with
Response, and RequestHeaders with Response.
This commit is contained in:
jwilson
2013-12-29 19:14:06 -05:00
parent 63a278d2f4
commit 350c43b6fe
21 changed files with 738 additions and 643 deletions

View File

@@ -306,22 +306,17 @@ public final class Connection implements Closeable {
RawHeaders requestHeaders = tunnelRequest.getRequestHeaders();
while (true) {
out.write(requestHeaders.toBytes());
RawHeaders responseHeaders = RawHeaders.fromBytes(in);
RawHeaders responseHeaders = RawHeaders.readHttpHeaders(in);
switch (responseHeaders.getResponseCode()) {
case HTTP_OK:
return;
case HTTP_PROXY_AUTH:
requestHeaders = new RawHeaders(requestHeaders);
URL url = new URL("https", tunnelRequest.host, tunnelRequest.port, "/");
boolean credentialsFound = HttpAuthenticator.processAuthHeader(
route.address.authenticator, HTTP_PROXY_AUTH, responseHeaders, requestHeaders,
route.proxy, url);
if (credentialsFound) {
continue;
} else {
throw new IOException("Failed to authenticate with proxy");
}
requestHeaders = HttpAuthenticator.processAuthHeader(route.address.authenticator,
HTTP_PROXY_AUTH, responseHeaders, requestHeaders, route.proxy, url);
if (requestHeaders != null) continue;
throw new IOException("Failed to authenticate with proxy");
default:
throw new IOException(
"Unexpected response code for CONNECT: " + responseHeaders.getResponseCode());

View File

@@ -415,18 +415,20 @@ public final class HttpResponseCache extends ResponseCache implements OkResponse
StrictLineReader reader = new StrictLineReader(in, US_ASCII);
url = reader.readLine();
requestMethod = reader.readLine();
varyHeaders = new RawHeaders();
RawHeaders.Builder varyHeadersBuilder = new RawHeaders.Builder();
int varyRequestHeaderLineCount = reader.readInt();
for (int i = 0; i < varyRequestHeaderLineCount; i++) {
varyHeaders.addLine(reader.readLine());
varyHeadersBuilder.addLine(reader.readLine());
}
varyHeaders = varyHeadersBuilder.build();
responseHeaders = new RawHeaders();
responseHeaders.setStatusLine(reader.readLine());
RawHeaders.Builder responseHeadersBuilder = new RawHeaders.Builder();
responseHeadersBuilder.setStatusLine(reader.readLine());
int responseHeaderLineCount = reader.readInt();
for (int i = 0; i < responseHeaderLineCount; i++) {
responseHeaders.addLine(reader.readLine());
responseHeadersBuilder.addLine(reader.readLine());
}
responseHeaders = responseHeadersBuilder.build();
if (isHttps()) {
String blank = reader.readLine();

View File

@@ -104,17 +104,16 @@ final class Job implements Runnable, Policy {
Response redirectedBy = null;
while (true) {
HttpEngine engine = newEngine(connection);
Request.Body body = request.body();
if (body != null) {
MediaType contentType = body.contentType();
if (contentType == null) throw new IllegalStateException("contentType == null");
if (engine.getRequestHeaders().getContentType() == null) {
engine.getRequestHeaders().setContentType(contentType.toString());
if (request.header("Content-Type") == null) {
request = request.newBuilder().header("Content-Type", contentType.toString()).build();
}
}
HttpEngine engine = newEngine(connection);
engine.sendRequest();
if (body != null) {
@@ -183,11 +182,10 @@ final class Job implements Runnable, Policy {
}
// fall-through
case HTTP_UNAUTHORIZED:
RawHeaders successorRequestHeaders = request.rawHeaders();
boolean credentialsFound = HttpAuthenticator.processAuthHeader(client.getAuthenticator(),
response.code(), response.rawHeaders(), successorRequestHeaders, selectedProxy,
this.request.url());
return credentialsFound
RawHeaders successorRequestHeaders = HttpAuthenticator.processAuthHeader(
client.getAuthenticator(), response.code(), response.rawHeaders(), request.rawHeaders(),
selectedProxy, this.request.url());
return successorRequestHeaders != null
? request.newBuilder().rawHeaders(successorRequestHeaders).build()
: null;

View File

@@ -45,7 +45,7 @@ public final class Request {
private Request(Builder builder) {
this.url = builder.url;
this.method = builder.method;
this.headers = new RawHeaders(builder.headers);
this.headers = builder.headers.build();
this.body = builder.body;
this.tag = builder.tag != null ? builder.tag : this;
}
@@ -75,7 +75,7 @@ public final class Request {
}
RawHeaders rawHeaders() {
return new RawHeaders(headers);
return headers;
}
public int headerCount() {
@@ -192,7 +192,7 @@ public final class Request {
public static class Builder {
private URL url;
private String method = "GET";
private RawHeaders headers = new RawHeaders();
private RawHeaders.Builder headers = new RawHeaders.Builder();
private Body body;
private Object tag;
@@ -239,7 +239,7 @@ public final class Request {
// TODO: this shouldn't be public.
public Builder rawHeaders(RawHeaders rawHeaders) {
headers = new RawHeaders(rawHeaders);
headers = rawHeaders.newBuilder();
return this;
}

View File

@@ -49,7 +49,7 @@ public final class Response {
this.request = builder.request;
this.code = builder.code;
this.handshake = builder.handshake;
this.headers = new RawHeaders(builder.headers);
this.headers = builder.headers.build();
this.body = builder.body;
this.redirectedBy = builder.redirectedBy;
}
@@ -109,7 +109,7 @@ public final class Response {
// TODO: this shouldn't be public.
public RawHeaders rawHeaders() {
return new RawHeaders(headers);
return headers;
}
public String headerValue(int index) {
@@ -254,7 +254,7 @@ public final class Response {
private final Request request;
private final int code;
private Handshake handshake;
private RawHeaders headers = new RawHeaders();
private RawHeaders.Builder headers = new RawHeaders.Builder();
private Body body;
private Response redirectedBy;
@@ -290,7 +290,7 @@ public final class Response {
// TODO: this shouldn't be public.
public Builder rawHeaders(RawHeaders rawHeaders) {
headers = new RawHeaders(rawHeaders);
headers = rawHeaders.newBuilder();
return this;
}

View File

@@ -55,8 +55,8 @@ public final class TunnelRequest {
* the proxy unencrypted.
*/
RawHeaders getRequestHeaders() {
RawHeaders result = new RawHeaders();
result.setRequestLine("CONNECT " + host + ":" + port + " HTTP/1.1");
RawHeaders.Builder result = new RawHeaders.Builder()
.setRequestLine("CONNECT " + host + ":" + port + " HTTP/1.1");
// Always set Host and User-Agent.
result.set("Host", port == getDefaultPort("https") ? host : (host + ":" + port));
@@ -70,6 +70,6 @@ public final class TunnelRequest {
// Always set the Proxy-Connection to Keep-Alive for the benefit of
// HTTP/1.0 proxies like Squid.
result.set("Proxy-Connection", "Keep-Alive");
return result;
return result.build();
}
}

View File

@@ -84,12 +84,11 @@ public final class HttpAuthenticator {
/**
* React to a failed authorization response by looking up new credentials.
*
* @return true if credentials have been added to successorRequestHeaders
* and another request should be attempted.
* Returns headers for a subsequent attempt, or null if no further attempts
* should be made.
*/
public static boolean processAuthHeader(OkAuthenticator authenticator, int responseCode,
RawHeaders responseHeaders, RawHeaders successorRequestHeaders, Proxy proxy, URL url)
public static RawHeaders processAuthHeader(OkAuthenticator authenticator, int responseCode,
RawHeaders responseHeaders, RawHeaders requestHeaders, Proxy proxy, URL url)
throws IOException {
String responseField;
String requestField;
@@ -103,18 +102,14 @@ public final class HttpAuthenticator {
throw new IllegalArgumentException(); // TODO: ProtocolException?
}
List<Challenge> challenges = parseChallenges(responseHeaders, responseField);
if (challenges.isEmpty()) {
return false; // Could not find a challenge so end the request cycle.
}
if (challenges.isEmpty()) return null; // Could not find a challenge so end the request cycle.
Credential credential = responseHeaders.getResponseCode() == HTTP_PROXY_AUTH
? authenticator.authenticateProxy(proxy, url, challenges)
: authenticator.authenticate(proxy, url, challenges);
if (credential == null) {
return false; // Could not satisfy the challenge so end the request cycle.
}
if (credential == null) return null; // Couldn't satisfy the challenge so end the request cycle.
// Add authorization credentials, bypassing the already-connected check.
successorRequestHeaders.set(requestField, credential.getHeaderValue());
return true;
return requestHeaders.newBuilder().set(requestField, credential.getHeaderValue()).build();
}
/**

View File

@@ -123,7 +123,7 @@ public class HttpEngine {
final URI uri;
final RequestHeaders requestHeaders;
RequestHeaders requestHeaders;
/** Null until a response is received from the network or the cache. */
ResponseHeaders responseHeaders;
@@ -170,7 +170,7 @@ public class HttpEngine {
throw new IOException(e.getMessage());
}
this.requestHeaders = new RequestHeaders(uri, new RawHeaders(requestHeaders));
this.requestHeaders = new RequestHeaders(uri, requestHeaders);
if (connection != null && connection.getSocket() instanceof SSLSocket) {
handshake = Handshake.get(((SSLSocket) connection.getSocket()).getSession());
@@ -208,8 +208,9 @@ public class HttpEngine {
}
this.responseSource = ResponseSource.CACHE;
RawHeaders gatewayTimeoutHeaders = new RawHeaders();
gatewayTimeoutHeaders.setStatusLine("HTTP/1.1 504 Gateway Timeout");
RawHeaders gatewayTimeoutHeaders = new RawHeaders.Builder()
.setStatusLine("HTTP/1.1 504 Gateway Timeout")
.build();
this.validatingResponse = new Response.Builder(request(), 504)
.rawHeaders(gatewayTimeoutHeaders)
.body(EMPTY_BODY)
@@ -246,7 +247,12 @@ public class HttpEngine {
ResponseHeaders cachedResponseHeaders = new ResponseHeaders(uri, candidate.rawHeaders());
long now = System.currentTimeMillis();
this.responseSource = cachedResponseHeaders.chooseResponseSource(now, requestHeaders);
ResponseStrategy responseStrategy = ResponseStrategy.get(
now, cachedResponseHeaders, requestHeaders);
this.responseSource = responseStrategy.source;
this.requestHeaders = responseStrategy.request;
cachedResponseHeaders = responseStrategy.response;
if (responseSource == ResponseSource.CACHE) {
this.validatingResponse = candidate;
promoteValidatingResponse(cachedResponseHeaders);
@@ -292,6 +298,7 @@ public class HttpEngine {
}
transport = (Transport) connection.newTransport(this);
requestHeaders = transport.prepareRequestHeaders(requestHeaders);
if (hasRequestBody() && requestBodyOut == null) {
// Create a request body if we don't have one already. We'll already
@@ -332,7 +339,7 @@ public class HttpEngine {
connected(connection);
if (connection.getRoute().getProxy() != client.getProxy()) {
// Update the request line if the proxy changed; it may need a host name.
requestHeaders.getHeaders().setRequestLine(getRequestLine());
requestHeaders = requestHeaders.newBuilder().setRequestLine(getRequestLine()).build();
}
}
@@ -428,7 +435,7 @@ public class HttpEngine {
if (responseCache == null) return;
// Should we cache this response for this request?
if (!responseHeaders.isCacheable(requestHeaders)) {
if (!ResponseStrategy.isCacheable(responseHeaders, requestHeaders)) {
responseCache.maybeRemove(request());
return;
}
@@ -488,8 +495,10 @@ public class HttpEngine {
// length of the gzipped response. This isn't terribly useful and is
// dangerous because clients can query the content length, but not the
// content encoding.
responseHeaders.stripContentEncoding();
responseHeaders.stripContentLength();
responseHeaders = responseHeaders.newBuilder()
.stripContentEncoding()
.stripContentLength()
.build();
responseBodyIn = new GZIPInputStream(transferStream);
} else {
responseBodyIn = transferStream;
@@ -531,40 +540,44 @@ public class HttpEngine {
* doesn't know what content types the application is interested in.
*/
private void prepareRawRequestHeaders() throws IOException {
requestHeaders.getHeaders().setRequestLine(getRequestLine());
RequestHeaders.Builder result = requestHeaders.newBuilder();
result.setRequestLine(getRequestLine());
if (requestHeaders.getUserAgent() == null) {
requestHeaders.setUserAgent(getDefaultUserAgent());
result.setUserAgent(getDefaultUserAgent());
}
if (requestHeaders.getHost() == null) {
requestHeaders.setHost(getOriginAddress(policy.getURL()));
result.setHost(getOriginAddress(policy.getURL()));
}
if ((connection == null || connection.getHttpMinorVersion() != 0)
&& requestHeaders.getConnection() == null) {
requestHeaders.setConnection("Keep-Alive");
result.setConnection("Keep-Alive");
}
if (requestHeaders.getAcceptEncoding() == null) {
transparentGzip = true;
requestHeaders.setAcceptEncoding("gzip");
result.setAcceptEncoding("gzip");
}
if (hasRequestBody() && requestHeaders.getContentType() == null) {
requestHeaders.setContentType("application/x-www-form-urlencoded");
result.setContentType("application/x-www-form-urlencoded");
}
long ifModifiedSince = policy.getIfModifiedSince();
if (ifModifiedSince != 0) {
requestHeaders.setIfModifiedSince(new Date(ifModifiedSince));
result.setIfModifiedSince(new Date(ifModifiedSince));
}
CookieHandler cookieHandler = client.getCookieHandler();
if (cookieHandler != null) {
requestHeaders.addCookies(
result.addCookies(
cookieHandler.get(uri, requestHeaders.getHeaders().toMultimap(false)));
}
requestHeaders = result.build();
}
/**
@@ -645,7 +658,7 @@ public class HttpEngine {
*/
public final void readResponse() throws IOException {
if (hasResponse()) {
responseHeaders.setResponseSource(responseSource);
responseHeaders = responseHeaders.newBuilder().setResponseSource(responseSource).build();
return;
}
@@ -658,9 +671,11 @@ public class HttpEngine {
}
if (sentRequestMillis == -1) {
if (requestBodyOut instanceof RetryableOutputStream) {
if (requestHeaders.getContentLength() == -1
&& requestBodyOut instanceof RetryableOutputStream) {
// We might not learn the Content-Length until the request body has been buffered.
int contentLength = ((RetryableOutputStream) requestBodyOut).contentLength();
requestHeaders.setContentLength(contentLength);
requestHeaders = requestHeaders.newBuilder().setContentLength(contentLength).build();
}
transport.writeRequestHeaders();
}
@@ -674,9 +689,11 @@ public class HttpEngine {
transport.flushRequest();
responseHeaders = transport.readResponseHeaders();
responseHeaders.setLocalTimestamps(sentRequestMillis, System.currentTimeMillis());
responseHeaders.setResponseSource(responseSource);
responseHeaders = transport.readResponseHeaders()
.newBuilder()
.setLocalTimestamps(sentRequestMillis, System.currentTimeMillis())
.setResponseSource(responseSource)
.build();
if (responseSource == ResponseSource.CONDITIONAL_CACHE) {
ResponseHeaders validatingResponseHeaders = new ResponseHeaders(

View File

@@ -58,21 +58,28 @@ public final class HttpTransport implements Transport {
this.socketIn = inputStream;
}
@Override public OutputStream createRequestBody() throws IOException {
boolean chunked = httpEngine.requestHeaders.isChunked();
if (!chunked
public RequestHeaders prepareRequestHeaders(RequestHeaders requestHeaders) {
if (!httpEngine.hasRequestBody()) return requestHeaders;
if (!requestHeaders.isChunked()
&& httpEngine.policy.getChunkLength() > 0
&& httpEngine.connection.getHttpMinorVersion() != 0) {
httpEngine.requestHeaders.setChunked();
chunked = true;
return requestHeaders.newBuilder().setChunked().build();
}
long fixedContentLength = httpEngine.policy.getFixedContentLength();
if (fixedContentLength != -1) {
return requestHeaders.newBuilder().setContentLength(fixedContentLength).build();
}
return requestHeaders;
}
@Override public OutputStream createRequestBody() throws IOException {
// Stream a request body of unknown length.
if (chunked) {
if (httpEngine.requestHeaders.isChunked()) {
int chunkLength = httpEngine.policy.getChunkLength();
if (chunkLength == -1) {
chunkLength = DEFAULT_CHUNK_LENGTH;
}
if (chunkLength == -1) chunkLength = DEFAULT_CHUNK_LENGTH;
writeRequestHeaders();
return new ChunkedOutputStream(requestOut, chunkLength);
}
@@ -80,7 +87,6 @@ public final class HttpTransport implements Transport {
// Stream a request body of a known length.
long fixedContentLength = httpEngine.policy.getFixedContentLength();
if (fixedContentLength != -1) {
httpEngine.requestHeaders.setContentLength(fixedContentLength);
writeRequestHeaders();
return new FixedLengthOutputStream(requestOut, fixedContentLength);
}
@@ -132,13 +138,10 @@ public final class HttpTransport implements Transport {
}
@Override public ResponseHeaders readResponseHeaders() throws IOException {
RawHeaders rawHeaders = RawHeaders.fromBytes(socketIn);
RawHeaders rawHeaders = RawHeaders.readHttpHeaders(socketIn);
httpEngine.connection.setHttpMinorVersion(rawHeaders.getHttpMinorVersion());
httpEngine.receiveHeaders(rawHeaders);
ResponseHeaders headers = new ResponseHeaders(httpEngine.uri, rawHeaders);
headers.setTransport("http/1.1");
return headers;
return new ResponseHeaders(httpEngine.uri, rawHeaders);
}
public boolean makeReusable(boolean streamCanceled, OutputStream requestBodyOut,
@@ -469,9 +472,10 @@ public final class HttpTransport implements Transport {
}
if (bytesRemainingInChunk == 0) {
hasMoreChunks = false;
RawHeaders rawResponseHeaders = httpEngine.responseHeaders.getHeaders();
RawHeaders.readHeaders(transport.socketIn, rawResponseHeaders);
httpEngine.receiveHeaders(rawResponseHeaders);
RawHeaders trailers = new RawHeaders.Builder()
.readHeaders(transport.socketIn)
.build();
httpEngine.receiveHeaders(trailers);
endOfInput();
}
}

View File

@@ -70,7 +70,8 @@ public class HttpURLConnectionImpl extends HttpURLConnection implements Policy {
final OkHttpClient client;
private final RawHeaders rawRequestHeaders = new RawHeaders();
private RawHeaders.Builder requestHeaders = new RawHeaders.Builder();
/** Like the superclass field of the same name, but a long and available on all platforms. */
private long fixedContentLength = -1;
private int redirectionCount;
@@ -169,7 +170,7 @@ public class HttpURLConnectionImpl extends HttpURLConnection implements Policy {
throw new IllegalStateException(
"Cannot access request header fields after connection is set");
}
return rawRequestHeaders.toMultimap(false);
return requestHeaders.build().toMultimap(false);
}
@Override public final InputStream getInputStream() throws IOException {
@@ -219,10 +220,8 @@ public class HttpURLConnectionImpl extends HttpURLConnection implements Policy {
}
@Override public final String getRequestProperty(String field) {
if (field == null) {
return null;
}
return rawRequestHeaders.get(field);
if (field == null) return null;
return requestHeaders.get(field);
}
@Override public void setConnectTimeout(int timeoutMillis) {
@@ -259,19 +258,19 @@ public class HttpURLConnectionImpl extends HttpURLConnection implements Policy {
throw new ProtocolException(method + " does not support writing");
}
}
httpEngine = newHttpEngine(method, rawRequestHeaders, null, null);
httpEngine = newHttpEngine(method, null, null);
} catch (IOException e) {
httpEngineFailure = e;
throw e;
}
}
private HttpEngine newHttpEngine(String method, RawHeaders requestHeaders,
Connection connection, RetryableOutputStream requestBody) throws IOException {
private HttpEngine newHttpEngine(String method, Connection connection,
RetryableOutputStream requestBody) throws IOException {
if (url.getProtocol().equals("http")) {
return new HttpEngine(client, this, method, requestHeaders, connection, requestBody);
return new HttpEngine(client, this, method, requestHeaders.build(), connection, requestBody);
} else if (url.getProtocol().equals("https")) {
return new HttpsEngine(client, this, method, requestHeaders, connection, requestBody);
return new HttpsEngine(client, this, method, requestHeaders.build(), connection, requestBody);
} else {
throw new AssertionError();
}
@@ -313,6 +312,7 @@ public class HttpURLConnectionImpl extends HttpURLConnection implements Policy {
|| responseCode == HTTP_MOVED_TEMP
|| responseCode == HTTP_SEE_OTHER) {
retryMethod = "GET";
requestHeaders.removeAll("Content-Length");
requestBody = null;
}
@@ -325,14 +325,8 @@ public class HttpURLConnectionImpl extends HttpURLConnection implements Policy {
}
httpEngine.release(false);
httpEngine = newHttpEngine(retryMethod, rawRequestHeaders, httpEngine.getConnection(),
httpEngine = newHttpEngine(retryMethod, httpEngine.getConnection(),
(RetryableOutputStream) requestBody);
if (requestBody == null) {
// Drop the Content-Length header when redirected from POST to GET.
httpEngine.getRequestHeaders().removeContentLength();
}
}
}
@@ -382,7 +376,7 @@ public class HttpURLConnectionImpl extends HttpURLConnection implements Policy {
httpEngine.release(true);
RetryableOutputStream retryableOutputStream = (RetryableOutputStream) requestBody;
httpEngine = newHttpEngine(method, rawRequestHeaders, null, retryableOutputStream);
httpEngine = newHttpEngine(method, null, retryableOutputStream);
httpEngine.routeSelector = routeSelector; // Keep the same routeSelector.
return true;
}
@@ -423,10 +417,13 @@ public class HttpURLConnectionImpl extends HttpURLConnection implements Policy {
}
// fall-through
case HTTP_UNAUTHORIZED:
boolean credentialsFound = HttpAuthenticator.processAuthHeader(client.getAuthenticator(),
getResponseCode(), httpEngine.getResponseHeaders().getHeaders(), rawRequestHeaders,
selectedProxy, url);
return credentialsFound ? Retry.SAME_CONNECTION : Retry.NONE;
RawHeaders successorRequestHeaders = HttpAuthenticator.processAuthHeader(
client.getAuthenticator(), getResponseCode(),
httpEngine.getResponseHeaders().getHeaders(), requestHeaders.build(), selectedProxy,
url);
if (successorRequestHeaders == null) return Retry.NONE;
requestHeaders = successorRequestHeaders.newBuilder();
return Retry.SAME_CONNECTION;
case HTTP_MULT_CHOICE:
case HTTP_MOVED_PERM:
@@ -524,7 +521,7 @@ public class HttpURLConnectionImpl extends HttpURLConnection implements Policy {
if ("X-Android-Transports".equals(field)) {
setTransports(newValue, false /* append */);
} else {
rawRequestHeaders.set(field, newValue);
requestHeaders.set(field, newValue);
}
}
@@ -548,7 +545,7 @@ public class HttpURLConnectionImpl extends HttpURLConnection implements Policy {
if ("X-Android-Transports".equals(field)) {
setTransports(value, true /* append */);
} else {
rawRequestHeaders.add(field, value);
requestHeaders.add(field, value);
}
}

View File

@@ -29,7 +29,6 @@ import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
@@ -66,78 +65,20 @@ public final class RawHeaders {
}
};
private final List<String> namesAndValues = new ArrayList<String>(20);
private String requestLine;
private String statusLine;
private int httpMinorVersion = 1;
private int responseCode = -1;
private String responseMessage;
private final List<String> namesAndValues;
private final String requestLine;
private final String statusLine;
private final int httpMinorVersion;
private final int responseCode;
private final String responseMessage;
public RawHeaders() {
}
public RawHeaders(RawHeaders copyFrom) {
namesAndValues.addAll(copyFrom.namesAndValues);
requestLine = copyFrom.requestLine;
statusLine = copyFrom.statusLine;
httpMinorVersion = copyFrom.httpMinorVersion;
responseCode = copyFrom.responseCode;
responseMessage = copyFrom.responseMessage;
}
/** Sets the request line (like "GET / HTTP/1.1"). */
public void setRequestLine(String requestLine) {
requestLine = requestLine.trim();
this.requestLine = requestLine;
}
/** Sets the response status line (like "HTTP/1.0 200 OK"). */
public void setStatusLine(String statusLine) throws IOException {
// H T T P / 1 . 1 2 0 0 T e m p o r a r y R e d i r e c t
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0
if (this.responseMessage != null) {
throw new IllegalStateException("statusLine is already set");
}
// We allow empty message without leading white space since some servers
// do not send the white space when the message is empty.
boolean hasMessage = statusLine.length() > 13;
if (!statusLine.startsWith("HTTP/1.")
|| statusLine.length() < 12
|| statusLine.charAt(8) != ' '
|| (hasMessage && statusLine.charAt(12) != ' ')) {
throw new ProtocolException("Unexpected status line: " + statusLine);
}
int httpMinorVersion = statusLine.charAt(7) - '0';
if (httpMinorVersion < 0 || httpMinorVersion > 9) {
throw new ProtocolException("Unexpected status line: " + statusLine);
}
int responseCode;
try {
responseCode = Integer.parseInt(statusLine.substring(9, 12));
} catch (NumberFormatException e) {
throw new ProtocolException("Unexpected status line: " + statusLine);
}
this.responseMessage = hasMessage ? statusLine.substring(13) : "";
this.responseCode = responseCode;
this.statusLine = statusLine;
this.httpMinorVersion = httpMinorVersion;
}
/**
* @param method like "GET", "POST", "HEAD", etc.
* @param path like "/foo/bar.html"
* @param version like "HTTP/1.1"
* @param host like "www.android.com:1234"
* @param scheme like "https"
*/
public void addSpdyRequestHeaders(String method, String path, String version, String host,
String scheme) {
// TODO: populate the statusLine for the client's benefit?
add(":method", method);
add(":scheme", scheme);
add(":path", path);
add(":version", version);
add(":host", host);
private RawHeaders(Builder builder) {
this.namesAndValues = Util.immutableList(builder.namesAndValues);
this.requestLine = builder.requestLine;
this.statusLine = builder.statusLine;
this.httpMinorVersion = builder.httpMinorVersion;
this.responseCode = builder.responseCode;
this.responseMessage = builder.responseMessage;
}
public String getStatusLine() {
@@ -162,65 +103,6 @@ public final class RawHeaders {
return responseMessage;
}
/**
* 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).
*/
public void addLine(String line) {
int index = line.indexOf(":", 1);
if (index != -1) {
addLenient(line.substring(0, index), line.substring(index + 1));
} else if (line.startsWith(":")) {
addLenient("", line.substring(1)); // Empty header name.
} else {
addLenient("", line); // No header name.
}
}
/** Add a field with the specified value. */
public void add(String fieldName, String value) {
if (fieldName == null) throw new IllegalArgumentException("fieldname == null");
if (value == null) throw new IllegalArgumentException("value == null");
if (fieldName.length() == 0 || fieldName.indexOf('\0') != -1 || value.indexOf('\0') != -1) {
throw new IllegalArgumentException("Unexpected header: " + fieldName + ": " + value);
}
addLenient(fieldName, value);
}
/**
* Add a field with the specified value without any validation. Only
* appropriate for headers from the remote peer.
*/
private void addLenient(String fieldName, String value) {
namesAndValues.add(fieldName);
namesAndValues.add(value.trim());
}
public void removeAll(String fieldName) {
for (int i = 0; i < namesAndValues.size(); i += 2) {
if (fieldName.equalsIgnoreCase(namesAndValues.get(i))) {
namesAndValues.remove(i); // field name
namesAndValues.remove(i); // value
}
}
}
public void addAll(String fieldName, List<String> headerFields) {
for (String value : headerFields) {
add(fieldName, value);
}
}
/**
* Set a field with the specified value. If the field is not found, it is
* added. If the field is found, the existing values are replaced.
*/
public void set(String fieldName, String value) {
removeAll(fieldName);
add(fieldName, value);
}
/** Returns the number of field values. */
public int length() {
return namesAndValues.size() / 2;
@@ -255,12 +137,7 @@ public final class RawHeaders {
/** Returns the last value corresponding to the specified field, or null. */
public String get(String fieldName) {
for (int i = namesAndValues.size() - 2; i >= 0; i -= 2) {
if (fieldName.equalsIgnoreCase(namesAndValues.get(i))) {
return namesAndValues.get(i + 1);
}
}
return null;
return get(namesAndValues, fieldName);
}
/** Returns an immutable list of the header values for {@code name}. */
@@ -279,14 +156,14 @@ public final class RawHeaders {
/** @param fieldNames a case-insensitive set of HTTP header field names. */
public RawHeaders getAll(Set<String> fieldNames) {
RawHeaders result = new RawHeaders();
Builder result = new Builder();
for (int i = 0; i < namesAndValues.size(); i += 2) {
String fieldName = namesAndValues.get(i);
if (fieldNames.contains(fieldName)) {
result.add(fieldName, namesAndValues.get(i + 1));
}
}
return result;
return result.build();
}
/** Returns bytes of a request header for sending on an HTTP transport. */
@@ -304,23 +181,15 @@ public final class RawHeaders {
}
/** Parses bytes of a response header from an HTTP transport. */
public static RawHeaders fromBytes(InputStream in) throws IOException {
RawHeaders headers;
public static RawHeaders readHttpHeaders(InputStream in) throws IOException {
Builder builder;
do {
headers = new RawHeaders();
headers.setStatusLine(Util.readAsciiLine(in));
readHeaders(in, headers);
} while (headers.getResponseCode() == HttpEngine.HTTP_CONTINUE);
return headers;
}
/** Reads headers or trailers into {@code out}. */
public static void readHeaders(InputStream in, RawHeaders out) throws IOException {
// parse the result headers until the first blank line
String line;
while ((line = Util.readAsciiLine(in)).length() != 0) {
out.addLine(line);
}
builder = new Builder();
builder.set(ResponseHeaders.SELECTED_TRANSPORT, "http/1.1");
builder.setStatusLine(Util.readAsciiLine(in));
builder.readHeaders(in);
} while (builder.responseCode == HttpEngine.HTTP_CONTINUE);
return builder.build();
}
/**
@@ -349,29 +218,6 @@ public final class RawHeaders {
return Collections.unmodifiableMap(result);
}
/**
* Creates a new instance from the given map of fields to values. If
* present, the null field's last element will be used to set the status
* line.
*/
public static RawHeaders fromMultimap(Map<String, List<String>> map, boolean response)
throws IOException {
if (!response) throw new UnsupportedOperationException();
RawHeaders result = new RawHeaders();
for (Entry<String, List<String>> entry : map.entrySet()) {
String fieldName = entry.getKey();
List<String> values = entry.getValue();
if (fieldName != null) {
for (String value : values) {
result.addLenient(fieldName, value);
}
} else if (!values.isEmpty()) {
result.setStatusLine(values.get(values.size() - 1));
}
}
return result;
}
/**
* Returns a list of alternating names and values. Names are all lower case.
* No names are repeated. If any name has multiple values, they are
@@ -418,7 +264,8 @@ public final class RawHeaders {
}
String status = null;
String version = null;
RawHeaders result = new RawHeaders();
Builder builder = new Builder();
builder.set(ResponseHeaders.SELECTED_TRANSPORT, "spdy/3");
for (int i = 0; i < nameValueBlock.size(); i += 2) {
String name = nameValueBlock.get(i);
String values = nameValueBlock.get(i + 1);
@@ -433,15 +280,170 @@ public final class RawHeaders {
} else if (":version".equals(name)) {
version = value;
} else {
result.namesAndValues.add(name);
result.namesAndValues.add(value);
builder.namesAndValues.add(name);
builder.namesAndValues.add(value);
}
start = end + 1;
}
}
if (status == null) throw new ProtocolException("Expected ':status' header not present");
if (version == null) throw new ProtocolException("Expected ':version' header not present");
result.setStatusLine(version + " " + status);
builder.setStatusLine(version + " " + status);
return builder.build();
}
public Builder newBuilder() {
Builder result = new Builder();
result.namesAndValues.addAll(namesAndValues);
result.requestLine = requestLine;
result.statusLine = statusLine;
result.httpMinorVersion = httpMinorVersion;
result.responseCode = responseCode;
result.responseMessage = responseMessage;
return result;
}
private static String get(List<String> namesAndValues, String fieldName) {
for (int i = namesAndValues.size() - 2; i >= 0; i -= 2) {
if (fieldName.equalsIgnoreCase(namesAndValues.get(i))) {
return namesAndValues.get(i + 1);
}
}
return null;
}
public static class Builder {
private final List<String> namesAndValues = new ArrayList<String>(20);
private String requestLine;
private String statusLine;
private int httpMinorVersion = 1;
private int responseCode = -1;
private String responseMessage;
private String transport;
/** Sets the request line (like "GET / HTTP/1.1"). */
public Builder setRequestLine(String requestLine) {
this.requestLine = requestLine.trim();
return this;
}
/** Equivalent to {@code build().get(fieldName)}, but potentially faster. */
public String get(String fieldName) {
return RawHeaders.get(namesAndValues, fieldName);
}
/** Equivalent to {@code build().getResponseCode()}, but potentially faster. */
public int getResponseCode() {
return responseCode;
}
/** Sets the response status line (like "HTTP/1.0 200 OK"). */
public Builder setStatusLine(String statusLine) throws IOException {
// H T T P / 1 . 1 2 0 0 T e m p o r a r y R e d i r e c t
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0
if (this.responseMessage != null) {
throw new IllegalStateException("statusLine is already set");
}
// We allow empty message without leading white space since some servers
// do not send the white space when the message is empty.
boolean hasMessage = statusLine.length() > 13;
if (!statusLine.startsWith("HTTP/1.")
|| statusLine.length() < 12
|| statusLine.charAt(8) != ' '
|| (hasMessage && statusLine.charAt(12) != ' ')) {
throw new ProtocolException("Unexpected status line: " + statusLine);
}
int httpMinorVersion = statusLine.charAt(7) - '0';
if (httpMinorVersion < 0 || httpMinorVersion > 9) {
throw new ProtocolException("Unexpected status line: " + statusLine);
}
int responseCode;
try {
responseCode = Integer.parseInt(statusLine.substring(9, 12));
} catch (NumberFormatException e) {
throw new ProtocolException("Unexpected status line: " + statusLine);
}
this.responseMessage = hasMessage ? statusLine.substring(13) : "";
this.responseCode = responseCode;
this.statusLine = statusLine;
this.httpMinorVersion = httpMinorVersion;
return this;
}
/**
* 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).
*/
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(":")) {
return addLenient("", line.substring(1)); // Empty header name.
} else {
return addLenient("", line); // No header name.
}
}
/** Add a field with the specified value. */
public Builder add(String fieldName, String value) {
if (fieldName == null) throw new IllegalArgumentException("fieldname == null");
if (value == null) throw new IllegalArgumentException("value == null");
if (fieldName.length() == 0 || fieldName.indexOf('\0') != -1 || value.indexOf('\0') != -1) {
throw new IllegalArgumentException("Unexpected header: " + fieldName + ": " + value);
}
return addLenient(fieldName, value);
}
/**
* Add a field with the specified value without any validation. Only
* appropriate for headers from the remote peer.
*/
private Builder addLenient(String fieldName, String value) {
namesAndValues.add(fieldName);
namesAndValues.add(value.trim());
return this;
}
public Builder removeAll(String fieldName) {
for (int i = 0; i < namesAndValues.size(); i += 2) {
if (fieldName.equalsIgnoreCase(namesAndValues.get(i))) {
namesAndValues.remove(i); // field name
namesAndValues.remove(i); // value
}
}
return this;
}
public Builder addAll(String fieldName, List<String> headerFields) {
for (String value : headerFields) {
add(fieldName, value);
}
return this;
}
/**
* Set a field with the specified value. If the field is not found, it is
* added. If the field is found, the existing values are replaced.
*/
public Builder set(String fieldName, String value) {
removeAll(fieldName);
add(fieldName, value);
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;
}
public RawHeaders build() {
return new RawHeaders(this);
}
}
}

View File

@@ -197,91 +197,6 @@ public final class RequestHeaders {
return proxyAuthorization;
}
public void setChunked() {
if (this.transferEncoding != null) {
headers.removeAll("Transfer-Encoding");
}
headers.add("Transfer-Encoding", "chunked");
this.transferEncoding = "chunked";
}
public void setContentLength(long contentLength) {
if (this.contentLength != -1) {
headers.removeAll("Content-Length");
}
headers.add("Content-Length", Long.toString(contentLength));
this.contentLength = contentLength;
}
/**
* Remove the Content-Length headers. Call this when dropping the body on a
* request or response, such as when a redirect changes the method from POST
* to GET.
*/
public void removeContentLength() {
if (contentLength != -1) {
headers.removeAll("Content-Length");
contentLength = -1;
}
}
public void setUserAgent(String userAgent) {
if (this.userAgent != null) {
headers.removeAll("User-Agent");
}
headers.add("User-Agent", userAgent);
this.userAgent = userAgent;
}
public void setHost(String host) {
if (this.host != null) {
headers.removeAll("Host");
}
headers.add("Host", host);
this.host = host;
}
public void setConnection(String connection) {
if (this.connection != null) {
headers.removeAll("Connection");
}
headers.add("Connection", connection);
this.connection = connection;
}
public void setAcceptEncoding(String acceptEncoding) {
if (this.acceptEncoding != null) {
headers.removeAll("Accept-Encoding");
}
headers.add("Accept-Encoding", acceptEncoding);
this.acceptEncoding = acceptEncoding;
}
public void setContentType(String contentType) {
if (this.contentType != null) {
headers.removeAll("Content-Type");
}
headers.add("Content-Type", contentType);
this.contentType = contentType;
}
public void setIfModifiedSince(Date date) {
if (ifModifiedSince != null) {
headers.removeAll("If-Modified-Since");
}
String formattedDate = HttpDate.format(date);
headers.add("If-Modified-Since", formattedDate);
ifModifiedSince = formattedDate;
}
public void setIfNoneMatch(String ifNoneMatch) {
if (this.ifNoneMatch != null) {
headers.removeAll("If-None-Match");
}
headers.add("If-None-Match", ifNoneMatch);
this.ifNoneMatch = ifNoneMatch;
}
/**
* Returns true if the request contains conditions that save the server from
* sending a response that the client has locally. When the caller adds
@@ -291,27 +206,115 @@ public final class RequestHeaders {
return ifModifiedSince != null || ifNoneMatch != null;
}
public void addCookies(Map<String, List<String>> allCookieHeaders) {
for (Map.Entry<String, List<String>> entry : allCookieHeaders.entrySet()) {
String key = entry.getKey();
if (("Cookie".equalsIgnoreCase(key) || "Cookie2".equalsIgnoreCase(key))
&& !entry.getValue().isEmpty()) {
headers.add(key, buildCookieHeader(entry.getValue()));
}
}
public Builder newBuilder() {
return new Builder(uri, headers);
}
/**
* 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));
static class Builder {
private final URI uri;
private final RawHeaders.Builder headers;
public Builder(URI uri, RawHeaders headers) {
this.uri = uri;
this.headers = headers.newBuilder();
}
public Builder setRequestLine(String requestLine) {
headers.setRequestLine(requestLine);
return this;
}
public Builder setChunked() {
headers.set("Transfer-Encoding", "chunked");
return this;
}
public Builder setContentLength(long contentLength) {
headers.set("Content-Length", Long.toString(contentLength));
return this;
}
/**
* Remove the Content-Length headers. Call this when dropping the body on a
* request or response, such as when a redirect changes the method from POST
* to GET.
*/
public void removeContentLength() {
headers.removeAll("Content-Length");
}
public void setUserAgent(String userAgent) {
headers.set("User-Agent", userAgent);
}
public void setHost(String host) {
headers.set("Host", host);
}
public void setConnection(String connection) {
headers.set("Connection", connection);
}
public void setAcceptEncoding(String acceptEncoding) {
headers.set("Accept-Encoding", acceptEncoding);
}
public void setContentType(String contentType) {
headers.set("Content-Type", contentType);
}
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>> allCookieHeaders) {
for (Map.Entry<String, List<String>> entry : allCookieHeaders.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();
}
/**
* @param method like "GET", "POST", "HEAD", etc.
* @param path like "/foo/bar.html"
* @param version like "HTTP/1.1"
* @param host like "www.android.com:1234"
* @param scheme like "https"
*/
public Builder addSpdyRequestHeaders(
String method, String path, String version, String host, String scheme) {
// TODO: populate the statusLine for the client's benefit?
headers.add(":method", method);
headers.add(":scheme", scheme);
headers.add(":path", path);
headers.add(":version", version);
headers.add(":host", host);
return this;
}
public RequestHeaders build() {
return new RequestHeaders(uri, headers.build());
}
return sb.toString();
}
}

View File

@@ -26,7 +26,6 @@ import java.util.Collections;
import java.util.Date;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.TimeUnit;
import static com.squareup.okhttp.internal.Util.equal;
@@ -45,32 +44,32 @@ public final class ResponseHeaders {
/** HTTP synthetic header with the selected transport (spdy/3, http/1.1, etc). */
static final String SELECTED_TRANSPORT = Platform.get().getPrefix() + "-Selected-Transport";
private final URI uri;
private final RawHeaders headers;
final URI uri;
final RawHeaders headers;
/** The server's time when this response was served, if known. */
private Date servedDate;
Date servedDate;
/** The last modified date of the response, if known. */
private Date lastModified;
Date lastModified;
/**
* The expiration date of the response, if known. If both this field and the
* max age are set, the max age is preferred.
*/
private Date expires;
Date expires;
/**
* Extension header set by HttpURLConnectionImpl specifying the timestamp
* when the HTTP request was first initiated.
*/
private long sentRequestMillis;
long sentRequestMillis;
/**
* Extension header set by HttpURLConnectionImpl specifying the timestamp
* when the HTTP response was first received.
*/
private long receivedResponseMillis;
long receivedResponseMillis;
/**
* In the response, this field's name "no-cache" is misleading. It doesn't
@@ -78,23 +77,23 @@ public final class ResponseHeaders {
* the response with the origin server before returning it. We can do this
* with a conditional get.
*/
private boolean noCache;
boolean noCache;
/** If true, this response should not be cached. */
private boolean noStore;
boolean noStore;
/**
* The duration past the response's served date that it can be served
* without validation.
*/
private int maxAgeSeconds = -1;
int maxAgeSeconds = -1;
/**
* 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.
*/
private int sMaxAgeSeconds = -1;
int sMaxAgeSeconds = -1;
/**
* This request header field's name "only-if-cached" is misleading. It
@@ -103,10 +102,10 @@ public final class ResponseHeaders {
* Cached responses that would require validation (ie. conditional gets) are
* not permitted if this header is set.
*/
private boolean isPublic;
private boolean mustRevalidate;
private String etag;
private int ageSeconds = -1;
boolean isPublic;
boolean mustRevalidate;
String etag;
int ageSeconds = -1;
/** Case-insensitive set of field names. */
private Set<String> varyFields = Collections.emptySet();
@@ -191,16 +190,6 @@ public final class ResponseHeaders {
return "gzip".equalsIgnoreCase(contentEncoding);
}
public void stripContentEncoding() {
contentEncoding = null;
headers.removeAll("Content-Encoding");
}
public void stripContentLength() {
contentLength = -1;
headers.removeAll("Content-Length");
}
public boolean isChunked() {
return "chunked".equalsIgnoreCase(transferEncoding);
}
@@ -277,97 +266,6 @@ public final class ResponseHeaders {
return connection;
}
public void setLocalTimestamps(long sentRequestMillis, long receivedResponseMillis) {
this.sentRequestMillis = sentRequestMillis;
headers.add(SENT_MILLIS, Long.toString(sentRequestMillis));
this.receivedResponseMillis = receivedResponseMillis;
headers.add(RECEIVED_MILLIS, Long.toString(receivedResponseMillis));
}
public void setResponseSource(ResponseSource responseSource) {
headers.set(RESPONSE_SOURCE, responseSource.toString() + " " + headers.getResponseCode());
}
public void setTransport(String transport) {
headers.set(SELECTED_TRANSPORT, transport);
}
/**
* Returns the current age of the response, in milliseconds. The calculation
* is specified by RFC 2616, 13.2.3 Age Calculations.
*/
private long computeAge(long nowMillis) {
long apparentReceivedAge =
servedDate != null ? Math.max(0, receivedResponseMillis - servedDate.getTime()) : 0;
long receivedAge =
ageSeconds != -1 ? Math.max(apparentReceivedAge, TimeUnit.SECONDS.toMillis(ageSeconds))
: apparentReceivedAge;
long responseDuration = receivedResponseMillis - sentRequestMillis;
long residentDuration = nowMillis - receivedResponseMillis;
return receivedAge + responseDuration + residentDuration;
}
/**
* Returns the number of milliseconds that the response was fresh for,
* starting from the served date.
*/
private long computeFreshnessLifetime() {
if (maxAgeSeconds != -1) {
return TimeUnit.SECONDS.toMillis(maxAgeSeconds);
} else if (expires != null) {
long servedMillis = servedDate != null ? servedDate.getTime() : receivedResponseMillis;
long delta = expires.getTime() - servedMillis;
return delta > 0 ? delta : 0;
} else if (lastModified != null && uri.getRawQuery() == null) {
// As recommended by the HTTP RFC and implemented in Firefox, the
// max age of a document should be defaulted to 10% of the
// document's age at the time it was served. Default expiration
// dates aren't used for URIs containing a query.
long servedMillis = servedDate != null ? servedDate.getTime() : sentRequestMillis;
long delta = servedMillis - lastModified.getTime();
return delta > 0 ? (delta / 10) : 0;
}
return 0;
}
/**
* Returns true if computeFreshnessLifetime used a heuristic. If we used a
* heuristic to serve a cached response older than 24 hours, we are required
* to attach a warning.
*/
private boolean isFreshnessLifetimeHeuristic() {
return maxAgeSeconds == -1 && expires == null;
}
/**
* Returns true if this response can be stored to later serve another
* request.
*/
public boolean isCacheable(RequestHeaders request) {
// Always go to network for uncacheable response codes (RFC 2616, 13.4),
// This implementation doesn't support caching partial content.
int responseCode = headers.getResponseCode();
if (responseCode != HttpURLConnection.HTTP_OK
&& responseCode != HttpURLConnection.HTTP_NOT_AUTHORITATIVE
&& responseCode != HttpURLConnection.HTTP_MULT_CHOICE
&& responseCode != HttpURLConnection.HTTP_MOVED_PERM
&& responseCode != HttpURLConnection.HTTP_GONE) {
return false;
}
// Responses to authorized requests aren't cacheable unless they include
// a 'public', 'must-revalidate' or 's-maxage' directive.
if (request.hasAuthorization() && !isPublic && !mustRevalidate && sMaxAgeSeconds == -1) {
return false;
}
if (noStore) {
return false;
}
return true;
}
/**
* Returns true if a Vary header contains an asterisk. Such responses cannot
* be cached.
@@ -387,60 +285,6 @@ public final class ResponseHeaders {
return true;
}
/** Returns the source to satisfy {@code request} given this cached response. */
public ResponseSource chooseResponseSource(long nowMillis, RequestHeaders request) {
// If this response shouldn't have been stored, it should never be used
// as a response source. This check should be redundant as long as the
// persistence store is well-behaved and the rules are constant.
if (!isCacheable(request)) {
return ResponseSource.NETWORK;
}
if (request.isNoCache() || request.hasConditions()) {
return ResponseSource.NETWORK;
}
long ageMillis = computeAge(nowMillis);
long freshMillis = computeFreshnessLifetime();
if (request.getMaxAgeSeconds() != -1) {
freshMillis = Math.min(freshMillis, TimeUnit.SECONDS.toMillis(request.getMaxAgeSeconds()));
}
long minFreshMillis = 0;
if (request.getMinFreshSeconds() != -1) {
minFreshMillis = TimeUnit.SECONDS.toMillis(request.getMinFreshSeconds());
}
long maxStaleMillis = 0;
if (!mustRevalidate && request.getMaxStaleSeconds() != -1) {
maxStaleMillis = TimeUnit.SECONDS.toMillis(request.getMaxStaleSeconds());
}
if (!noCache && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
if (ageMillis + minFreshMillis >= freshMillis) {
headers.add("Warning", "110 HttpURLConnection \"Response is stale\"");
}
long oneDayMillis = 24 * 60 * 60 * 1000L;
if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
headers.add("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
}
return ResponseSource.CACHE;
}
if (lastModified != null) {
request.setIfModifiedSince(lastModified);
} else if (servedDate != null) {
request.setIfModifiedSince(servedDate);
}
if (etag != null) {
request.setIfNoneMatch(etag);
}
return request.hasConditions() ? ResponseSource.CONDITIONAL_CACHE : ResponseSource.NETWORK;
}
/**
* Returns true if this cached response should be used; false if the
* network response should be used.
@@ -467,7 +311,7 @@ public final class ResponseHeaders {
* 13.5.3.
*/
public ResponseHeaders combine(ResponseHeaders network) throws IOException {
RawHeaders result = new RawHeaders();
RawHeaders.Builder result = new RawHeaders.Builder();
result.setStatusLine(headers.getStatusLine());
for (int i = 0; i < headers.length(); i++) {
@@ -488,7 +332,7 @@ public final class ResponseHeaders {
}
}
return new ResponseHeaders(uri, result);
return new ResponseHeaders(uri, result.build());
}
/**
@@ -505,4 +349,48 @@ public final class ResponseHeaders {
&& !"Transfer-Encoding".equalsIgnoreCase(fieldName)
&& !"Upgrade".equalsIgnoreCase(fieldName);
}
public Builder newBuilder() {
return new Builder(uri, headers);
}
static class Builder {
private final URI uri;
private final RawHeaders.Builder headers;
public Builder(URI uri, RawHeaders headers) {
this.uri = uri;
this.headers = headers.newBuilder();
}
public Builder stripContentEncoding() {
headers.removeAll("Content-Encoding");
return this;
}
public Builder stripContentLength() {
headers.removeAll("Content-Length");
return this;
}
public Builder setLocalTimestamps(long sentRequestMillis, long receivedResponseMillis) {
headers.set(SENT_MILLIS, Long.toString(sentRequestMillis));
headers.set(RECEIVED_MILLIS, Long.toString(receivedResponseMillis));
return this;
}
public Builder setResponseSource(ResponseSource responseSource) {
headers.set(RESPONSE_SOURCE, responseSource.toString() + " " + headers.getResponseCode());
return this;
}
public Builder addWarning(String message) {
headers.add("Warning", message);
return this;
}
public ResponseHeaders build() {
return new ResponseHeaders(uri, headers.build());
}
}
}

View File

@@ -0,0 +1,172 @@
package com.squareup.okhttp.internal.http;
import com.squareup.okhttp.ResponseSource;
import java.net.HttpURLConnection;
import java.util.concurrent.TimeUnit;
/**
* Given a request and cached response, this figures out the next action. It
* may also update the request to add conditions, or the response to add
* warnings.
*/
public final class ResponseStrategy {
public final RequestHeaders request;
public final ResponseHeaders response;
public final ResponseSource source;
private ResponseStrategy(
RequestHeaders request, ResponseHeaders response, ResponseSource source) {
this.request = request;
this.response = response;
this.source = source;
}
/**
* Returns the current age of the response, in milliseconds. The calculation
* is specified by RFC 2616, 13.2.3 Age Calculations.
*/
private static long computeAge(ResponseHeaders response, long nowMillis) {
long apparentReceivedAge = response.servedDate != null
? Math.max(0, response.receivedResponseMillis - response.servedDate.getTime())
: 0;
long receivedAge = response.ageSeconds != -1
? Math.max(apparentReceivedAge, TimeUnit.SECONDS.toMillis(response.ageSeconds))
: apparentReceivedAge;
long responseDuration = response.receivedResponseMillis - response.sentRequestMillis;
long residentDuration = nowMillis - response.receivedResponseMillis;
return receivedAge + responseDuration + residentDuration;
}
/**
* Returns the number of milliseconds that the response was fresh for,
* starting from the served date.
*/
private static long computeFreshnessLifetime(ResponseHeaders response) {
if (response.maxAgeSeconds != -1) {
return TimeUnit.SECONDS.toMillis(response.maxAgeSeconds);
} else if (response.expires != null) {
long servedMillis = response.servedDate != null
? response.servedDate.getTime()
: response.receivedResponseMillis;
long delta = response.expires.getTime() - servedMillis;
return delta > 0 ? delta : 0;
} else if (response.lastModified != null && response.uri.getRawQuery() == null) {
// As recommended by the HTTP RFC and implemented in Firefox, the
// max age of a document should be defaulted to 10% of the
// document's age at the time it was served. Default expiration
// dates aren't used for URIs containing a query.
long servedMillis = response.servedDate != null
? response.servedDate.getTime()
: response.sentRequestMillis;
long delta = servedMillis - response.lastModified.getTime();
return delta > 0 ? (delta / 10) : 0;
}
return 0;
}
/**
* Returns true if computeFreshnessLifetime used a heuristic. If we used a
* heuristic to serve a cached response older than 24 hours, we are required
* to attach a warning.
*/
private static boolean isFreshnessLifetimeHeuristic(ResponseHeaders response) {
return response.maxAgeSeconds == -1 && response.expires == null;
}
/**
* Returns true if this response can be stored to later serve another
* request.
*/
public static boolean isCacheable(ResponseHeaders response, RequestHeaders request) {
// Always go to network for uncacheable response codes (RFC 2616, 13.4),
// This implementation doesn't support caching partial content.
int responseCode = response.headers.getResponseCode();
if (responseCode != HttpURLConnection.HTTP_OK
&& responseCode != HttpURLConnection.HTTP_NOT_AUTHORITATIVE
&& responseCode != HttpURLConnection.HTTP_MULT_CHOICE
&& responseCode != HttpURLConnection.HTTP_MOVED_PERM
&& responseCode != HttpURLConnection.HTTP_GONE) {
return false;
}
// Responses to authorized requests aren't cacheable unless they include
// a 'public', 'must-revalidate' or 's-maxage' directive.
if (request.hasAuthorization()
&& !response.isPublic
&& !response.mustRevalidate
&& response.sMaxAgeSeconds == -1) {
return false;
}
if (response.noStore) {
return false;
}
return true;
}
/**
* Returns a strategy to satisfy {@code request} using the a cached response
* {@code response}.
*/
public static ResponseStrategy get(
long nowMillis, ResponseHeaders response, RequestHeaders request) {
// If this response shouldn't have been stored, it should never be used
// as a response source. This check should be redundant as long as the
// persistence store is well-behaved and the rules are constant.
if (!isCacheable(response, request)) {
return new ResponseStrategy(request, response, ResponseSource.NETWORK);
}
if (request.isNoCache() || request.hasConditions()) {
return new ResponseStrategy(request, response, ResponseSource.NETWORK);
}
long ageMillis = computeAge(response, nowMillis);
long freshMillis = computeFreshnessLifetime(response);
if (request.getMaxAgeSeconds() != -1) {
freshMillis = Math.min(freshMillis, TimeUnit.SECONDS.toMillis(request.getMaxAgeSeconds()));
}
long minFreshMillis = 0;
if (request.getMinFreshSeconds() != -1) {
minFreshMillis = TimeUnit.SECONDS.toMillis(request.getMinFreshSeconds());
}
long maxStaleMillis = 0;
if (!response.mustRevalidate && request.getMaxStaleSeconds() != -1) {
maxStaleMillis = TimeUnit.SECONDS.toMillis(request.getMaxStaleSeconds());
}
if (!response.noCache && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
ResponseHeaders.Builder builder = response.newBuilder();
if (ageMillis + minFreshMillis >= freshMillis) {
builder.addWarning("110 HttpURLConnection \"Response is stale\"");
}
long oneDayMillis = 24 * 60 * 60 * 1000L;
if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic(response)) {
builder.addWarning("113 HttpURLConnection \"Heuristic expiration\"");
}
return new ResponseStrategy(request, builder.build(), ResponseSource.CACHE);
}
RequestHeaders.Builder conditionalRequestBuilder = request.newBuilder();
if (response.lastModified != null) {
conditionalRequestBuilder.setIfModifiedSince(response.lastModified);
} else if (response.servedDate != null) {
conditionalRequestBuilder.setIfModifiedSince(response.servedDate);
}
if (response.etag != null) {
conditionalRequestBuilder.setIfNoneMatch(response.etag);
}
RequestHeaders conditionalRequest = conditionalRequestBuilder.build();
ResponseSource responseSource = conditionalRequest.hasConditions()
? ResponseSource.CONDITIONAL_CACHE
: ResponseSource.NETWORK;
return new ResponseStrategy(conditionalRequest, response, responseSource);
}
}

View File

@@ -36,30 +36,38 @@ public final class SpdyTransport implements Transport {
this.spdyConnection = spdyConnection;
}
@Override public OutputStream createRequestBody() throws IOException {
long fixedContentLength = httpEngine.policy.getFixedContentLength();
if (fixedContentLength != -1) {
httpEngine.requestHeaders.setContentLength(fixedContentLength);
@Override public RequestHeaders prepareRequestHeaders(RequestHeaders requestHeaders) {
RequestHeaders.Builder builder = requestHeaders.newBuilder();
String version = httpEngine.connection.getHttpMinorVersion() == 1 ? "HTTP/1.1" : "HTTP/1.0";
URL url = httpEngine.policy.getURL();
builder.addSpdyRequestHeaders(httpEngine.method, HttpEngine.requestPath(url), version,
HttpEngine.getOriginAddress(url), httpEngine.uri.getScheme());
if (httpEngine.hasRequestBody()) {
long fixedContentLength = httpEngine.policy.getFixedContentLength();
if (fixedContentLength != -1) {
builder.setContentLength(fixedContentLength);
}
}
return builder.build();
}
@Override public OutputStream createRequestBody() throws IOException {
// TODO: if we aren't streaming up to the server, we should buffer the whole request
writeRequestHeaders();
return stream.getOutputStream();
}
@Override public void writeRequestHeaders() throws IOException {
if (stream != null) {
return;
}
if (stream != null) return;
httpEngine.writingRequestHeaders();
RawHeaders requestHeaders = httpEngine.requestHeaders.getHeaders();
String version = httpEngine.connection.getHttpMinorVersion() == 1 ? "HTTP/1.1" : "HTTP/1.0";
URL url = httpEngine.policy.getURL();
requestHeaders.addSpdyRequestHeaders(httpEngine.method, HttpEngine.requestPath(url), version,
HttpEngine.getOriginAddress(url), httpEngine.uri.getScheme());
boolean hasRequestBody = httpEngine.hasRequestBody();
boolean hasResponseBody = true;
stream = spdyConnection.newStream(requestHeaders.toNameValueBlock(), hasRequestBody,
hasResponseBody);
stream = spdyConnection.newStream(httpEngine.requestHeaders.getHeaders().toNameValueBlock(),
hasRequestBody, hasResponseBody);
stream.setReadTimeout(httpEngine.client.getReadTimeout());
}
@@ -75,10 +83,7 @@ public final class SpdyTransport implements Transport {
List<String> nameValueBlock = stream.getResponseHeaders();
RawHeaders rawHeaders = RawHeaders.fromNameValueBlock(nameValueBlock);
httpEngine.receiveHeaders(rawHeaders);
ResponseHeaders headers = new ResponseHeaders(httpEngine.uri, rawHeaders);
headers.setTransport("spdy/3");
return headers;
return new ResponseHeaders(httpEngine.uri, rawHeaders);
}
@Override public InputStream getTransferStream(CacheRequest cacheRequest) throws IOException {

View File

@@ -22,6 +22,13 @@ import java.io.OutputStream;
import java.net.CacheRequest;
interface Transport {
/**
* Returns headers equivalent to {@code requestHeaders} but with
* transport-specific changes. For example, this may set a Transfer-Encoding
* header if it is required but not present for the current transport.
*/
RequestHeaders prepareRequestHeaders(RequestHeaders requestHeaders);
/**
* Returns an output stream where the request body can be written. The
* returned stream will of one of two types:

View File

@@ -62,7 +62,7 @@ public final class AsyncApiTest {
.build();
client.enqueue(request, receiver);
receiver.await(request)
receiver.await(request.url())
.assertCode(200)
.assertContainsHeaders("Content-Type: text/plain")
.assertBody("abc");
@@ -83,7 +83,7 @@ public final class AsyncApiTest {
Request request = new Request.Builder(server.getUrl("/")).build();
client.enqueue(request, receiver);
receiver.await(request).assertHandshake();
receiver.await(request.url()).assertHandshake();
}
@Test public void post() throws Exception {
@@ -95,7 +95,7 @@ public final class AsyncApiTest {
.build();
client.enqueue(request, receiver);
receiver.await(request)
receiver.await(request.url())
.assertCode(200)
.assertBody("abc");
@@ -114,12 +114,12 @@ public final class AsyncApiTest {
Request request1 = new Request.Builder(server.getUrl("/")).build();
client.enqueue(request1, receiver);
receiver.await(request1).assertCode(200).assertBody("A");
receiver.await(request1.url()).assertCode(200).assertBody("A");
assertNull(server.takeRequest().getHeader("If-None-Match"));
Request request2 = new Request.Builder(server.getUrl("/")).build();
client.enqueue(request2, receiver);
receiver.await(request2).assertCode(200).assertBody("A");
receiver.await(request2.url()).assertCode(200).assertBody("A");
assertEquals("v1", server.takeRequest().getHeader("If-None-Match"));
}
}

View File

@@ -17,6 +17,7 @@ package com.squareup.okhttp;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
@@ -29,8 +30,8 @@ import java.util.concurrent.TimeUnit;
public class RecordingReceiver implements Response.Receiver {
public static final long TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(10);
private final Map<Request, ByteArrayOutputStream> inFlightResponses
= new LinkedHashMap<Request, ByteArrayOutputStream>();
private final Map<Response, ByteArrayOutputStream> inFlightResponses
= new LinkedHashMap<Response, ByteArrayOutputStream>();
private final List<RecordedResponse> responses = new ArrayList<RecordedResponse>();
@Override public synchronized void onFailure(Failure failure) {
@@ -39,10 +40,10 @@ public class RecordingReceiver implements Response.Receiver {
}
@Override public synchronized boolean onResponse(Response response) throws IOException {
ByteArrayOutputStream out = inFlightResponses.get(response.request());
ByteArrayOutputStream out = inFlightResponses.get(response);
if (out == null) {
out = new ByteArrayOutputStream();
inFlightResponses.put(response.request(), out);
inFlightResponses.put(response, out);
}
byte[] buffer = new byte[1024];
@@ -52,7 +53,7 @@ public class RecordingReceiver implements Response.Receiver {
int c = body.byteStream().read(buffer);
if (c == -1) {
inFlightResponses.remove(response.request());
inFlightResponses.remove(response);
responses.add(new RecordedResponse(
response.request(), response, out.toString("UTF-8"), null));
notifyAll();
@@ -69,11 +70,11 @@ public class RecordingReceiver implements Response.Receiver {
* Returns the recorded response triggered by {@code request}. Throws if the
* response isn't enqueued before the timeout.
*/
public synchronized RecordedResponse await(Request request) throws Exception {
public synchronized RecordedResponse await(URL url) throws Exception {
long timeoutMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) + TIMEOUT_MILLIS;
while (true) {
for (RecordedResponse recordedResponse : responses) {
if (recordedResponse.request == request) {
if (recordedResponse.request.url().equals(url)) {
return recordedResponse;
}
}
@@ -83,6 +84,6 @@ public class RecordingReceiver implements Response.Receiver {
wait(timeoutMillis - nowMillis);
}
throw new AssertionError("Timed out waiting for response to " + request);
throw new AssertionError("Timed out waiting for response to " + url);
}
}

View File

@@ -774,7 +774,7 @@ public final class HttpResponseCacheTest {
URLConnection request2 = openConnection(url);
if (expectCached) {
assertEquals("1", request1.getHeaderField("X-Response-ID"));
assertEquals("1", request2.getHeaderField("X-Response-ID"));
} else {
assertEquals("2", request2.getHeaderField("X-Response-ID"));
}

View File

@@ -31,57 +31,63 @@ public final class RawHeadersTest {
":status", "200 OK",
":version", "HTTP/1.1");
RawHeaders rawHeaders = RawHeaders.fromNameValueBlock(nameValueBlock);
assertEquals(3, rawHeaders.length());
assertEquals(4, rawHeaders.length());
assertEquals("HTTP/1.1 200 OK", rawHeaders.getStatusLine());
assertEquals("no-cache, no-store", rawHeaders.get("cache-control"));
assertEquals("Cookie2", rawHeaders.get("set-cookie"));
assertEquals("cache-control", rawHeaders.getFieldName(0));
assertEquals("no-cache, no-store", rawHeaders.getValue(0));
assertEquals("set-cookie", rawHeaders.getFieldName(1));
assertEquals("Cookie1", rawHeaders.getValue(1));
assertEquals("spdy/3", rawHeaders.get(ResponseHeaders.SELECTED_TRANSPORT));
assertEquals(ResponseHeaders.SELECTED_TRANSPORT, rawHeaders.getFieldName(0));
assertEquals("spdy/3", rawHeaders.getValue(0));
assertEquals("cache-control", rawHeaders.getFieldName(1));
assertEquals("no-cache, no-store", rawHeaders.getValue(1));
assertEquals("set-cookie", rawHeaders.getFieldName(2));
assertEquals("Cookie2", rawHeaders.getValue(2));
assertEquals("Cookie1", rawHeaders.getValue(2));
assertEquals("set-cookie", rawHeaders.getFieldName(3));
assertEquals("Cookie2", rawHeaders.getValue(3));
assertNull(rawHeaders.get(":status"));
assertNull(rawHeaders.get(":version"));
}
@Test public void toNameValueBlock() {
RawHeaders rawHeaders = new RawHeaders();
rawHeaders.add("cache-control", "no-cache, no-store");
rawHeaders.add("set-cookie", "Cookie1");
rawHeaders.add("set-cookie", "Cookie2");
rawHeaders.add(":status", "200 OK");
RawHeaders.Builder builder = new RawHeaders.Builder();
builder.add("cache-control", "no-cache, no-store");
builder.add("set-cookie", "Cookie1");
builder.add("set-cookie", "Cookie2");
builder.add(":status", "200 OK");
// TODO: fromNameValueBlock should take the status line headers
List<String> nameValueBlock = rawHeaders.toNameValueBlock();
List<String> expected =
Arrays.asList("cache-control", "no-cache, no-store", "set-cookie", "Cookie1\u0000Cookie2",
":status", "200 OK");
List<String> nameValueBlock = builder.build().toNameValueBlock();
List<String> expected = Arrays.asList(
"cache-control", "no-cache, no-store",
"set-cookie", "Cookie1\u0000Cookie2",
":status", "200 OK");
assertEquals(expected, nameValueBlock);
}
@Test public void toNameValueBlockDropsForbiddenHeaders() {
RawHeaders rawHeaders = new RawHeaders();
rawHeaders.add("Connection", "close");
rawHeaders.add("Transfer-Encoding", "chunked");
assertEquals(Arrays.<String>asList(), rawHeaders.toNameValueBlock());
RawHeaders.Builder builder = new RawHeaders.Builder();
builder.add("Connection", "close");
builder.add("Transfer-Encoding", "chunked");
assertEquals(Arrays.<String>asList(), builder.build().toNameValueBlock());
}
@Test public void statusMessage() throws IOException {
RawHeaders rawHeaders = new RawHeaders();
final String message = "Temporary Redirect";
final int version = 1;
final int code = 200;
rawHeaders.setStatusLine("HTTP/1." + version + " " + code + " " + message);
RawHeaders.Builder builder = new RawHeaders.Builder();
String message = "Temporary Redirect";
int version = 1;
int code = 200;
builder.setStatusLine("HTTP/1." + version + " " + code + " " + message);
RawHeaders rawHeaders = builder.build();
assertEquals(message, rawHeaders.getResponseMessage());
assertEquals(version, rawHeaders.getHttpMinorVersion());
assertEquals(code, rawHeaders.getResponseCode());
}
@Test public void statusMessageWithEmptyMessage() throws IOException {
RawHeaders rawHeaders = new RawHeaders();
final int version = 1;
final int code = 503;
rawHeaders.setStatusLine("HTTP/1." + version + " " + code + " ");
RawHeaders.Builder builder = new RawHeaders.Builder();
int version = 1;
int code = 503;
builder.setStatusLine("HTTP/1." + version + " " + code + " ");
RawHeaders rawHeaders = builder.build();
assertEquals("", rawHeaders.getResponseMessage());
assertEquals(version, rawHeaders.getHttpMinorVersion());
assertEquals(code, rawHeaders.getResponseCode());
@@ -93,10 +99,11 @@ public final class RawHeadersTest {
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1
*/
@Test public void statusMessageWithEmptyMessageAndNoLeadingSpace() throws IOException {
RawHeaders rawHeaders = new RawHeaders();
final int version = 1;
final int code = 503;
rawHeaders.setStatusLine("HTTP/1." + version + " " + code);
RawHeaders.Builder builder = new RawHeaders.Builder();
int version = 1;
int code = 503;
builder.setStatusLine("HTTP/1." + version + " " + code);
RawHeaders rawHeaders = builder.build();
assertEquals("", rawHeaders.getResponseMessage());
assertEquals(version, rawHeaders.getHttpMinorVersion());
assertEquals(code, rawHeaders.getResponseCode());

View File

@@ -213,12 +213,14 @@ public final class URLConnectionTest {
fail("Modified an unmodifiable view.");
} catch (UnsupportedOperationException expected) {
}
assertEquals("A", urlConnection.getHeaderFieldKey(0));
assertEquals("c", urlConnection.getHeaderField(0));
assertEquals("B", urlConnection.getHeaderFieldKey(1));
assertEquals("d", urlConnection.getHeaderField(1));
assertEquals("A", urlConnection.getHeaderFieldKey(2));
assertEquals("e", urlConnection.getHeaderField(2));
assertEquals(ResponseHeaders.SELECTED_TRANSPORT, urlConnection.getHeaderFieldKey(0));
assertEquals("http/1.1", urlConnection.getHeaderField(0));
assertEquals("A", urlConnection.getHeaderFieldKey(1));
assertEquals("c", urlConnection.getHeaderField(1));
assertEquals("B", urlConnection.getHeaderFieldKey(2));
assertEquals("d", urlConnection.getHeaderField(2));
assertEquals("A", urlConnection.getHeaderFieldKey(3));
assertEquals("e", urlConnection.getHeaderField(3));
}
@Test public void serverSendsInvalidResponseHeaders() throws Exception {