aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorElliott Hughes <enh@google.com>2013-07-18 16:33:40 +0000
committerGerrit Code Review <noreply-gerritcodereview@google.com>2013-07-18 16:33:40 +0000
commit4d31eb4667a86cca81ecf5943b0b61cd1e8d1fae (patch)
treee508857d2203d128b780e594f3a6a63d52f18cbf
parent00834c9a00d53b29c6e486cb0b6f80fd505416e1 (diff)
parenta82f42bbeedd0b07f3892f3b0efaa8122dc8f264 (diff)
downloadokhttp-4d31eb4667a86cca81ecf5943b0b61cd1e8d1fae.tar.gz
Merge "Update okhttp to commit abc8c9a30bc0c5a9a"
-rw-r--r--android/main/java/com/squareup/okhttp/internal/Platform.java12
-rw-r--r--src/main/java/com/squareup/okhttp/Connection.java2
-rw-r--r--src/main/java/com/squareup/okhttp/Failure.java59
-rw-r--r--src/main/java/com/squareup/okhttp/HttpResponseCache.java44
-rw-r--r--src/main/java/com/squareup/okhttp/MediaType.java120
-rw-r--r--src/main/java/com/squareup/okhttp/OkHttpClient.java115
-rw-r--r--src/main/java/com/squareup/okhttp/OkResponseCache.java (renamed from src/main/java/com/squareup/okhttp/internal/http/OkResponseCache.java)11
-rw-r--r--src/main/java/com/squareup/okhttp/Request.java270
-rw-r--r--src/main/java/com/squareup/okhttp/Response.java217
-rw-r--r--src/main/java/com/squareup/okhttp/Route.java6
-rw-r--r--src/main/java/com/squareup/okhttp/RouteDatabase.java57
-rw-r--r--src/main/java/com/squareup/okhttp/internal/Platform.java27
-rw-r--r--src/main/java/com/squareup/okhttp/internal/StrictLineReader.java3
-rw-r--r--src/main/java/com/squareup/okhttp/internal/http/Dispatcher.java83
-rw-r--r--src/main/java/com/squareup/okhttp/internal/http/HttpDate.java28
-rw-r--r--src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java81
-rw-r--r--src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java15
-rw-r--r--src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java140
-rw-r--r--src/main/java/com/squareup/okhttp/internal/http/HttpsEngine.java71
-rw-r--r--src/main/java/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java227
-rw-r--r--src/main/java/com/squareup/okhttp/internal/http/Job.java88
-rw-r--r--src/main/java/com/squareup/okhttp/internal/http/OkResponseCacheAdapter.java4
-rw-r--r--src/main/java/com/squareup/okhttp/internal/http/Policy.java42
-rw-r--r--src/main/java/com/squareup/okhttp/internal/http/RawHeaders.java24
-rw-r--r--src/main/java/com/squareup/okhttp/internal/http/RequestHeaders.java8
-rw-r--r--src/main/java/com/squareup/okhttp/internal/http/ResponseHeaders.java9
-rw-r--r--src/main/java/com/squareup/okhttp/internal/http/RouteSelector.java18
-rw-r--r--src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java2
-rw-r--r--src/main/java/com/squareup/okhttp/internal/spdy/SpdyConnection.java5
-rw-r--r--src/main/java/com/squareup/okhttp/internal/spdy/SpdyReader.java4
-rw-r--r--src/main/java/com/squareup/okhttp/internal/spdy/SpdyStream.java4
-rw-r--r--src/test/java/com/squareup/okhttp/MediaTypeTest.java151
-rw-r--r--src/test/java/com/squareup/okhttp/RequestTest.java90
-rw-r--r--src/test/java/com/squareup/okhttp/internal/AsyncApiTest.java75
-rw-r--r--src/test/java/com/squareup/okhttp/internal/RecordedResponse.java64
-rw-r--r--src/test/java/com/squareup/okhttp/internal/RecordingReceiver.java65
-rw-r--r--src/test/java/com/squareup/okhttp/internal/http/HttpResponseCacheTest.java22
-rw-r--r--src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java52
-rw-r--r--src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java34
-rw-r--r--src/test/java/com/squareup/okhttp/internal/spdy/MockSpdyPeer.java5
40 files changed, 1932 insertions, 422 deletions
diff --git a/android/main/java/com/squareup/okhttp/internal/Platform.java b/android/main/java/com/squareup/okhttp/internal/Platform.java
index db633c6..0a95f07 100644
--- a/android/main/java/com/squareup/okhttp/internal/Platform.java
+++ b/android/main/java/com/squareup/okhttp/internal/Platform.java
@@ -17,7 +17,9 @@
package com.squareup.okhttp.internal;
import dalvik.system.SocketTagger;
+import java.io.IOException;
import java.io.OutputStream;
+import java.net.InetSocketAddress;
import java.net.NetworkInterface;
import java.net.Socket;
import java.net.SocketException;
@@ -123,4 +125,14 @@ public final class Platform {
return DEFAULT_MTU;
}
}
+
+ public void connectSocket(Socket socket, InetSocketAddress address,
+ int connectTimeout) throws IOException {
+ socket.connect(address, connectTimeout);
+ }
+
+ /** Prefix used on custom headers. */
+ public String getPrefix() {
+ return "X-Android";
+ }
}
diff --git a/src/main/java/com/squareup/okhttp/Connection.java b/src/main/java/com/squareup/okhttp/Connection.java
index 5ec99cc..73c4b56 100644
--- a/src/main/java/com/squareup/okhttp/Connection.java
+++ b/src/main/java/com/squareup/okhttp/Connection.java
@@ -97,7 +97,7 @@ public final class Connection implements Closeable {
}
connected = true;
socket = (route.proxy.type() != Proxy.Type.HTTP) ? new Socket(route.proxy) : new Socket();
- socket.connect(route.inetSocketAddress, connectTimeout);
+ Platform.get().connectSocket(socket, route.inetSocketAddress, connectTimeout);
socket.setSoTimeout(readTimeout);
in = socket.getInputStream();
out = socket.getOutputStream();
diff --git a/src/main/java/com/squareup/okhttp/Failure.java b/src/main/java/com/squareup/okhttp/Failure.java
new file mode 100644
index 0000000..b40133b
--- /dev/null
+++ b/src/main/java/com/squareup/okhttp/Failure.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp;
+
+/**
+ * A failure attempting to retrieve an HTTP response.
+ *
+ * <h3>Warning: Experimental OkHttp 2.0 API</h3>
+ * This class is in beta. APIs are subject to change!
+ */
+public class Failure {
+ private final Request request;
+ private final Throwable exception;
+
+ private Failure(Builder builder) {
+ this.request = builder.request;
+ this.exception = builder.exception;
+ }
+
+ public Request request() {
+ return request;
+ }
+
+ public Throwable exception() {
+ return exception;
+ }
+
+ public static class Builder {
+ private Request request;
+ private Throwable exception;
+
+ public Builder request(Request request) {
+ this.request = request;
+ return this;
+ }
+
+ public Builder exception(Throwable exception) {
+ this.exception = exception;
+ return this;
+ }
+
+ public Failure build() {
+ return new Failure(this);
+ }
+ }
+}
diff --git a/src/main/java/com/squareup/okhttp/HttpResponseCache.java b/src/main/java/com/squareup/okhttp/HttpResponseCache.java
index ce68e72..a1c653c 100644
--- a/src/main/java/com/squareup/okhttp/HttpResponseCache.java
+++ b/src/main/java/com/squareup/okhttp/HttpResponseCache.java
@@ -22,8 +22,8 @@ import com.squareup.okhttp.internal.StrictLineReader;
import com.squareup.okhttp.internal.Util;
import com.squareup.okhttp.internal.http.HttpEngine;
import com.squareup.okhttp.internal.http.HttpURLConnectionImpl;
+import com.squareup.okhttp.internal.http.HttpsEngine;
import com.squareup.okhttp.internal.http.HttpsURLConnectionImpl;
-import com.squareup.okhttp.internal.http.OkResponseCache;
import com.squareup.okhttp.internal.http.RawHeaders;
import com.squareup.okhttp.internal.http.ResponseHeaders;
import java.io.BufferedWriter;
@@ -153,6 +153,10 @@ public final class HttpResponseCache extends ResponseCache {
return HttpResponseCache.this.put(uri, connection);
}
+ @Override public void maybeRemove(String requestMethod, URI uri) throws IOException {
+ HttpResponseCache.this.maybeRemove(requestMethod, uri);
+ }
+
@Override public void update(
CacheResponse conditionalCacheHit, HttpURLConnection connection) throws IOException {
HttpResponseCache.this.update(conditionalCacheHit, connection);
@@ -226,17 +230,11 @@ public final class HttpResponseCache extends ResponseCache {
HttpURLConnection httpConnection = (HttpURLConnection) urlConnection;
String requestMethod = httpConnection.getRequestMethod();
- String key = uriToKey(uri);
- if (requestMethod.equals("POST") || requestMethod.equals("PUT") || requestMethod.equals(
- "DELETE")) {
- try {
- cache.remove(key);
- } catch (IOException ignored) {
- // The cache cannot be written.
- }
+ if (maybeRemove(requestMethod, uri)) {
return null;
- } else if (!requestMethod.equals("GET")) {
+ }
+ if (!requestMethod.equals("GET")) {
// Don't cache non-GET responses. We're technically allowed to cache
// HEAD requests and some POST requests, but the complexity of doing
// so is high and the benefit is low.
@@ -259,7 +257,7 @@ public final class HttpResponseCache extends ResponseCache {
Entry entry = new Entry(uri, varyHeaders, httpConnection);
DiskLruCache.Editor editor = null;
try {
- editor = cache.edit(key);
+ editor = cache.edit(uriToKey(uri));
if (editor == null) {
return null;
}
@@ -271,6 +269,23 @@ public final class HttpResponseCache extends ResponseCache {
}
}
+ /**
+ * Returns true if the supplied {@code requestMethod} potentially invalidates an entry in the
+ * cache.
+ */
+ private boolean maybeRemove(String requestMethod, URI uri) {
+ if (requestMethod.equals("POST") || requestMethod.equals("PUT") || requestMethod.equals(
+ "DELETE")) {
+ try {
+ cache.remove(uriToKey(uri));
+ } catch (IOException ignored) {
+ // The cache cannot be written.
+ }
+ return true;
+ }
+ return false;
+ }
+
private void update(CacheResponse conditionalCacheHit, HttpURLConnection httpConnection)
throws IOException {
HttpEngine httpEngine = getHttpEngine(httpConnection);
@@ -407,8 +422,7 @@ public final class HttpResponseCache extends ResponseCache {
editor.commit();
}
- @Override
- public void write(byte[] buffer, int offset, int length) throws IOException {
+ @Override public void write(byte[] buffer, int offset, int length) throws IOException {
// Since we don't override "write(int oneByte)", we can write directly to "out"
// and avoid the inefficient implementation from the FilterOutputStream.
out.write(buffer, offset, length);
@@ -565,8 +579,8 @@ public final class HttpResponseCache extends ResponseCache {
HttpEngine engine = httpConnection instanceof HttpsURLConnectionImpl
? ((HttpsURLConnectionImpl) httpConnection).getHttpEngine()
: ((HttpURLConnectionImpl) httpConnection).getHttpEngine();
- return engine instanceof HttpsURLConnectionImpl.HttpsEngine
- ? ((HttpsURLConnectionImpl.HttpsEngine) engine).getSslSocket()
+ return engine instanceof HttpsEngine
+ ? ((HttpsEngine) engine).getSslSocket()
: null;
}
diff --git a/src/main/java/com/squareup/okhttp/MediaType.java b/src/main/java/com/squareup/okhttp/MediaType.java
new file mode 100644
index 0000000..2c09596
--- /dev/null
+++ b/src/main/java/com/squareup/okhttp/MediaType.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp;
+
+import java.nio.charset.Charset;
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * An <a href="http://tools.ietf.org/html/rfc2045">RFC 2045</a> Media Type,
+ * appropriate to describe the content type of an HTTP request or response body.
+ */
+public final class MediaType {
+ private static final String TOKEN = "([a-zA-Z0-9-!#$%&'*+.^_`{|}~]+)";
+ private static final String QUOTED = "\"([^\"]*)\"";
+ private static final Pattern TYPE_SUBTYPE = Pattern.compile(TOKEN + "/" + TOKEN);
+ private static final Pattern PARAMETER = Pattern.compile(
+ ";\\s*" + TOKEN + "=(?:" + TOKEN + "|" + QUOTED + ")");
+
+ private final String mediaType;
+ private final String type;
+ private final String subtype;
+ private final String charset;
+
+ private MediaType(String mediaType, String type, String subtype, String charset) {
+ this.mediaType = mediaType;
+ this.type = type;
+ this.subtype = subtype;
+ this.charset = charset;
+ }
+
+ /**
+ * Returns a media type for {@code string}, or null if {@code string} is not a
+ * well-formed media type.
+ */
+ public static MediaType parse(String string) {
+ Matcher typeSubtype = TYPE_SUBTYPE.matcher(string);
+ if (!typeSubtype.lookingAt()) return null;
+ String type = typeSubtype.group(1).toLowerCase(Locale.US);
+ String subtype = typeSubtype.group(2).toLowerCase(Locale.US);
+
+ String charset = null;
+ Matcher parameter = PARAMETER.matcher(string);
+ for (int s = typeSubtype.end(); s < string.length(); s = parameter.end()) {
+ parameter.region(s, string.length());
+ if (!parameter.lookingAt()) return null; // This is not a well-formed media type.
+
+ String name = parameter.group(1);
+ if (name == null || !name.equalsIgnoreCase("charset")) continue;
+ if (charset != null) throw new IllegalArgumentException("Multiple charsets: " + string);
+ charset = parameter.group(2) != null
+ ? parameter.group(2) // Value is a token.
+ : parameter.group(3); // Value is a quoted string.
+ }
+
+ return new MediaType(string, type, subtype, charset);
+ }
+
+ /**
+ * Returns the high-level media type, such as "text", "image", "audio",
+ * "video", or "application".
+ */
+ public String type() {
+ return type;
+ }
+
+ /**
+ * Returns a specific media subtype, such as "plain" or "png", "mpeg",
+ * "mp4" or "xml".
+ */
+ public String subtype() {
+ return subtype;
+ }
+
+ /**
+ * Returns the charset of this media type, or null if this media type doesn't
+ * specify a charset.
+ */
+ public Charset charset() {
+ return charset != null ? Charset.forName(charset) : null;
+ }
+
+ /**
+ * Returns the charset of this media type, or {@code defaultValue} if this
+ * media type doesn't specify a charset.
+ */
+ public Charset charset(Charset defaultValue) {
+ return charset != null ? Charset.forName(charset) : defaultValue;
+ }
+
+ /**
+ * Returns the encoded media type, like "text/plain; charset=utf-8",
+ * appropriate for use in a Content-Type header.
+ */
+ @Override public String toString() {
+ return mediaType;
+ }
+
+ @Override public boolean equals(Object o) {
+ return o instanceof MediaType && ((MediaType) o).mediaType.equals(mediaType);
+ }
+
+ @Override public int hashCode() {
+ return mediaType.hashCode();
+ }
+}
diff --git a/src/main/java/com/squareup/okhttp/OkHttpClient.java b/src/main/java/com/squareup/okhttp/OkHttpClient.java
index 68ff59b..a86123d 100644
--- a/src/main/java/com/squareup/okhttp/OkHttpClient.java
+++ b/src/main/java/com/squareup/okhttp/OkHttpClient.java
@@ -16,10 +16,10 @@
package com.squareup.okhttp;
import com.squareup.okhttp.internal.Util;
+import com.squareup.okhttp.internal.http.Dispatcher;
import com.squareup.okhttp.internal.http.HttpAuthenticator;
import com.squareup.okhttp.internal.http.HttpURLConnectionImpl;
import com.squareup.okhttp.internal.http.HttpsURLConnectionImpl;
-import com.squareup.okhttp.internal.http.OkResponseCache;
import com.squareup.okhttp.internal.http.OkResponseCacheAdapter;
import com.squareup.okhttp.internal.tls.OkHostnameVerifier;
import java.net.CookieHandler;
@@ -32,10 +32,8 @@ import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.net.URLStreamHandlerFactory;
import java.util.Arrays;
-import java.util.Collections;
-import java.util.LinkedHashSet;
import java.util.List;
-import java.util.Set;
+import java.util.concurrent.TimeUnit;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSocketFactory;
@@ -45,9 +43,10 @@ public final class OkHttpClient implements URLStreamHandlerFactory {
private static final List<String> DEFAULT_TRANSPORTS
= Util.immutableList(Arrays.asList("spdy/3", "http/1.1"));
+ private final RouteDatabase routeDatabase;
+ private final Dispatcher dispatcher;
private Proxy proxy;
private List<String> transports;
- private final Set<Route> failedRoutes;
private ProxySelector proxySelector;
private CookieHandler cookieHandler;
private ResponseCache responseCache;
@@ -56,13 +55,65 @@ public final class OkHttpClient implements URLStreamHandlerFactory {
private OkAuthenticator authenticator;
private ConnectionPool connectionPool;
private boolean followProtocolRedirects = true;
+ private int connectTimeout;
+ private int readTimeout;
public OkHttpClient() {
- this.failedRoutes = Collections.synchronizedSet(new LinkedHashSet<Route>());
+ routeDatabase = new RouteDatabase();
+ dispatcher = new Dispatcher();
}
private OkHttpClient(OkHttpClient copyFrom) {
- this.failedRoutes = copyFrom.failedRoutes; // Avoid allocating an unnecessary LinkedHashSet.
+ routeDatabase = copyFrom.routeDatabase;
+ dispatcher = copyFrom.dispatcher;
+ }
+
+ /**
+ * Sets the default connect timeout for new connections. A value of 0 means no timeout.
+ *
+ * @see URLConnection#setConnectTimeout(int)
+ */
+ public void setConnectTimeout(long timeout, TimeUnit unit) {
+ if (timeout < 0) {
+ throw new IllegalArgumentException("timeout < 0");
+ }
+ if (unit == null) {
+ throw new IllegalArgumentException("unit == null");
+ }
+ long millis = unit.toMillis(timeout);
+ if (millis > Integer.MAX_VALUE) {
+ throw new IllegalArgumentException("Timeout too large.");
+ }
+ connectTimeout = (int) millis;
+ }
+
+ /** Default connect timeout (in milliseconds). */
+ public int getConnectTimeout() {
+ return connectTimeout;
+ }
+
+ /**
+ * Sets the default read timeout for new connections. A value of 0 means no timeout.
+ *
+ * @see URLConnection#setReadTimeout(int)
+ */
+ public void setReadTimeout(long timeout, TimeUnit unit) {
+ if (timeout < 0) {
+ throw new IllegalArgumentException("timeout < 0");
+ }
+ if (unit == null) {
+ throw new IllegalArgumentException("unit == null");
+ }
+ long millis = unit.toMillis(timeout);
+ if (millis > Integer.MAX_VALUE) {
+ throw new IllegalArgumentException("Timeout too large.");
+ }
+ readTimeout = (int) millis;
+ }
+
+ /** Default read timeout (in milliseconds). */
+ public int getReadTimeout() {
+ return readTimeout;
}
/**
@@ -129,7 +180,7 @@ public final class OkHttpClient implements URLStreamHandlerFactory {
return responseCache;
}
- private OkResponseCache okResponseCache() {
+ public OkResponseCache getOkResponseCache() {
if (responseCache instanceof HttpResponseCache) {
return ((HttpResponseCache) responseCache).okResponseCache;
} else if (responseCache != null) {
@@ -217,6 +268,10 @@ public final class OkHttpClient implements URLStreamHandlerFactory {
return followProtocolRedirects;
}
+ public RouteDatabase getRoutesDatabase() {
+ return routeDatabase;
+ }
+
/**
* Configure the transports used by this client to communicate with remote
* servers. By default this client will prefer the most efficient transport
@@ -252,6 +307,9 @@ public final class OkHttpClient implements URLStreamHandlerFactory {
if (transports.contains(null)) {
throw new IllegalArgumentException("transports must not contain null");
}
+ if (transports.contains("")) {
+ throw new IllegalArgumentException("transports contains an empty string");
+ }
this.transports = transports;
return this;
}
@@ -260,29 +318,36 @@ public final class OkHttpClient implements URLStreamHandlerFactory {
return transports;
}
+ /**
+ * Schedules {@code request} to be executed.
+ */
+ public void enqueue(Request request, Response.Receiver responseReceiver) {
+ // Create the HttpURLConnection immediately so the enqueued job gets the current settings of
+ // this client. Otherwise changes to this client (socket factory, redirect policy, etc.) may
+ // incorrectly be reflected in the request when it is dispatched later.
+ dispatcher.enqueue(open(request.url()), request, responseReceiver);
+ }
+
+ /**
+ * Cancels all scheduled tasks tagged with {@code tag}. Requests that are already
+ * in flight might not be canceled.
+ */
+ public void cancel(Object tag) {
+ dispatcher.cancel(tag);
+ }
+
public HttpURLConnection open(URL url) {
- String protocol = url.getProtocol();
- OkHttpClient copy = copyWithDefaults();
- if (protocol.equals("http")) {
- return new HttpURLConnectionImpl(url, copy, copy.okResponseCache(), copy.failedRoutes);
- } else if (protocol.equals("https")) {
- return new HttpsURLConnectionImpl(url, copy, copy.okResponseCache(), copy.failedRoutes);
- } else {
- throw new IllegalArgumentException("Unexpected protocol: " + protocol);
- }
+ return open(url, proxy);
}
HttpURLConnection open(URL url, Proxy proxy) {
String protocol = url.getProtocol();
OkHttpClient copy = copyWithDefaults();
copy.proxy = proxy;
- if (protocol.equals("http")) {
- return new HttpURLConnectionImpl(url, copy, copy.okResponseCache(), copy.failedRoutes);
- } else if (protocol.equals("https")) {
- return new HttpsURLConnectionImpl(url, copy, copy.okResponseCache(), copy.failedRoutes);
- } else {
- throw new IllegalArgumentException("Unexpected protocol: " + protocol);
- }
+
+ if (protocol.equals("http")) return new HttpURLConnectionImpl(url, copy);
+ if (protocol.equals("https")) return new HttpsURLConnectionImpl(url, copy);
+ throw new IllegalArgumentException("Unexpected protocol: " + protocol);
}
/**
@@ -307,6 +372,8 @@ public final class OkHttpClient implements URLStreamHandlerFactory {
result.connectionPool = connectionPool != null ? connectionPool : ConnectionPool.getDefault();
result.followProtocolRedirects = followProtocolRedirects;
result.transports = transports != null ? transports : DEFAULT_TRANSPORTS;
+ result.connectTimeout = connectTimeout;
+ result.readTimeout = readTimeout;
return result;
}
diff --git a/src/main/java/com/squareup/okhttp/internal/http/OkResponseCache.java b/src/main/java/com/squareup/okhttp/OkResponseCache.java
index 5829f02..ffe6f54 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/OkResponseCache.java
+++ b/src/main/java/com/squareup/okhttp/OkResponseCache.java
@@ -13,9 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.squareup.okhttp.internal.http;
+package com.squareup.okhttp;
-import com.squareup.okhttp.ResponseSource;
import java.io.IOException;
import java.net.CacheRequest;
import java.net.CacheResponse;
@@ -29,9 +28,8 @@ import java.util.Map;
* An extended response cache API. Unlike {@link java.net.ResponseCache}, this
* interface supports conditional caching and statistics.
*
- * <p>Along with the rest of the {@code internal} package, this is not a public
- * API. Applications wishing to supply their own caches must use the more
- * limited {@link java.net.ResponseCache} interface.
+ * <h3>Warning: Experimental OkHttp 2.0 API</h3>
+ * This class is in beta. APIs are subject to change!
*/
public interface OkResponseCache {
CacheResponse get(URI uri, String requestMethod, Map<String, List<String>> requestHeaders)
@@ -39,6 +37,9 @@ public interface OkResponseCache {
CacheRequest put(URI uri, URLConnection urlConnection) throws IOException;
+ /** Remove any cache entries for the supplied {@code uri} if the request method invalidates. */
+ void maybeRemove(String requestMethod, URI uri) throws IOException;
+
/**
* Handles a conditional request hit by updating the stored cache response
* with the headers from {@code httpConnection}. The cached response body is
diff --git a/src/main/java/com/squareup/okhttp/Request.java b/src/main/java/com/squareup/okhttp/Request.java
new file mode 100644
index 0000000..6f3569b
--- /dev/null
+++ b/src/main/java/com/squareup/okhttp/Request.java
@@ -0,0 +1,270 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp;
+
+import com.squareup.okhttp.internal.Util;
+import com.squareup.okhttp.internal.http.RawHeaders;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * An HTTP request. Instances of this class are immutable if their {@link #body}
+ * is null or itself immutable.
+ *
+ * <h3>Warning: Experimental OkHttp 2.0 API</h3>
+ * This class is in beta. APIs are subject to change!
+ */
+public final class Request {
+ private final URL url;
+ private final String method;
+ private final RawHeaders headers;
+ private final Body body;
+ private final Object tag;
+
+ private Request(Builder builder) {
+ this.url = builder.url;
+ this.method = builder.method;
+ this.headers = new RawHeaders(builder.headers);
+ this.body = builder.body;
+ this.tag = builder.tag != null ? builder.tag : this;
+ }
+
+ public URL url() {
+ return url;
+ }
+
+ public String urlString() {
+ return url.toString();
+ }
+
+ public String method() {
+ return method;
+ }
+
+ public String header(String name) {
+ return headers.get(name);
+ }
+
+ public List<String> headers(String name) {
+ return headers.values(name);
+ }
+
+ public Set<String> headerNames() {
+ return headers.names();
+ }
+
+ public int headerCount() {
+ return headers.length();
+ }
+
+ public String headerName(int index) {
+ return headers.getFieldName(index);
+ }
+
+ public String headerValue(int index) {
+ return headers.getValue(index);
+ }
+
+ public Body body() {
+ return body;
+ }
+
+ public Object tag() {
+ return tag;
+ }
+
+ public abstract static class Body {
+ /**
+ * Returns the Content-Type header for this body, or null if the content
+ * type is unknown.
+ */
+ public MediaType contentType() {
+ return null;
+ }
+
+ /** Returns the number of bytes in this body, or -1 if that count is unknown. */
+ public long contentLength() {
+ return -1;
+ }
+
+ /** Writes the content of this request to {@code out}. */
+ public abstract void writeTo(OutputStream out) throws IOException;
+
+ /**
+ * Returns a new request body that transmits {@code content}. If {@code
+ * contentType} lacks a charset, this will use UTF-8.
+ */
+ public static Body create(MediaType contentType, String content) {
+ contentType = contentType.charset() != null
+ ? contentType
+ : MediaType.parse(contentType + "; charset=utf-8");
+ try {
+ byte[] bytes = content.getBytes(contentType.charset().name());
+ return create(contentType, bytes);
+ } catch (UnsupportedEncodingException e) {
+ throw new AssertionError();
+ }
+ }
+
+ /** Returns a new request body that transmits {@code content}. */
+ public static Body create(final MediaType contentType, final byte[] content) {
+ if (contentType == null) throw new NullPointerException("contentType == null");
+ if (content == null) throw new NullPointerException("content == null");
+
+ return new Body() {
+ @Override public MediaType contentType() {
+ return contentType;
+ }
+
+ @Override public long contentLength() {
+ return content.length;
+ }
+
+ @Override public void writeTo(OutputStream out) throws IOException {
+ out.write(content);
+ }
+ };
+ }
+
+ /** Returns a new request body that transmits the content of {@code file}. */
+ public static Body create(final MediaType contentType, final File file) {
+ if (contentType == null) throw new NullPointerException("contentType == null");
+ if (file == null) throw new NullPointerException("content == null");
+
+ return new Body() {
+ @Override public MediaType contentType() {
+ return contentType;
+ }
+
+ @Override public long contentLength() {
+ return file.length();
+ }
+
+ @Override public void writeTo(OutputStream out) throws IOException {
+ long length = contentLength();
+ if (length == 0) return;
+
+ InputStream in = null;
+ try {
+ in = new FileInputStream(file);
+ byte[] buffer = new byte[(int) Math.min(8192, length)];
+ for (int c; (c = in.read(buffer)) != -1; ) {
+ out.write(buffer, 0, c);
+ }
+ } finally {
+ Util.closeQuietly(in);
+ }
+ }
+ };
+ }
+ }
+
+ public static class Builder {
+ private URL url;
+ private String method = "GET";
+ private final RawHeaders headers = new RawHeaders();
+ private Body body;
+ private Object tag;
+
+ public Builder(String url) {
+ url(url);
+ }
+
+ public Builder(URL url) {
+ url(url);
+ }
+
+ public Builder url(String url) {
+ try {
+ this.url = new URL(url);
+ return this;
+ } catch (MalformedURLException e) {
+ throw new IllegalArgumentException("Malformed URL: " + url);
+ }
+ }
+
+ public Builder url(URL url) {
+ if (url == null) throw new IllegalStateException("url == null");
+ this.url = url;
+ return this;
+ }
+
+ /**
+ * Sets the header named {@code name} to {@code value}. If this request
+ * already has any headers with that name, they are all replaced.
+ */
+ public Builder header(String name, String value) {
+ headers.set(name, value);
+ return this;
+ }
+
+ /**
+ * Adds a header with {@code name} and {@code value}. Prefer this method for
+ * multiply-valued headers like "Cookie".
+ */
+ public Builder addHeader(String name, String value) {
+ headers.add(name, value);
+ return this;
+ }
+
+ public Builder get() {
+ return method("GET", null);
+ }
+
+ public Builder head() {
+ return method("HEAD", null);
+ }
+
+ public Builder post(Body body) {
+ return method("POST", body);
+ }
+
+ public Builder put(Body body) {
+ return method("PUT", body);
+ }
+
+ public Builder method(String method, Body body) {
+ if (method == null || method.length() == 0) {
+ throw new IllegalArgumentException("method == null || method.length() == 0");
+ }
+ this.method = method;
+ this.body = body;
+ return this;
+ }
+
+ /**
+ * Attaches {@code tag} to the request. It can be used later to cancel the
+ * request. If the tag is unspecified or null, the request is canceled by
+ * using the request itself as the tag.
+ */
+ public Builder tag(Object tag) {
+ this.tag = tag;
+ return this;
+ }
+
+ public Request build() {
+ return new Request(this);
+ }
+ }
+}
diff --git a/src/main/java/com/squareup/okhttp/Response.java b/src/main/java/com/squareup/okhttp/Response.java
new file mode 100644
index 0000000..4896a38
--- /dev/null
+++ b/src/main/java/com/squareup/okhttp/Response.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp;
+
+import com.squareup.okhttp.internal.Util;
+import com.squareup.okhttp.internal.http.RawHeaders;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * An HTTP response. Instances of this class are not immutable: the response
+ * body is a one-shot value that may be consumed only once. All other properties
+ * are immutable.
+ *
+ * <h3>Warning: Experimental OkHttp 2.0 API</h3>
+ * This class is in beta. APIs are subject to change!
+ */
+public final class Response {
+ private final Request request;
+ private final int code;
+ private final RawHeaders headers;
+ private final Body body;
+ private final Response redirectedBy;
+
+ private Response(Builder builder) {
+ this.request = builder.request;
+ this.code = builder.code;
+ this.headers = new RawHeaders(builder.headers);
+ this.body = builder.body;
+ this.redirectedBy = builder.redirectedBy;
+ }
+
+ /**
+ * The wire-level request that initiated this HTTP response. This is usually
+ * <strong>not</strong> the same request instance provided to the HTTP client:
+ * <ul>
+ * <li>It may be transformed by the HTTP client. For example, the client
+ * may have added its own {@code Content-Encoding} header to enable
+ * response compression.
+ * <li>It may be the request generated in response to an HTTP redirect.
+ * In this case the request URL may be different than the initial
+ * request URL.
+ * </ul>
+ */
+ public Request request() {
+ return request;
+ }
+
+ public int code() {
+ return code;
+ }
+
+ public String header(String name) {
+ return header(name, null);
+ }
+
+ public String header(String name, String defaultValue) {
+ String result = headers.get(name);
+ 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.length();
+ }
+
+ public String headerName(int index) {
+ return headers.getFieldName(index);
+ }
+
+ public String headerValue(int index) {
+ return headers.getValue(index);
+ }
+
+ public Body body() {
+ return body;
+ }
+
+ /**
+ * Returns the response for the HTTP redirect that triggered this response, or
+ * null if this response wasn't triggered by an automatic redirect. The body
+ * of the returned response should not be read because it has already been
+ * consumed by the redirecting client.
+ */
+ public Response redirectedBy() {
+ return redirectedBy;
+ }
+
+ public abstract static class Body {
+ public String contentType() {
+ return null;
+ }
+
+ public long contentLength() {
+ return -1;
+ }
+
+ public abstract InputStream byteStream() throws IOException;
+
+ public byte[] bytes() throws IOException {
+ long contentLength = contentLength();
+ if (contentLength > Integer.MAX_VALUE) {
+ throw new IOException("Cannot buffer entire body for content length: " + contentLength);
+ }
+
+ if (contentLength != -1) {
+ byte[] content = new byte[(int) contentLength];
+ InputStream in = byteStream();
+ Util.readFully(in, content);
+ if (in.read() != -1) throw new IOException("Content-Length and stream length disagree");
+ return content;
+
+ } else {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ Util.copy(byteStream(), out);
+ return out.toByteArray();
+ }
+ }
+
+ /**
+ * Returns the response bytes as a UTF-8 character stream. Do not call this
+ * method if the response content is not a UTF-8 character stream.
+ */
+ public Reader charStream() throws IOException {
+ // TODO: parse content-type.
+ return new InputStreamReader(byteStream(), "UTF-8");
+ }
+
+ /**
+ * Returns the response bytes as a UTF-8 string. Do not call this method if
+ * the response content is not a UTF-8 character stream.
+ */
+ public String string() throws IOException {
+ // TODO: parse content-type.
+ return new String(bytes(), "UTF-8");
+ }
+ }
+
+ public interface Receiver {
+ void onFailure(Failure failure);
+ void onResponse(Response response) throws IOException;
+ }
+
+ public static class Builder {
+ private final Request request;
+ private final int code;
+ private final RawHeaders headers = new RawHeaders();
+ private Body body;
+ private Response redirectedBy;
+
+ public Builder(Request request, int code) {
+ if (request == null) throw new IllegalArgumentException("request == null");
+ if (code <= 0) throw new IllegalArgumentException("code <= 0");
+ this.request = request;
+ this.code = code;
+ }
+
+ /**
+ * Sets the header named {@code name} to {@code value}. If this request
+ * already has any headers with that name, they are all replaced.
+ */
+ public Builder header(String name, String value) {
+ headers.set(name, value);
+ return this;
+ }
+
+ /**
+ * Adds a header with {@code name} and {@code value}. Prefer this method for
+ * multiply-valued headers like "Set-Cookie".
+ */
+ public Builder addHeader(String name, String value) {
+ headers.add(name, value);
+ return this;
+ }
+
+ public Builder body(Body body) {
+ this.body = body;
+ return this;
+ }
+
+ public Builder redirectedBy(Response redirectedBy) {
+ this.redirectedBy = redirectedBy;
+ return this;
+ }
+
+ public Response build() {
+ if (request == null) throw new IllegalStateException("Response has no request.");
+ if (code == -1) throw new IllegalStateException("Response has no code.");
+ return new Response(this);
+ }
+ }
+}
diff --git a/src/main/java/com/squareup/okhttp/Route.java b/src/main/java/com/squareup/okhttp/Route.java
index 6968c60..4b8786d 100644
--- a/src/main/java/com/squareup/okhttp/Route.java
+++ b/src/main/java/com/squareup/okhttp/Route.java
@@ -59,13 +59,13 @@ public class Route {
return inetSocketAddress;
}
- /** Returns true if this route uses modern tls. */
+ /** Returns true if this route uses modern TLS. */
public boolean isModernTls() {
return modernTls;
}
- /** Returns a copy of this route with flipped tls mode. */
- public Route flipTlsMode() {
+ /** Returns a copy of this route with flipped TLS mode. */
+ Route flipTlsMode() {
return new Route(address, proxy, inetSocketAddress, !modernTls);
}
diff --git a/src/main/java/com/squareup/okhttp/RouteDatabase.java b/src/main/java/com/squareup/okhttp/RouteDatabase.java
new file mode 100644
index 0000000..9cbeaa7
--- /dev/null
+++ b/src/main/java/com/squareup/okhttp/RouteDatabase.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp;
+
+import java.io.IOException;
+import java.util.LinkedHashSet;
+import java.util.Set;
+import javax.net.ssl.SSLHandshakeException;
+
+/**
+ * A blacklist of failed routes to avoid when creating a new connection to a
+ * target address. This is used so that OkHttp can learn from its mistakes: if
+ * there was a failure attempting to connect to a specific IP address, proxy
+ * server or TLS mode, that failure is remembered and alternate routes are
+ * preferred.
+ */
+public final class RouteDatabase {
+ private final Set<Route> failedRoutes = new LinkedHashSet<Route>();
+
+ /** Records a failure connecting to {@code failedRoute}. */
+ public synchronized void failed(Route failedRoute, IOException failure) {
+ failedRoutes.add(failedRoute);
+
+ if (!(failure instanceof SSLHandshakeException)) {
+ // If the problem was not related to SSL then it will also fail with
+ // a different TLS mode therefore we can be proactive about it.
+ failedRoutes.add(failedRoute.flipTlsMode());
+ }
+ }
+
+ /** Records success connecting to {@code failedRoute}. */
+ public synchronized void connected(Route route) {
+ failedRoutes.remove(route);
+ }
+
+ /** Returns true if {@code route} has failed recently and should be avoided. */
+ public synchronized boolean shouldPostpone(Route route) {
+ return failedRoutes.contains(route);
+ }
+
+ public synchronized int failedRoutesCount() {
+ return failedRoutes.size();
+ }
+}
diff --git a/src/main/java/com/squareup/okhttp/internal/Platform.java b/src/main/java/com/squareup/okhttp/internal/Platform.java
index c06f480..13ea4df 100644
--- a/src/main/java/com/squareup/okhttp/internal/Platform.java
+++ b/src/main/java/com/squareup/okhttp/internal/Platform.java
@@ -25,6 +25,7 @@ import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
+import java.net.InetSocketAddress;
import java.net.NetworkInterface;
import java.net.Socket;
import java.net.SocketException;
@@ -57,6 +58,11 @@ public class Platform {
return PLATFORM;
}
+ /** Prefix used on custom headers. */
+ public String getPrefix() {
+ return "OkHttp";
+ }
+
public void logW(String warning) {
System.out.println(warning);
}
@@ -99,6 +105,11 @@ public class Platform {
public void setNpnProtocols(SSLSocket socket, byte[] npnProtocols) {
}
+ public void connectSocket(Socket socket, InetSocketAddress address,
+ int connectTimeout) throws IOException {
+ socket.connect(address, connectTimeout);
+ }
+
/**
* Returns a deflater output stream that supports SYNC_FLUSH for SPDY name
* value blocks. This throws an {@link UnsupportedOperationException} on
@@ -212,6 +223,9 @@ public class Platform {
return super.getMtu(socket); // There's no longer an interface with this local address.
}
return (Integer) getMtu.invoke(networkInterface);
+ } catch (NullPointerException e) {
+ // Certain Alcatel devices throw on getByInetAddress. Return default.
+ return super.getMtu(socket);
} catch (SocketException e) {
// Certain Motorola devices always throw on getByInetAddress. Return the default for those.
return super.getMtu(socket);
@@ -238,6 +252,19 @@ public class Platform {
this.setHostname = setHostname;
}
+ @Override public void connectSocket(Socket socket, InetSocketAddress address,
+ int connectTimeout) throws IOException {
+ try {
+ socket.connect(address, connectTimeout);
+ } catch (SecurityException se) {
+ // Before android 4.3, socket.connect could throw a SecurityException
+ // if opening a socket resulted in an EACCES error.
+ IOException ioException = new IOException("Exception in connect");
+ ioException.initCause(se);
+ throw ioException;
+ }
+ }
+
@Override public void enableTlsExtensions(SSLSocket socket, String uriHost) {
super.enableTlsExtensions(socket, uriHost);
if (openSslSocketClass.isInstance(socket)) {
diff --git a/src/main/java/com/squareup/okhttp/internal/StrictLineReader.java b/src/main/java/com/squareup/okhttp/internal/StrictLineReader.java
index 3ddc693..74af6fd 100644
--- a/src/main/java/com/squareup/okhttp/internal/StrictLineReader.java
+++ b/src/main/java/com/squareup/okhttp/internal/StrictLineReader.java
@@ -146,8 +146,7 @@ public class StrictLineReader implements Closeable {
// Let's anticipate up to 80 characters on top of those already read.
ByteArrayOutputStream out = new ByteArrayOutputStream(end - pos + 80) {
- @Override
- public String toString() {
+ @Override public String toString() {
int length = (count > 0 && buf[count - 1] == CR) ? count - 1 : count;
try {
return new String(buf, 0, length, charset.name());
diff --git a/src/main/java/com/squareup/okhttp/internal/http/Dispatcher.java b/src/main/java/com/squareup/okhttp/internal/http/Dispatcher.java
new file mode 100644
index 0000000..d5c5006
--- /dev/null
+++ b/src/main/java/com/squareup/okhttp/internal/http/Dispatcher.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal.http;
+
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+public final class Dispatcher {
+ // TODO: thread pool size should be configurable; possibly configurable per host.
+ private final ThreadPoolExecutor executorService = new ThreadPoolExecutor(
+ 8, 8, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
+ private final Map<Object, List<Job>> enqueuedJobs = new LinkedHashMap<Object, List<Job>>();
+
+ public synchronized void enqueue(
+ HttpURLConnection connection, Request request, Response.Receiver responseReceiver) {
+ Job job = new Job(this, connection, request, responseReceiver);
+ List<Job> jobsForTag = enqueuedJobs.get(request.tag());
+ if (jobsForTag == null) {
+ jobsForTag = new ArrayList<Job>(2);
+ enqueuedJobs.put(request.tag(), jobsForTag);
+ }
+ jobsForTag.add(job);
+ executorService.execute(job);
+ }
+
+ public synchronized void cancel(Object tag) {
+ List<Job> jobs = enqueuedJobs.remove(tag);
+ if (jobs == null) return;
+ for (Job job : jobs) {
+ executorService.remove(job);
+ }
+ }
+
+ synchronized void finished(Job job) {
+ List<Job> jobs = enqueuedJobs.get(job.request.tag());
+ if (jobs != null) jobs.remove(job);
+ }
+
+ static class RealResponseBody extends Response.Body {
+ private final HttpURLConnection connection;
+ private final InputStream in;
+
+ RealResponseBody(HttpURLConnection connection, InputStream in) {
+ this.connection = connection;
+ this.in = in;
+ }
+
+ @Override public String contentType() {
+ return connection.getHeaderField("Content-Type");
+ }
+
+ @Override public long contentLength() {
+ return connection.getContentLength(); // TODO: getContentLengthLong
+ }
+
+ @Override public InputStream byteStream() throws IOException {
+ return in;
+ }
+ }
+}
diff --git a/src/main/java/com/squareup/okhttp/internal/http/HttpDate.java b/src/main/java/com/squareup/okhttp/internal/http/HttpDate.java
index acb5fda..8275958 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/HttpDate.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/HttpDate.java
@@ -42,8 +42,7 @@ final class HttpDate {
};
/** If we fail to parse a date in a non-standard format, try each of these formats in sequence. */
- private static final String[] BROWSER_COMPATIBLE_DATE_FORMATS = new String[] {
- /* This list comes from {@code org.apache.http.impl.cookie.BrowserCompatSpec}. */
+ private static final String[] BROWSER_COMPATIBLE_DATE_FORMAT_STRINGS = new String[] {
"EEEE, dd-MMM-yy HH:mm:ss zzz", // RFC 1036
"EEE MMM d HH:mm:ss yyyy", // ANSI C asctime()
"EEE, dd-MMM-yyyy HH:mm:ss z", "EEE, dd-MMM-yyyy HH-mm-ss z", "EEE, dd MMM yy HH:mm:ss z",
@@ -54,19 +53,26 @@ final class HttpDate {
/* RI bug 6641315 claims a cookie of this format was once served by www.yahoo.com */
"EEE MMM d yyyy HH:mm:ss z", };
- /**
- * Returns the date for {@code value}. Returns null if the value couldn't be
- * parsed.
- */
+ private static final DateFormat[] BROWSER_COMPATIBLE_DATE_FORMATS =
+ new DateFormat[BROWSER_COMPATIBLE_DATE_FORMAT_STRINGS.length];
+
+ /** Returns the date for {@code value}. Returns null if the value couldn't be parsed. */
public static Date parse(String value) {
try {
return STANDARD_DATE_FORMAT.get().parse(value);
- } catch (ParseException ignore) {
+ } catch (ParseException ignored) {
}
- for (String formatString : BROWSER_COMPATIBLE_DATE_FORMATS) {
- try {
- return new SimpleDateFormat(formatString, Locale.US).parse(value);
- } catch (ParseException ignore) {
+ synchronized (BROWSER_COMPATIBLE_DATE_FORMAT_STRINGS) {
+ for (int i = 0, count = BROWSER_COMPATIBLE_DATE_FORMAT_STRINGS.length; i < count; i++) {
+ DateFormat format = BROWSER_COMPATIBLE_DATE_FORMATS[i];
+ if (format == null) {
+ format = new SimpleDateFormat(BROWSER_COMPATIBLE_DATE_FORMAT_STRINGS[i], Locale.US);
+ BROWSER_COMPATIBLE_DATE_FORMATS[i] = format;
+ }
+ try {
+ return format.parse(value);
+ } catch (ParseException ignored) {
+ }
}
}
return null;
diff --git a/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java b/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java
index 49e6032..51fd2a7 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java
@@ -19,6 +19,8 @@ package com.squareup.okhttp.internal.http;
import com.squareup.okhttp.Address;
import com.squareup.okhttp.Connection;
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.OkResponseCache;
import com.squareup.okhttp.ResponseSource;
import com.squareup.okhttp.TunnelRequest;
import com.squareup.okhttp.internal.Dns;
@@ -31,6 +33,7 @@ import java.io.OutputStream;
import java.net.CacheRequest;
import java.net.CacheResponse;
import java.net.CookieHandler;
+import java.net.HttpURLConnection;
import java.net.Proxy;
import java.net.URI;
import java.net.URISyntaxException;
@@ -85,7 +88,8 @@ public class HttpEngine {
};
public static final int HTTP_CONTINUE = 100;
- protected final HttpURLConnectionImpl policy;
+ protected final Policy policy;
+ protected final OkHttpClient client;
protected final String method;
@@ -137,14 +141,15 @@ public class HttpEngine {
/**
* @param requestHeaders the client's supplied request headers. This class
- * creates a private copy that it can mutate.
+ * creates a private copy that it can mutate.
* @param connection the connection used for an intermediate response
- * immediately prior to this request/response pair, such as a same-host
- * redirect. This engine assumes ownership of the connection and must
- * release it when it is unneeded.
+ * immediately prior to this request/response pair, such as a same-host
+ * redirect. This engine assumes ownership of the connection and must
+ * release it when it is unneeded.
*/
- public HttpEngine(HttpURLConnectionImpl policy, String method, RawHeaders requestHeaders,
+ public HttpEngine(OkHttpClient client, Policy policy, String method, RawHeaders requestHeaders,
Connection connection, RetryableOutputStream requestBodyOut) throws IOException {
+ this.client = client;
this.policy = policy;
this.method = method;
this.connection = connection;
@@ -175,8 +180,9 @@ public class HttpEngine {
prepareRawRequestHeaders();
initResponseSource();
- if (policy.responseCache != null) {
- policy.responseCache.trackResponse(responseSource);
+ OkResponseCache responseCache = client.getOkResponseCache();
+ if (responseCache != null) {
+ responseCache.trackResponse(responseSource);
}
// The raw response source may require the network, but the request
@@ -196,7 +202,7 @@ public class HttpEngine {
if (responseSource.requiresConnection()) {
sendSocketRequest();
} else if (connection != null) {
- policy.connectionPool.recycle(connection);
+ client.getConnectionPool().recycle(connection);
connection = null;
}
}
@@ -207,15 +213,14 @@ public class HttpEngine {
*/
private void initResponseSource() throws IOException {
responseSource = ResponseSource.NETWORK;
- if (!policy.getUseCaches() || policy.responseCache == null) {
- return;
- }
+ if (!policy.getUseCaches()) return;
- CacheResponse candidate =
- policy.responseCache.get(uri, method, requestHeaders.getHeaders().toMultimap(false));
- if (candidate == null) {
- return;
- }
+ OkResponseCache responseCache = client.getOkResponseCache();
+ if (responseCache == null) return;
+
+ CacheResponse candidate = responseCache.get(
+ uri, method, requestHeaders.getHeaders().toMultimap(false));
+ if (candidate == null) return;
Map<String, List<String>> responseHeadersMap = candidate.getHeaders();
cachedResponseBody = candidate.getBody();
@@ -273,22 +278,22 @@ public class HttpEngine {
SSLSocketFactory sslSocketFactory = null;
HostnameVerifier hostnameVerifier = null;
if (uri.getScheme().equalsIgnoreCase("https")) {
- sslSocketFactory = policy.sslSocketFactory;
- hostnameVerifier = policy.hostnameVerifier;
+ sslSocketFactory = client.getSslSocketFactory();
+ hostnameVerifier = client.getHostnameVerifier();
}
Address address = new Address(uriHost, getEffectivePort(uri), sslSocketFactory,
- hostnameVerifier, policy.authenticator, policy.requestedProxy, policy.getTransports());
- routeSelector = new RouteSelector(address, uri, policy.proxySelector, policy.connectionPool,
- Dns.DEFAULT, policy.getFailedRoutes());
+ hostnameVerifier, client.getAuthenticator(), client.getProxy(), client.getTransports());
+ routeSelector = new RouteSelector(address, uri, client.getProxySelector(),
+ client.getConnectionPool(), Dns.DEFAULT, client.getRoutesDatabase());
}
connection = routeSelector.next();
if (!connection.isConnected()) {
- connection.connect(policy.getConnectTimeout(), policy.getReadTimeout(), getTunnelConfig());
- policy.connectionPool.maybeShare(connection);
- policy.getFailedRoutes().remove(connection.getRoute());
+ connection.connect(client.getConnectTimeout(), client.getReadTimeout(), getTunnelConfig());
+ client.getConnectionPool().maybeShare(connection);
+ client.getRoutesDatabase().connected(connection.getRoute());
}
connected(connection);
- if (connection.getRoute().getProxy() != policy.requestedProxy) {
+ if (connection.getRoute().getProxy() != client.getProxy()) {
// Update the request line if the proxy changed; it may need a host name.
requestHeaders.getHeaders().setRequestLine(getRequestLine());
}
@@ -386,17 +391,20 @@ public class HttpEngine {
private void maybeCache() throws IOException {
// Are we caching at all?
- if (!policy.getUseCaches() || policy.responseCache == null) {
- return;
- }
+ if (!policy.getUseCaches()) return;
+ OkResponseCache responseCache = client.getOkResponseCache();
+ if (responseCache == null) return;
+
+ HttpURLConnection connectionToCache = policy.getHttpConnectionToCache();
// Should we cache this response for this request?
if (!responseHeaders.isCacheable(requestHeaders)) {
+ responseCache.maybeRemove(connectionToCache.getRequestMethod(), uri);
return;
}
// Offer this request to the cache.
- cacheRequest = policy.responseCache.put(uri, policy.getHttpConnectionToCache());
+ cacheRequest = responseCache.put(uri, connectionToCache);
}
/**
@@ -408,7 +416,7 @@ public class HttpEngine {
public final void automaticallyReleaseConnectionToPool() {
automaticallyReleaseConnectionToPool = true;
if (connection != null && connectionReleased) {
- policy.connectionPool.recycle(connection);
+ client.getConnectionPool().recycle(connection);
connection = null;
}
}
@@ -432,7 +440,7 @@ public class HttpEngine {
Util.closeQuietly(connection);
connection = null;
} else if (automaticallyReleaseConnectionToPool) {
- policy.connectionPool.recycle(connection);
+ client.getConnectionPool().recycle(connection);
connection = null;
}
}
@@ -520,7 +528,7 @@ public class HttpEngine {
requestHeaders.setIfModifiedSince(new Date(ifModifiedSince));
}
- CookieHandler cookieHandler = policy.cookieHandler;
+ CookieHandler cookieHandler = client.getCookieHandler();
if (cookieHandler != null) {
requestHeaders.addCookies(
cookieHandler.get(uri, requestHeaders.getHeaders().toMultimap(false)));
@@ -635,8 +643,9 @@ public class HttpEngine {
release(false);
ResponseHeaders combinedHeaders = cachedResponseHeaders.combine(responseHeaders);
setResponse(combinedHeaders, cachedResponseBody);
- policy.responseCache.trackConditionalCacheHit();
- policy.responseCache.update(cacheResponse, policy.getHttpConnectionToCache());
+ OkResponseCache responseCache = client.getOkResponseCache();
+ responseCache.trackConditionalCacheHit();
+ responseCache.update(cacheResponse, policy.getHttpConnectionToCache());
return;
} else {
Util.closeQuietly(cachedResponseBody);
@@ -655,7 +664,7 @@ public class HttpEngine {
}
public void receiveHeaders(RawHeaders headers) throws IOException {
- CookieHandler cookieHandler = policy.cookieHandler;
+ CookieHandler cookieHandler = client.getCookieHandler();
if (cookieHandler != null) {
cookieHandler.put(uri, headers.toMultimap(true));
}
diff --git a/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java b/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java
index f04b317..9d64fa0 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java
@@ -78,18 +78,23 @@ public final class HttpTransport implements Transport {
}
// Stream a request body of a known length.
- int fixedContentLength = httpEngine.policy.getFixedContentLength();
+ long fixedContentLength = httpEngine.policy.getFixedContentLength();
if (fixedContentLength != -1) {
httpEngine.requestHeaders.setContentLength(fixedContentLength);
writeRequestHeaders();
return new FixedLengthOutputStream(requestOut, fixedContentLength);
}
+ long contentLength = httpEngine.requestHeaders.getContentLength();
+ if (contentLength > Integer.MAX_VALUE) {
+ throw new IllegalArgumentException("Use setFixedLengthStreamingMode() or "
+ + "setChunkedStreamingMode() for requests larger than 2 GiB.");
+ }
+
// Buffer a request body of a known length.
- int contentLength = httpEngine.requestHeaders.getContentLength();
if (contentLength != -1) {
writeRequestHeaders();
- return new RetryableOutputStream(contentLength);
+ return new RetryableOutputStream((int) contentLength);
}
// Buffer a request body of an unknown length. Don't write request
@@ -215,9 +220,9 @@ public final class HttpTransport implements Transport {
/** An HTTP body with a fixed length known in advance. */
private static final class FixedLengthOutputStream extends AbstractOutputStream {
private final OutputStream socketOut;
- private int bytesRemaining;
+ private long bytesRemaining;
- private FixedLengthOutputStream(OutputStream socketOut, int bytesRemaining) {
+ private FixedLengthOutputStream(OutputStream socketOut, long bytesRemaining) {
this.socketOut = socketOut;
this.bytesRemaining = bytesRemaining;
}
diff --git a/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java b/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java
index f8c5e9a..e8c198f 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java
@@ -18,10 +18,7 @@
package com.squareup.okhttp.internal.http;
import com.squareup.okhttp.Connection;
-import com.squareup.okhttp.ConnectionPool;
-import com.squareup.okhttp.OkAuthenticator;
import com.squareup.okhttp.OkHttpClient;
-import com.squareup.okhttp.Route;
import com.squareup.okhttp.internal.AbstractOutputStream;
import com.squareup.okhttp.internal.FaultRecoveringOutputStream;
import com.squareup.okhttp.internal.Platform;
@@ -30,13 +27,11 @@ import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
-import java.net.CookieHandler;
import java.net.HttpRetryException;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.ProtocolException;
import java.net.Proxy;
-import java.net.ProxySelector;
import java.net.SocketPermission;
import java.net.URL;
import java.security.Permission;
@@ -44,10 +39,8 @@ import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
-import java.util.Set;
-import javax.net.ssl.HostnameVerifier;
+import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLHandshakeException;
-import javax.net.ssl.SSLSocketFactory;
import static com.squareup.okhttp.internal.Util.getEffectivePort;
@@ -65,7 +58,7 @@ import static com.squareup.okhttp.internal.Util.getEffectivePort;
* connection} field on this class for null/non-null to determine of an instance
* is currently connected to a server.
*/
-public class HttpURLConnectionImpl extends HttpURLConnection {
+public class HttpURLConnectionImpl extends HttpURLConnection implements Policy {
/** Numeric status code, 307: Temporary Redirect. */
static final int HTTP_TEMP_REDIRECT = 307;
@@ -83,52 +76,19 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
*/
private static final int MAX_REPLAY_BUFFER_LENGTH = 8192;
- private final boolean followProtocolRedirects;
-
- /** The proxy requested by the client, or null for a proxy to be selected automatically. */
- final Proxy requestedProxy;
-
- final ProxySelector proxySelector;
- final CookieHandler cookieHandler;
- final OkResponseCache responseCache;
- final ConnectionPool connectionPool;
- /* SSL configuration; necessary for HTTP requests that get redirected to HTTPS. */
- SSLSocketFactory sslSocketFactory;
- HostnameVerifier hostnameVerifier;
- private List<String> transports;
- OkAuthenticator authenticator;
- final Set<Route> failedRoutes;
+ final OkHttpClient client;
private final RawHeaders rawRequestHeaders = new RawHeaders();
-
+ /** Like the superclass field of the same name, but a long and available on all platforms. */
+ private long fixedContentLength = -1;
private int redirectionCount;
private FaultRecoveringOutputStream faultRecoveringRequestBody;
-
protected IOException httpEngineFailure;
protected HttpEngine httpEngine;
- public HttpURLConnectionImpl(URL url, OkHttpClient client, OkResponseCache responseCache,
- Set<Route> failedRoutes) {
+ public HttpURLConnectionImpl(URL url, OkHttpClient client) {
super(url);
- this.followProtocolRedirects = client.getFollowProtocolRedirects();
- this.failedRoutes = failedRoutes;
- this.requestedProxy = client.getProxy();
- this.proxySelector = client.getProxySelector();
- this.cookieHandler = client.getCookieHandler();
- this.connectionPool = client.getConnectionPool();
- this.sslSocketFactory = client.getSslSocketFactory();
- this.hostnameVerifier = client.getHostnameVerifier();
- this.transports = client.getTransports();
- this.authenticator = client.getAuthenticator();
- this.responseCache = responseCache;
- }
-
- Set<Route> getFailedRoutes() {
- return failedRoutes;
- }
-
- List<String> getTransports() {
- return transports;
+ this.client = client;
}
@Override public final void connect() throws IOException {
@@ -274,7 +234,7 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
String hostName = getURL().getHost();
int hostPort = Util.getEffectivePort(getURL());
if (usingProxy()) {
- InetSocketAddress proxyAddress = (InetSocketAddress) requestedProxy.address();
+ InetSocketAddress proxyAddress = (InetSocketAddress) client.getProxy().address();
hostName = proxyAddress.getHostName();
hostPort = proxyAddress.getPort();
}
@@ -288,6 +248,22 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
return rawRequestHeaders.get(field);
}
+ @Override public void setConnectTimeout(int timeoutMillis) {
+ client.setConnectTimeout(timeoutMillis, TimeUnit.MILLISECONDS);
+ }
+
+ @Override public int getConnectTimeout() {
+ return client.getConnectTimeout();
+ }
+
+ @Override public void setReadTimeout(int timeoutMillis) {
+ client.setReadTimeout(timeoutMillis, TimeUnit.MILLISECONDS);
+ }
+
+ @Override public int getReadTimeout() {
+ return client.getReadTimeout();
+ }
+
private void initHttpEngine() throws IOException {
if (httpEngineFailure != null) {
throw httpEngineFailure;
@@ -313,17 +289,16 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
}
}
- protected HttpURLConnection getHttpConnectionToCache() {
+ @Override public HttpURLConnection getHttpConnectionToCache() {
return this;
}
private HttpEngine newHttpEngine(String method, RawHeaders requestHeaders,
Connection connection, RetryableOutputStream requestBody) throws IOException {
if (url.getProtocol().equals("http")) {
- return new HttpEngine(this, method, requestHeaders, connection, requestBody);
+ return new HttpEngine(client, this, method, requestHeaders, connection, requestBody);
} else if (url.getProtocol().equals("https")) {
- return new HttpsURLConnectionImpl.HttpsEngine(
- this, method, requestHeaders, connection, requestBody);
+ return new HttpsEngine(client, this, method, requestHeaders, connection, requestBody);
} else {
throw new AssertionError();
}
@@ -468,7 +443,7 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
private Retry processResponseHeaders() throws IOException {
Proxy selectedProxy = httpEngine.connection != null
? httpEngine.connection.getRoute().getProxy()
- : requestedProxy;
+ : client.getProxy();
final int responseCode = getResponseCode();
switch (responseCode) {
case HTTP_PROXY_AUTH:
@@ -477,7 +452,7 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
}
// fall-through
case HTTP_UNAUTHORIZED:
- boolean credentialsFound = HttpAuthenticator.processAuthHeader(authenticator,
+ boolean credentialsFound = HttpAuthenticator.processAuthHeader(client.getAuthenticator(),
getResponseCode(), httpEngine.getResponseHeaders().getHeaders(), rawRequestHeaders,
selectedProxy, url);
return credentialsFound ? Retry.SAME_CONNECTION : Retry.NONE;
@@ -508,7 +483,7 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
return Retry.NONE; // Don't follow redirects to unsupported protocols.
}
boolean sameProtocol = previousUrl.getProtocol().equals(url.getProtocol());
- if (!sameProtocol && !followProtocolRedirects) {
+ if (!sameProtocol && !client.getFollowProtocolRedirects()) {
return Retry.NONE; // This client doesn't follow redirects across protocols.
}
boolean sameHost = previousUrl.getHost().equals(url.getHost());
@@ -525,17 +500,17 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
}
/** @see java.net.HttpURLConnection#setFixedLengthStreamingMode(int) */
- final int getFixedContentLength() {
+ @Override public final long getFixedContentLength() {
return fixedContentLength;
}
- /** @see java.net.HttpURLConnection#setChunkedStreamingMode(int) */
- final int getChunkLength() {
+ @Override public final int getChunkLength() {
return chunkLength;
}
@Override public final boolean usingProxy() {
- return (requestedProxy != null && requestedProxy.type() != Proxy.Type.DIRECT);
+ Proxy proxy = client.getProxy();
+ return proxy != null && proxy.type() != Proxy.Type.DIRECT;
}
@Override public String getResponseMessage() throws IOException {
@@ -599,37 +574,26 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
* When append == false, we require that the transport list contains "http/1.1".
*/
private void setTransports(String transportsString, boolean append) {
- String[] transports = transportsString.split(",", -1);
- ArrayList<String> transportsList = new ArrayList<String>();
- if (!append) {
- // If we're not appending to the list, we need to make sure
- // the list contains "http/1.1". We do this in a separate loop
- // to avoid modifying any state before we validate the input.
- boolean containsHttp = false;
- for (int i = 0; i < transports.length; ++i) {
- if ("http/1.1".equals(transports[i])) {
- containsHttp = true;
- break;
- }
- }
-
- if (!containsHttp) {
- throw new IllegalArgumentException("Transport list doesn't contain http/1.1");
- }
- } else {
- transportsList.addAll(this.transports);
+ List<String> transportsList = new ArrayList<String>();
+ if (append) {
+ transportsList.addAll(client.getTransports());
}
-
- for (int i = 0; i < transports.length; ++i) {
- if (transports[i].length() == 0) {
- throw new IllegalArgumentException("Transport list contains an empty transport");
- }
-
- if (!transportsList.contains(transports[i])) {
- transportsList.add(transports[i]);
- }
+ for (String transport : transportsString.split(",", -1)) {
+ transportsList.add(transport);
}
+ client.setTransports(transportsList);
+ }
+
+ @Override public void setFixedLengthStreamingMode(int contentLength) {
+ setFixedLengthStreamingMode((long) contentLength);
+ }
- this.transports = Util.immutableList(transportsList);
+ // @Override Don't override: this overload method doesn't exist prior to Java 1.7.
+ public void setFixedLengthStreamingMode(long contentLength) {
+ if (super.connected) throw new IllegalStateException("Already connected");
+ if (chunkLength > 0) throw new IllegalStateException("Already in chunked mode");
+ if (contentLength < 0) throw new IllegalArgumentException("contentLength < 0");
+ this.fixedContentLength = contentLength;
+ super.fixedContentLength = (int) Math.min(contentLength, Integer.MAX_VALUE);
}
}
diff --git a/src/main/java/com/squareup/okhttp/internal/http/HttpsEngine.java b/src/main/java/com/squareup/okhttp/internal/http/HttpsEngine.java
new file mode 100644
index 0000000..2427d4e
--- /dev/null
+++ b/src/main/java/com/squareup/okhttp/internal/http/HttpsEngine.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal.http;
+
+import com.squareup.okhttp.Connection;
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.TunnelRequest;
+import java.io.IOException;
+import java.net.CacheResponse;
+import java.net.SecureCacheResponse;
+import java.net.URL;
+import javax.net.ssl.SSLSocket;
+
+import static com.squareup.okhttp.internal.Util.getEffectivePort;
+
+public final class HttpsEngine extends HttpEngine {
+ /**
+ * Stash of HttpsEngine.connection.socket to implement requests like {@code
+ * HttpsURLConnection#getCipherSuite} even after the connection has been
+ * recycled.
+ */
+ private SSLSocket sslSocket;
+
+ public HttpsEngine(OkHttpClient client, Policy policy, String method, RawHeaders requestHeaders,
+ Connection connection, RetryableOutputStream requestBody) throws IOException {
+ super(client, policy, method, requestHeaders, connection, requestBody);
+ this.sslSocket = connection != null ? (SSLSocket) connection.getSocket() : null;
+ }
+
+ @Override protected void connected(Connection connection) {
+ this.sslSocket = (SSLSocket) connection.getSocket();
+ }
+
+ @Override protected boolean acceptCacheResponseType(CacheResponse cacheResponse) {
+ return cacheResponse instanceof SecureCacheResponse;
+ }
+
+ @Override protected boolean includeAuthorityInRequestLine() {
+ // Even if there is a proxy, it isn't involved. Always request just the path.
+ return false;
+ }
+
+ public SSLSocket getSslSocket() {
+ return sslSocket;
+ }
+
+ @Override protected TunnelRequest getTunnelConfig() {
+ String userAgent = requestHeaders.getUserAgent();
+ if (userAgent == null) {
+ userAgent = getDefaultUserAgent();
+ }
+
+ URL url = policy.getURL();
+ return new TunnelRequest(url.getHost(), getEffectivePort(url), userAgent,
+ requestHeaders.getProxyAuthorization());
+ }
+}
diff --git a/src/main/java/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java b/src/main/java/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java
index 249fb98..0a4efea 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java
@@ -16,14 +16,10 @@
*/
package com.squareup.okhttp.internal.http;
-import com.squareup.okhttp.Connection;
import com.squareup.okhttp.OkHttpClient;
-import com.squareup.okhttp.Route;
-import com.squareup.okhttp.TunnelRequest;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
-import java.net.CacheResponse;
import java.net.HttpURLConnection;
import java.net.ProtocolException;
import java.net.SecureCacheResponse;
@@ -33,24 +29,20 @@ import java.security.Principal;
import java.security.cert.Certificate;
import java.util.List;
import java.util.Map;
-import java.util.Set;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
-import static com.squareup.okhttp.internal.Util.getEffectivePort;
-
public final class HttpsURLConnectionImpl extends HttpsURLConnection {
/** HttpUrlConnectionDelegate allows reuse of HttpURLConnectionImpl. */
private final HttpUrlConnectionDelegate delegate;
- public HttpsURLConnectionImpl(URL url, OkHttpClient client, OkResponseCache responseCache,
- Set<Route> failedRoutes) {
+ public HttpsURLConnectionImpl(URL url, OkHttpClient client) {
super(url);
- delegate = new HttpUrlConnectionDelegate(url, client, responseCache, failedRoutes);
+ delegate = new HttpUrlConnectionDelegate(url, client);
}
@Override public String getCipherSuite() {
@@ -124,290 +116,238 @@ public final class HttpsURLConnectionImpl extends HttpsURLConnection {
throw new IllegalStateException("Connection has not yet been established");
}
return delegate.httpEngine instanceof HttpsEngine
- ? ((HttpsEngine) delegate.httpEngine).sslSocket
+ ? ((HttpsEngine) delegate.httpEngine).getSslSocket()
: null; // Not HTTPS! Probably an https:// to http:// redirect.
}
- @Override
- public void disconnect() {
+ @Override public void disconnect() {
delegate.disconnect();
}
- @Override
- public InputStream getErrorStream() {
+ @Override public InputStream getErrorStream() {
return delegate.getErrorStream();
}
- @Override
- public String getRequestMethod() {
+ @Override public String getRequestMethod() {
return delegate.getRequestMethod();
}
- @Override
- public int getResponseCode() throws IOException {
+ @Override public int getResponseCode() throws IOException {
return delegate.getResponseCode();
}
- @Override
- public String getResponseMessage() throws IOException {
+ @Override public String getResponseMessage() throws IOException {
return delegate.getResponseMessage();
}
- @Override
- public void setRequestMethod(String method) throws ProtocolException {
+ @Override public void setRequestMethod(String method) throws ProtocolException {
delegate.setRequestMethod(method);
}
- @Override
- public boolean usingProxy() {
+ @Override public boolean usingProxy() {
return delegate.usingProxy();
}
- @Override
- public boolean getInstanceFollowRedirects() {
+ @Override public boolean getInstanceFollowRedirects() {
return delegate.getInstanceFollowRedirects();
}
- @Override
- public void setInstanceFollowRedirects(boolean followRedirects) {
+ @Override public void setInstanceFollowRedirects(boolean followRedirects) {
delegate.setInstanceFollowRedirects(followRedirects);
}
- @Override
- public void connect() throws IOException {
+ @Override public void connect() throws IOException {
connected = true;
delegate.connect();
}
- @Override
- public boolean getAllowUserInteraction() {
+ @Override public boolean getAllowUserInteraction() {
return delegate.getAllowUserInteraction();
}
- @Override
- public Object getContent() throws IOException {
+ @Override public Object getContent() throws IOException {
return delegate.getContent();
}
@SuppressWarnings("unchecked") // Spec does not generify
- @Override
- public Object getContent(Class[] types) throws IOException {
+ @Override public Object getContent(Class[] types) throws IOException {
return delegate.getContent(types);
}
- @Override
- public String getContentEncoding() {
+ @Override public String getContentEncoding() {
return delegate.getContentEncoding();
}
- @Override
- public int getContentLength() {
+ @Override public int getContentLength() {
return delegate.getContentLength();
}
- @Override
- public String getContentType() {
+ @Override public String getContentType() {
return delegate.getContentType();
}
- @Override
- public long getDate() {
+ @Override public long getDate() {
return delegate.getDate();
}
- @Override
- public boolean getDefaultUseCaches() {
+ @Override public boolean getDefaultUseCaches() {
return delegate.getDefaultUseCaches();
}
- @Override
- public boolean getDoInput() {
+ @Override public boolean getDoInput() {
return delegate.getDoInput();
}
- @Override
- public boolean getDoOutput() {
+ @Override public boolean getDoOutput() {
return delegate.getDoOutput();
}
- @Override
- public long getExpiration() {
+ @Override public long getExpiration() {
return delegate.getExpiration();
}
- @Override
- public String getHeaderField(int pos) {
+ @Override public String getHeaderField(int pos) {
return delegate.getHeaderField(pos);
}
- @Override
- public Map<String, List<String>> getHeaderFields() {
+ @Override public Map<String, List<String>> getHeaderFields() {
return delegate.getHeaderFields();
}
- @Override
- public Map<String, List<String>> getRequestProperties() {
+ @Override public Map<String, List<String>> getRequestProperties() {
return delegate.getRequestProperties();
}
- @Override
- public void addRequestProperty(String field, String newValue) {
+ @Override public void addRequestProperty(String field, String newValue) {
delegate.addRequestProperty(field, newValue);
}
- @Override
- public String getHeaderField(String key) {
+ @Override public String getHeaderField(String key) {
return delegate.getHeaderField(key);
}
- @Override
- public long getHeaderFieldDate(String field, long defaultValue) {
+ @Override public long getHeaderFieldDate(String field, long defaultValue) {
return delegate.getHeaderFieldDate(field, defaultValue);
}
- @Override
- public int getHeaderFieldInt(String field, int defaultValue) {
+ @Override public int getHeaderFieldInt(String field, int defaultValue) {
return delegate.getHeaderFieldInt(field, defaultValue);
}
- @Override
- public String getHeaderFieldKey(int position) {
+ @Override public String getHeaderFieldKey(int position) {
return delegate.getHeaderFieldKey(position);
}
- @Override
- public long getIfModifiedSince() {
+ @Override public long getIfModifiedSince() {
return delegate.getIfModifiedSince();
}
- @Override
- public InputStream getInputStream() throws IOException {
+ @Override public InputStream getInputStream() throws IOException {
return delegate.getInputStream();
}
- @Override
- public long getLastModified() {
+ @Override public long getLastModified() {
return delegate.getLastModified();
}
- @Override
- public OutputStream getOutputStream() throws IOException {
+ @Override public OutputStream getOutputStream() throws IOException {
return delegate.getOutputStream();
}
- @Override
- public Permission getPermission() throws IOException {
+ @Override public Permission getPermission() throws IOException {
return delegate.getPermission();
}
- @Override
- public String getRequestProperty(String field) {
+ @Override public String getRequestProperty(String field) {
return delegate.getRequestProperty(field);
}
- @Override
- public URL getURL() {
+ @Override public URL getURL() {
return delegate.getURL();
}
- @Override
- public boolean getUseCaches() {
+ @Override public boolean getUseCaches() {
return delegate.getUseCaches();
}
- @Override
- public void setAllowUserInteraction(boolean newValue) {
+ @Override public void setAllowUserInteraction(boolean newValue) {
delegate.setAllowUserInteraction(newValue);
}
- @Override
- public void setDefaultUseCaches(boolean newValue) {
+ @Override public void setDefaultUseCaches(boolean newValue) {
delegate.setDefaultUseCaches(newValue);
}
- @Override
- public void setDoInput(boolean newValue) {
+ @Override public void setDoInput(boolean newValue) {
delegate.setDoInput(newValue);
}
- @Override
- public void setDoOutput(boolean newValue) {
+ @Override public void setDoOutput(boolean newValue) {
delegate.setDoOutput(newValue);
}
- @Override
- public void setIfModifiedSince(long newValue) {
+ @Override public void setIfModifiedSince(long newValue) {
delegate.setIfModifiedSince(newValue);
}
- @Override
- public void setRequestProperty(String field, String newValue) {
+ @Override public void setRequestProperty(String field, String newValue) {
delegate.setRequestProperty(field, newValue);
}
- @Override
- public void setUseCaches(boolean newValue) {
+ @Override public void setUseCaches(boolean newValue) {
delegate.setUseCaches(newValue);
}
- @Override
- public void setConnectTimeout(int timeoutMillis) {
+ @Override public void setConnectTimeout(int timeoutMillis) {
delegate.setConnectTimeout(timeoutMillis);
}
- @Override
- public int getConnectTimeout() {
+ @Override public int getConnectTimeout() {
return delegate.getConnectTimeout();
}
- @Override
- public void setReadTimeout(int timeoutMillis) {
+ @Override public void setReadTimeout(int timeoutMillis) {
delegate.setReadTimeout(timeoutMillis);
}
- @Override
- public int getReadTimeout() {
+ @Override public int getReadTimeout() {
return delegate.getReadTimeout();
}
- @Override
- public String toString() {
+ @Override public String toString() {
return delegate.toString();
}
- @Override
- public void setFixedLengthStreamingMode(int contentLength) {
+ @Override public void setFixedLengthStreamingMode(int contentLength) {
delegate.setFixedLengthStreamingMode(contentLength);
}
- @Override
- public void setChunkedStreamingMode(int chunkLength) {
+ @Override public void setChunkedStreamingMode(int chunkLength) {
delegate.setChunkedStreamingMode(chunkLength);
}
@Override public void setHostnameVerifier(HostnameVerifier hostnameVerifier) {
- delegate.hostnameVerifier = hostnameVerifier;
+ delegate.client.setHostnameVerifier(hostnameVerifier);
}
@Override public HostnameVerifier getHostnameVerifier() {
- return delegate.hostnameVerifier;
+ return delegate.client.getHostnameVerifier();
}
@Override public void setSSLSocketFactory(SSLSocketFactory sslSocketFactory) {
- delegate.sslSocketFactory = sslSocketFactory;
+ delegate.client.setSslSocketFactory(sslSocketFactory);
}
@Override public SSLSocketFactory getSSLSocketFactory() {
- return delegate.sslSocketFactory;
+ return delegate.client.getSslSocketFactory();
}
private final class HttpUrlConnectionDelegate extends HttpURLConnectionImpl {
- private HttpUrlConnectionDelegate(URL url, OkHttpClient client, OkResponseCache responseCache,
- Set<Route> failedRoutes) {
- super(url, client, responseCache, failedRoutes);
+ private HttpUrlConnectionDelegate(URL url, OkHttpClient client) {
+ super(url, client);
}
- @Override protected HttpURLConnection getHttpConnectionToCache() {
+ @Override public HttpURLConnection getHttpConnectionToCache() {
return HttpsURLConnectionImpl.this;
}
@@ -417,49 +357,4 @@ public final class HttpsURLConnectionImpl extends HttpsURLConnection {
: null;
}
}
-
- public static final class HttpsEngine extends HttpEngine {
- /**
- * Stash of HttpsEngine.connection.socket to implement requests like
- * {@link #getCipherSuite} even after the connection has been recycled.
- */
- private SSLSocket sslSocket;
-
- /**
- * @param policy the HttpURLConnectionImpl with connection configuration
- */
- public HttpsEngine(HttpURLConnectionImpl policy, String method, RawHeaders requestHeaders,
- Connection connection, RetryableOutputStream requestBody) throws IOException {
- super(policy, method, requestHeaders, connection, requestBody);
- this.sslSocket = connection != null ? (SSLSocket) connection.getSocket() : null;
- }
-
- @Override protected void connected(Connection connection) {
- this.sslSocket = (SSLSocket) connection.getSocket();
- }
-
- @Override protected boolean acceptCacheResponseType(CacheResponse cacheResponse) {
- return cacheResponse instanceof SecureCacheResponse;
- }
-
- @Override protected boolean includeAuthorityInRequestLine() {
- // Even if there is a proxy, it isn't involved. Always request just the file.
- return false;
- }
-
- public SSLSocket getSslSocket() {
- return sslSocket;
- }
-
- @Override protected TunnelRequest getTunnelConfig() {
- String userAgent = requestHeaders.getUserAgent();
- if (userAgent == null) {
- userAgent = getDefaultUserAgent();
- }
-
- URL url = policy.getURL();
- return new TunnelRequest(url.getHost(), getEffectivePort(url), userAgent,
- requestHeaders.getProxyAuthorization());
- }
- }
}
diff --git a/src/main/java/com/squareup/okhttp/internal/http/Job.java b/src/main/java/com/squareup/okhttp/internal/http/Job.java
new file mode 100644
index 0000000..33c58e4
--- /dev/null
+++ b/src/main/java/com/squareup/okhttp/internal/http/Job.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal.http;
+
+import com.squareup.okhttp.Failure;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+
+public final class Job implements Runnable {
+ final HttpURLConnection connection;
+ final Request request;
+ final Response.Receiver responseReceiver;
+ final Dispatcher dispatcher;
+
+ public Job(Dispatcher dispatcher, HttpURLConnection connection, Request request,
+ Response.Receiver responseReceiver) {
+ this.dispatcher = dispatcher;
+ this.connection = connection;
+ this.request = request;
+ this.responseReceiver = responseReceiver;
+ }
+
+ @Override public void run() {
+ try {
+ sendRequest();
+ Response response = readResponse();
+ responseReceiver.onResponse(response);
+ } catch (IOException e) {
+ responseReceiver.onFailure(new Failure.Builder()
+ .request(request)
+ .exception(e)
+ .build());
+ } finally {
+ connection.disconnect();
+ dispatcher.finished(this);
+ }
+ }
+
+ private HttpURLConnection sendRequest() throws IOException {
+ for (int i = 0; i < request.headerCount(); i++) {
+ connection.addRequestProperty(request.headerName(i), request.headerValue(i));
+ }
+ Request.Body body = request.body();
+ if (body != null) {
+ connection.setDoOutput(true);
+ long contentLength = body.contentLength();
+ if (contentLength == -1 || contentLength > Integer.MAX_VALUE) {
+ connection.setChunkedStreamingMode(0);
+ } else {
+ // Don't call setFixedLengthStreamingMode(long); that's only available on Java 1.7+.
+ connection.setFixedLengthStreamingMode((int) contentLength);
+ }
+ body.writeTo(connection.getOutputStream());
+ }
+ return connection;
+ }
+
+ private Response readResponse() throws IOException {
+ int responseCode = connection.getResponseCode();
+ Response.Builder responseBuilder = new Response.Builder(request, responseCode);
+
+ for (int i = 0; true; i++) {
+ String name = connection.getHeaderFieldKey(i);
+ if (name == null) break;
+ String value = connection.getHeaderField(i);
+ responseBuilder.addHeader(name, value);
+ }
+
+ responseBuilder.body(new Dispatcher.RealResponseBody(connection, connection.getInputStream()));
+ // TODO: set redirectedBy
+ return responseBuilder.build();
+ }
+}
diff --git a/src/main/java/com/squareup/okhttp/internal/http/OkResponseCacheAdapter.java b/src/main/java/com/squareup/okhttp/internal/http/OkResponseCacheAdapter.java
index 2ac915a..5335c2b 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/OkResponseCacheAdapter.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/OkResponseCacheAdapter.java
@@ -15,6 +15,7 @@
*/
package com.squareup.okhttp.internal.http;
+import com.squareup.okhttp.OkResponseCache;
import com.squareup.okhttp.ResponseSource;
import java.io.IOException;
import java.net.CacheRequest;
@@ -41,6 +42,9 @@ public final class OkResponseCacheAdapter implements OkResponseCache {
return responseCache.put(uri, urlConnection);
}
+ @Override public void maybeRemove(String requestMethod, URI uri) throws IOException {
+ }
+
@Override public void update(CacheResponse conditionalCacheHit, HttpURLConnection connection)
throws IOException {
}
diff --git a/src/main/java/com/squareup/okhttp/internal/http/Policy.java b/src/main/java/com/squareup/okhttp/internal/http/Policy.java
new file mode 100644
index 0000000..fcdd5ce
--- /dev/null
+++ b/src/main/java/com/squareup/okhttp/internal/http/Policy.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal.http;
+
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+public interface Policy {
+ /** Returns true if HTTP response caches should be used. */
+ boolean getUseCaches();
+
+ /** Returns the HttpURLConnection instance to store in the cache. */
+ HttpURLConnection getHttpConnectionToCache();
+
+ /** Returns the current destination URL, possibly a redirect. */
+ URL getURL();
+
+ /** Returns the If-Modified-Since timestamp, or 0 if none is set. */
+ long getIfModifiedSince();
+
+ /** Returns true if a non-direct proxy is specified. */
+ boolean usingProxy();
+
+ /** @see java.net.HttpURLConnection#setChunkedStreamingMode(int) */
+ int getChunkLength();
+
+ /** @see java.net.HttpURLConnection#setFixedLengthStreamingMode(int) */
+ long getFixedContentLength();
+}
diff --git a/src/main/java/com/squareup/okhttp/internal/http/RawHeaders.java b/src/main/java/com/squareup/okhttp/internal/http/RawHeaders.java
index eba887e..e5abd2c 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/RawHeaders.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/RawHeaders.java
@@ -32,6 +32,7 @@ import java.util.Map;
import java.util.Map.Entry;
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
@@ -248,6 +249,15 @@ public final class RawHeaders {
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 < length(); i++) {
+ result.add(getFieldName(i));
+ }
+ return Collections.unmodifiableSet(result);
+ }
+
/** Returns the value at {@code index} or null if that is out of range. */
public String getValue(int index) {
int valueIndex = index * 2 + 1;
@@ -267,6 +277,20 @@ public final class RawHeaders {
return null;
}
+ /** Returns an immutable list of the header values for {@code name}. */
+ public List<String> values(String name) {
+ List<String> result = null;
+ for (int i = 0; i < length(); i++) {
+ if (name.equalsIgnoreCase(getFieldName(i))) {
+ if (result == null) result = new ArrayList<String>(2);
+ result.add(getValue(i));
+ }
+ }
+ return result != null
+ ? Collections.unmodifiableList(result)
+ : Collections.<String>emptyList();
+ }
+
/** @param fieldNames a case-insensitive set of HTTP header field names. */
public RawHeaders getAll(Set<String> fieldNames) {
RawHeaders result = new RawHeaders();
diff --git a/src/main/java/com/squareup/okhttp/internal/http/RequestHeaders.java b/src/main/java/com/squareup/okhttp/internal/http/RequestHeaders.java
index 5ec4fcc..d5e3bd8 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/RequestHeaders.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/RequestHeaders.java
@@ -48,7 +48,7 @@ public final class RequestHeaders {
*/
private boolean hasAuthorization;
- private int contentLength = -1;
+ private long contentLength = -1;
private String transferEncoding;
private String userAgent;
private String host;
@@ -157,7 +157,7 @@ public final class RequestHeaders {
return hasAuthorization;
}
- public int getContentLength() {
+ public long getContentLength() {
return contentLength;
}
@@ -205,11 +205,11 @@ public final class RequestHeaders {
this.transferEncoding = "chunked";
}
- public void setContentLength(int contentLength) {
+ public void setContentLength(long contentLength) {
if (this.contentLength != -1) {
headers.removeAll("Content-Length");
}
- headers.add("Content-Length", Integer.toString(contentLength));
+ headers.add("Content-Length", Long.toString(contentLength));
this.contentLength = contentLength;
}
diff --git a/src/main/java/com/squareup/okhttp/internal/http/ResponseHeaders.java b/src/main/java/com/squareup/okhttp/internal/http/ResponseHeaders.java
index 0be6abb..461de8e 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/ResponseHeaders.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/ResponseHeaders.java
@@ -17,6 +17,7 @@
package com.squareup.okhttp.internal.http;
import com.squareup.okhttp.ResponseSource;
+import com.squareup.okhttp.internal.Platform;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URI;
@@ -34,16 +35,16 @@ import static com.squareup.okhttp.internal.Util.equal;
public final class ResponseHeaders {
/** HTTP header name for the local time when the request was sent. */
- private static final String SENT_MILLIS = "X-Android-Sent-Millis";
+ private static final String SENT_MILLIS = Platform.get().getPrefix() + "-Sent-Millis";
/** HTTP header name for the local time when the response was received. */
- private static final String RECEIVED_MILLIS = "X-Android-Received-Millis";
+ private static final String RECEIVED_MILLIS = Platform.get().getPrefix() + "-Received-Millis";
/** HTTP synthetic header with the response source. */
- static final String RESPONSE_SOURCE = "X-Android-Response-Source";
+ static final String RESPONSE_SOURCE = Platform.get().getPrefix() + "-Response-Source";
/** HTTP synthetic header with the selected transport (spdy/3, http/1.1, etc). */
- static final String SELECTED_TRANSPORT = "X-Android-Selected-Transport";
+ static final String SELECTED_TRANSPORT = Platform.get().getPrefix() + "-Selected-Transport";
private final URI uri;
private final RawHeaders headers;
diff --git a/src/main/java/com/squareup/okhttp/internal/http/RouteSelector.java b/src/main/java/com/squareup/okhttp/internal/http/RouteSelector.java
index ce0a71d..bab9df2 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/RouteSelector.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/RouteSelector.java
@@ -19,6 +19,7 @@ import com.squareup.okhttp.Address;
import com.squareup.okhttp.Connection;
import com.squareup.okhttp.ConnectionPool;
import com.squareup.okhttp.Route;
+import com.squareup.okhttp.RouteDatabase;
import com.squareup.okhttp.internal.Dns;
import java.io.IOException;
import java.net.InetAddress;
@@ -32,8 +33,6 @@ import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.NoSuchElementException;
-import java.util.Set;
-import javax.net.ssl.SSLHandshakeException;
import static com.squareup.okhttp.internal.Util.getEffectivePort;
@@ -55,7 +54,7 @@ public final class RouteSelector {
private final ProxySelector proxySelector;
private final ConnectionPool pool;
private final Dns dns;
- private final Set<Route> failedRoutes;
+ private final RouteDatabase routeDatabase;
/* The most recently attempted route. */
private Proxy lastProxy;
@@ -78,13 +77,13 @@ public final class RouteSelector {
private final List<Route> postponedRoutes;
public RouteSelector(Address address, URI uri, ProxySelector proxySelector, ConnectionPool pool,
- Dns dns, Set<Route> failedRoutes) {
+ Dns dns, RouteDatabase routeDatabase) {
this.address = address;
this.uri = uri;
this.proxySelector = proxySelector;
this.pool = pool;
this.dns = dns;
- this.failedRoutes = failedRoutes;
+ this.routeDatabase = routeDatabase;
this.postponedRoutes = new LinkedList<Route>();
resetNextProxy(uri, address.getProxy());
@@ -128,7 +127,7 @@ public final class RouteSelector {
boolean modernTls = nextTlsMode() == TLS_MODE_MODERN;
Route route = new Route(address, lastProxy, lastInetSocketAddress, modernTls);
- if (failedRoutes.contains(route)) {
+ if (routeDatabase.shouldPostpone(route)) {
postponedRoutes.add(route);
// We will only recurse in order to skip previously failed routes. They will be
// tried last.
@@ -149,12 +148,7 @@ public final class RouteSelector {
proxySelector.connectFailed(uri, failedRoute.getProxy().address(), failure);
}
- failedRoutes.add(failedRoute);
- if (!(failure instanceof SSLHandshakeException)) {
- // If the problem was not related to SSL then it will also fail with
- // a different Tls mode therefore we can be proactive about it.
- failedRoutes.add(failedRoute.flipTlsMode());
- }
+ routeDatabase.failed(failedRoute, failure);
}
/** Resets {@link #nextProxy} to the first option. */
diff --git a/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java b/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java
index 73709b5..daa4e80 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java
@@ -55,7 +55,7 @@ public final class SpdyTransport implements Transport {
boolean hasResponseBody = true;
stream = spdyConnection.newStream(requestHeaders.toNameValueBlock(), hasRequestBody,
hasResponseBody);
- stream.setReadTimeout(httpEngine.policy.getReadTimeout());
+ stream.setReadTimeout(httpEngine.client.getReadTimeout());
}
@Override public void writeRequestBody(RetryableOutputStream requestBody) throws IOException {
diff --git a/src/main/java/com/squareup/okhttp/internal/spdy/SpdyConnection.java b/src/main/java/com/squareup/okhttp/internal/spdy/SpdyConnection.java
index b8d2ff5..d3a3c9c 100644
--- a/src/main/java/com/squareup/okhttp/internal/spdy/SpdyConnection.java
+++ b/src/main/java/com/squareup/okhttp/internal/spdy/SpdyConnection.java
@@ -446,9 +446,8 @@ public final class SpdyConnection implements Closeable {
}
}
- @Override
- public void synStream(int flags, int streamId, int associatedStreamId, int priority, int slot,
- List<String> nameValueBlock) {
+ @Override public void synStream(int flags, int streamId, int associatedStreamId, int priority,
+ int slot, List<String> nameValueBlock) {
final SpdyStream synStream;
final SpdyStream previous;
synchronized (SpdyConnection.this) {
diff --git a/src/main/java/com/squareup/okhttp/internal/spdy/SpdyReader.java b/src/main/java/com/squareup/okhttp/internal/spdy/SpdyReader.java
index 7d3f2bd..c4f60ab 100644
--- a/src/main/java/com/squareup/okhttp/internal/spdy/SpdyReader.java
+++ b/src/main/java/com/squareup/okhttp/internal/spdy/SpdyReader.java
@@ -222,8 +222,8 @@ final class SpdyReader implements Closeable {
// Subclass inflater to install a dictionary when it's needed.
Inflater inflater = new Inflater() {
- @Override
- public int inflate(byte[] buffer, int offset, int count) throws DataFormatException {
+ @Override public int inflate(byte[] buffer, int offset, int count)
+ throws DataFormatException {
int result = super.inflate(buffer, offset, count);
if (result == 0 && needsDictionary()) {
setDictionary(DICTIONARY);
diff --git a/src/main/java/com/squareup/okhttp/internal/spdy/SpdyStream.java b/src/main/java/com/squareup/okhttp/internal/spdy/SpdyStream.java
index 744a04e..a6b39be 100644
--- a/src/main/java/com/squareup/okhttp/internal/spdy/SpdyStream.java
+++ b/src/main/java/com/squareup/okhttp/internal/spdy/SpdyStream.java
@@ -670,7 +670,9 @@ public final class SpdyStream {
}
closed = true;
}
- writeFrame(true);
+ if (!out.finished) {
+ writeFrame(true);
+ }
connection.flush();
cancelStreamIfNecessary();
}
diff --git a/src/test/java/com/squareup/okhttp/MediaTypeTest.java b/src/test/java/com/squareup/okhttp/MediaTypeTest.java
new file mode 100644
index 0000000..acbfdd5
--- /dev/null
+++ b/src/test/java/com/squareup/okhttp/MediaTypeTest.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ * Copyright (C) 2011 The Guava Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp;
+
+import com.squareup.okhttp.internal.Util;
+import java.nio.charset.Charset;
+import java.nio.charset.IllegalCharsetNameException;
+import java.nio.charset.UnsupportedCharsetException;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * Test MediaType API and parsing.
+ *
+ * <p>This test includes tests from <a
+ * href="https://code.google.com/p/guava-libraries/">Guava's</a> MediaTypeTest.
+ */
+public class MediaTypeTest {
+ @Test public void testParse() throws Exception {
+ MediaType mediaType = MediaType.parse("text/plain;boundary=foo;charset=utf-8");
+ assertEquals("text", mediaType.type());
+ assertEquals("plain", mediaType.subtype());
+ assertEquals("UTF-8", mediaType.charset().name());
+ assertEquals("text/plain;boundary=foo;charset=utf-8", mediaType.toString());
+ assertTrue(mediaType.equals(MediaType.parse("text/plain;boundary=foo;charset=utf-8")));
+ assertEquals(mediaType.hashCode(),
+ MediaType.parse("text/plain;boundary=foo;charset=utf-8").hashCode());
+ }
+
+ @Test public void testValidParse() throws Exception {
+ assertMediaType("text/plain");
+ assertMediaType("application/atom+xml; charset=utf-8");
+ assertMediaType("application/atom+xml; a=1; a=2; b=3");
+ assertMediaType("image/gif; foo=bar");
+ assertMediaType("text/plain; a=1");
+ assertMediaType("text/plain; a=1; a=2; b=3");
+ assertMediaType("text/plain; charset=utf-16");
+ assertMediaType("text/plain; \t \n \r a=b");
+ }
+
+ @Test public void testInvalidParse() throws Exception {
+ assertInvalid("");
+ assertInvalid("/");
+ assertInvalid("/");
+ assertInvalid("text");
+ assertInvalid("text/");
+ assertInvalid("te<t/plain");
+ assertInvalid("text/pl@in");
+ assertInvalid("text/plain;");
+ assertInvalid("text/plain; ");
+ assertInvalid("text/plain; a");
+ assertInvalid("text/plain; a=");
+ assertInvalid("text/plain; a=@");
+ assertInvalid("text/plain; a=\"@");
+ assertInvalid("text/plain; a=1;");
+ assertInvalid("text/plain; a=1; ");
+ assertInvalid("text/plain; a=1; b");
+ assertInvalid("text/plain; a=1; b=");
+ assertInvalid("text/plain; a=\u2025");
+ assertInvalid(" text/plain");
+ assertInvalid("te xt/plain");
+ assertInvalid("text /plain");
+ assertInvalid("text/ plain");
+ assertInvalid("text/pl ain");
+ assertInvalid("text/plain ");
+ assertInvalid("text/plain ; a=1");
+ }
+
+ @Test public void testParseWithSpecialCharacters() throws Exception {
+ MediaType mediaType = MediaType.parse(
+ "!#$%&'*+-.{|}~/!#$%&'*+-.{|}~; !#$%&'*+-.{|}~=!#$%&'*+-.{|}~");
+ assertEquals("!#$%&'*+-.{|}~", mediaType.type());
+ assertEquals("!#$%&'*+-.{|}~", mediaType.subtype());
+ }
+
+ @Test public void testCharsetIsOneOfManyParameters() throws Exception {
+ MediaType mediaType = MediaType.parse("text/plain;a=1;b=2;charset=utf-8;c=3");
+ assertEquals("text", mediaType.type());
+ assertEquals("plain", mediaType.subtype());
+ assertEquals("UTF-8", mediaType.charset().name());
+ }
+
+ @Test public void testCharsetAndQuoting() throws Exception {
+ MediaType mediaType = MediaType.parse(
+ "text/plain;a=\";charset=us-ascii\";charset=\"utf-8\";b=\"iso-8859-1\"");
+ assertEquals("UTF-8", mediaType.charset().name());
+ }
+
+ @Test public void testMultipleCharsets() {
+ try {
+ MediaType.parse("text/plain; charset=utf-8; charset=utf-16");
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ @Test public void testIllegalCharsetName() {
+ MediaType mediaType = MediaType.parse("text/plain; charset=\"!@#$%^&*()\"");
+ try {
+ mediaType.charset();
+ fail();
+ } catch (IllegalCharsetNameException expected) {
+ }
+ }
+
+ @Test public void testUnsupportedCharset() {
+ MediaType mediaType = MediaType.parse("text/plain; charset=utf-wtf");
+ try {
+ mediaType.charset();
+ fail();
+ } catch (UnsupportedCharsetException expected) {
+ }
+ }
+
+ @Test public void testDefaultCharset() throws Exception {
+ MediaType noCharset = MediaType.parse("text/plain");
+ assertEquals("UTF-8", noCharset.charset(Util.UTF_8).name());
+ assertEquals("US-ASCII", noCharset.charset(Charset.forName("US-ASCII")).name());
+
+ MediaType charset = MediaType.parse("text/plain; charset=iso-8859-1");
+ assertEquals("ISO-8859-1", charset.charset(Util.UTF_8).name());
+ assertEquals("ISO-8859-1", charset.charset(Charset.forName("US-ASCII")).name());
+ }
+
+ private void assertMediaType(String string) {
+ MediaType mediaType = MediaType.parse(string);
+ assertEquals(string, mediaType.toString());
+ }
+
+ private void assertInvalid(String string) {
+ assertNull(string, MediaType.parse(string));
+ }
+}
diff --git a/src/test/java/com/squareup/okhttp/RequestTest.java b/src/test/java/com/squareup/okhttp/RequestTest.java
new file mode 100644
index 0000000..ed43e19
--- /dev/null
+++ b/src/test/java/com/squareup/okhttp/RequestTest.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp;
+
+import com.squareup.okhttp.internal.Util;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+public final class RequestTest {
+ @Test public void string() throws Exception {
+ MediaType contentType = MediaType.parse("text/plain; charset=utf-8");
+ Request.Body body = Request.Body.create(contentType, "abc".getBytes(Util.UTF_8));
+ assertEquals(contentType, body.contentType());
+ assertEquals(3, body.contentLength());
+ assertEquals("616263", bodyToHex(body));
+ assertEquals("Retransmit body", "616263", bodyToHex(body));
+ }
+
+ @Test public void stringWithDefaultCharsetAdded() throws Exception {
+ MediaType contentType = MediaType.parse("text/plain");
+ Request.Body body = Request.Body.create(contentType, "\u0800");
+ assertEquals(MediaType.parse("text/plain; charset=utf-8"), body.contentType());
+ assertEquals(3, body.contentLength());
+ assertEquals("e0a080", bodyToHex(body));
+ }
+
+ @Test public void stringWithNonDefaultCharsetSpecified() throws Exception {
+ MediaType contentType = MediaType.parse("text/plain; charset=utf-16be");
+ Request.Body body = Request.Body.create(contentType, "\u0800");
+ assertEquals(contentType, body.contentType());
+ assertEquals(2, body.contentLength());
+ assertEquals("0800", bodyToHex(body));
+ }
+
+ @Test public void byteArray() throws Exception {
+ MediaType contentType = MediaType.parse("text/plain");
+ Request.Body body = Request.Body.create(contentType, "abc".getBytes(Util.UTF_8));
+ assertEquals(contentType, body.contentType());
+ assertEquals(3, body.contentLength());
+ assertEquals("616263", bodyToHex(body));
+ assertEquals("Retransmit body", "616263", bodyToHex(body));
+ }
+
+ @Test public void file() throws Exception {
+ File file = File.createTempFile("RequestTest", "tmp");
+ FileWriter writer = new FileWriter(file);
+ writer.write("abc");
+ writer.close();
+
+ MediaType contentType = MediaType.parse("text/plain");
+ Request.Body body = Request.Body.create(contentType, file);
+ assertEquals(contentType, body.contentType());
+ assertEquals(3, body.contentLength());
+ assertEquals("616263", bodyToHex(body));
+ assertEquals("Retransmit body", "616263", bodyToHex(body));
+ }
+
+ private String bodyToHex(Request.Body body) throws IOException {
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ body.writeTo(bytes);
+ return bytesToHex(bytes.toByteArray());
+ }
+
+ private String bytesToHex(byte[] bytes) {
+ StringBuilder hex = new StringBuilder();
+ for (byte b : bytes) {
+ if ((b & 0xff) < 0x10) hex.append('0');
+ hex.append(Integer.toHexString(b & 0xff));
+ }
+ return hex.toString();
+ }
+}
diff --git a/src/test/java/com/squareup/okhttp/internal/AsyncApiTest.java b/src/test/java/com/squareup/okhttp/internal/AsyncApiTest.java
new file mode 100644
index 0000000..163b639
--- /dev/null
+++ b/src/test/java/com/squareup/okhttp/internal/AsyncApiTest.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal;
+
+import com.google.mockwebserver.MockResponse;
+import com.google.mockwebserver.MockWebServer;
+import com.google.mockwebserver.RecordedRequest;
+import com.squareup.okhttp.MediaType;
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Request;
+import org.junit.After;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public final class AsyncApiTest {
+ private MockWebServer server = new MockWebServer();
+ private OkHttpClient client = new OkHttpClient();
+ private RecordingReceiver receiver = new RecordingReceiver();
+
+ @After public void tearDown() throws Exception {
+ server.shutdown();
+ }
+
+ @Test public void get() throws Exception {
+ server.enqueue(new MockResponse()
+ .setBody("abc")
+ .addHeader("Content-Type: text/plain"));
+ server.play();
+
+ Request request = new Request.Builder(server.getUrl("/"))
+ .header("User-Agent", "AsyncApiTest")
+ .build();
+ client.enqueue(request, receiver);
+
+ receiver.await(request)
+ .assertCode(200)
+ .assertContainsHeaders("Content-Type: text/plain")
+ .assertBody("abc");
+
+ assertTrue(server.takeRequest().getHeaders().contains("User-Agent: AsyncApiTest"));
+ }
+
+ @Test public void post() throws Exception {
+ server.enqueue(new MockResponse().setBody("abc"));
+ server.play();
+
+ Request request = new Request.Builder(server.getUrl("/"))
+ .post(Request.Body.create(MediaType.parse("text/plain"), "def"))
+ .build();
+ client.enqueue(request, receiver);
+
+ receiver.await(request)
+ .assertCode(200)
+ .assertBody("abc");
+
+ RecordedRequest recordedRequest = server.takeRequest();
+ assertEquals("def", recordedRequest.getUtf8Body());
+ assertEquals("3", recordedRequest.getHeader("Content-Length"));
+ }
+}
diff --git a/src/test/java/com/squareup/okhttp/internal/RecordedResponse.java b/src/test/java/com/squareup/okhttp/internal/RecordedResponse.java
new file mode 100644
index 0000000..388a27d
--- /dev/null
+++ b/src/test/java/com/squareup/okhttp/internal/RecordedResponse.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal;
+
+import com.squareup.okhttp.Failure;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+/**
+ * A received response or failure recorded by the response recorder.
+ */
+public class RecordedResponse {
+ public final Request request;
+ public final Response response;
+ public final String body;
+ public final Failure failure;
+
+ RecordedResponse(Request request, Response response, String body, Failure failure) {
+ this.request = request;
+ this.response = response;
+ this.body = body;
+ this.failure = failure;
+ }
+
+ public RecordedResponse assertCode(int expectedCode) {
+ assertEquals(expectedCode, response.code());
+ return this;
+ }
+
+ 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));
+ }
+ if (!actualHeaders.containsAll(Arrays.asList(expectedHeaders))) {
+ fail("Expected: " + actualHeaders + "\nto contain: " + Arrays.toString(expectedHeaders));
+ }
+ return this;
+ }
+
+ public RecordedResponse assertBody(String expectedBody) {
+ assertEquals(expectedBody, body);
+ return this;
+ }
+}
diff --git a/src/test/java/com/squareup/okhttp/internal/RecordingReceiver.java b/src/test/java/com/squareup/okhttp/internal/RecordingReceiver.java
new file mode 100644
index 0000000..58cd205
--- /dev/null
+++ b/src/test/java/com/squareup/okhttp/internal/RecordingReceiver.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal;
+
+import com.squareup.okhttp.Failure;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Records received HTTP responses so they can be later retrieved by tests.
+ */
+public class RecordingReceiver implements Response.Receiver {
+ public static final long TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(10);
+
+ private final List<RecordedResponse> responses = new ArrayList<RecordedResponse>();
+
+ @Override public synchronized void onFailure(Failure failure) {
+ responses.add(new RecordedResponse(failure.request(), null, null, failure));
+ notifyAll();
+ }
+
+ @Override public synchronized void onResponse(Response response) throws IOException {
+ responses.add(new RecordedResponse(
+ response.request(), response, response.body().string(), null));
+ notifyAll();
+ }
+
+ /**
+ * 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 {
+ long timeoutMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) + TIMEOUT_MILLIS;
+ while (true) {
+ for (RecordedResponse recordedResponse : responses) {
+ if (recordedResponse.request == request) {
+ return recordedResponse;
+ }
+ }
+
+ long nowMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+ if (nowMillis >= timeoutMillis) break;
+ wait(timeoutMillis - nowMillis);
+ }
+
+ throw new AssertionError("Timed out waiting for response to " + request);
+ }
+}
diff --git a/src/test/java/com/squareup/okhttp/internal/http/HttpResponseCacheTest.java b/src/test/java/com/squareup/okhttp/internal/http/HttpResponseCacheTest.java
index dee87cf..9e39c34 100644
--- a/src/test/java/com/squareup/okhttp/internal/http/HttpResponseCacheTest.java
+++ b/src/test/java/com/squareup/okhttp/internal/http/HttpResponseCacheTest.java
@@ -812,6 +812,28 @@ public final class HttpResponseCacheTest {
assertEquals("C", readAscii(openConnection(url)));
}
+ @Test public void postInvalidatesCacheWithUncacheableResponse() throws Exception {
+ // 1. seed the cache
+ // 2. invalidate it with uncacheable response
+ // 3. expect a cache miss
+ server.enqueue(
+ new MockResponse().setBody("A").addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)));
+ server.enqueue(new MockResponse().setBody("B").setResponseCode(500));
+ server.enqueue(new MockResponse().setBody("C"));
+ server.play();
+
+ URL url = server.getUrl("/");
+
+ assertEquals("A", readAscii(openConnection(url)));
+
+ HttpURLConnection invalidate = openConnection(url);
+ invalidate.setRequestMethod("POST");
+ addRequestBodyIfNecessary("POST", invalidate);
+ assertEquals("B", readAscii(invalidate));
+
+ assertEquals("C", readAscii(openConnection(url)));
+ }
+
@Test public void etag() throws Exception {
RecordedRequest conditionalRequest =
assertConditionallyCached(new MockResponse().addHeader("ETag: v1"));
diff --git a/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java b/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java
index 687a397..1cdcb1d 100644
--- a/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java
+++ b/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java
@@ -19,7 +19,7 @@ import com.squareup.okhttp.Address;
import com.squareup.okhttp.Connection;
import com.squareup.okhttp.ConnectionPool;
import com.squareup.okhttp.OkAuthenticator;
-import com.squareup.okhttp.Route;
+import com.squareup.okhttp.RouteDatabase;
import com.squareup.okhttp.internal.Dns;
import com.squareup.okhttp.internal.SslContextBuilder;
import java.io.IOException;
@@ -32,11 +32,8 @@ import java.net.URI;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Collections;
-import java.util.LinkedHashSet;
import java.util.List;
import java.util.NoSuchElementException;
-import java.util.Set;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLHandshakeException;
@@ -87,7 +84,7 @@ public final class RouteSelectorTest {
@Test public void singleRoute() throws Exception {
Address address = new Address(uriHost, uriPort, null, null, authenticator, null, transports);
RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
- Collections.EMPTY_SET);
+ new RouteDatabase());
assertTrue(routeSelector.hasNext());
dns.inetAddresses = makeFakeAddresses(255, 1);
@@ -105,15 +102,14 @@ public final class RouteSelectorTest {
@Test public void singleRouteReturnsFailedRoute() throws Exception {
Address address = new Address(uriHost, uriPort, null, null, authenticator, null, transports);
RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
- Collections.EMPTY_SET);
+ new RouteDatabase());
assertTrue(routeSelector.hasNext());
dns.inetAddresses = makeFakeAddresses(255, 1);
Connection connection = routeSelector.next();
- Set<Route> failedRoutes = new LinkedHashSet<Route>();
- failedRoutes.add(connection.getRoute());
- routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
- Collections.EMPTY_SET);
+ RouteDatabase routeDatabase = new RouteDatabase();
+ routeDatabase.failed(connection.getRoute(), new IOException());
+ routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns, routeDatabase);
assertConnection(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort, false);
assertFalse(routeSelector.hasNext());
try {
@@ -126,7 +122,7 @@ public final class RouteSelectorTest {
@Test public void explicitProxyTriesThatProxiesAddressesOnly() throws Exception {
Address address = new Address(uriHost, uriPort, null, null, authenticator, proxyA, transports);
RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
- Collections.EMPTY_SET);
+ new RouteDatabase());
assertTrue(routeSelector.hasNext());
dns.inetAddresses = makeFakeAddresses(255, 2);
@@ -144,7 +140,7 @@ public final class RouteSelectorTest {
Address address = new Address(uriHost, uriPort, null, null, authenticator, NO_PROXY,
transports);
RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
- Collections.EMPTY_SET);
+ new RouteDatabase());
assertTrue(routeSelector.hasNext());
dns.inetAddresses = makeFakeAddresses(255, 2);
@@ -161,7 +157,7 @@ public final class RouteSelectorTest {
proxySelector.proxies = null;
RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
- Collections.EMPTY_SET);
+ new RouteDatabase());
proxySelector.assertRequests(uri);
assertTrue(routeSelector.hasNext());
@@ -175,7 +171,7 @@ public final class RouteSelectorTest {
@Test public void proxySelectorReturnsNoProxies() throws Exception {
Address address = new Address(uriHost, uriPort, null, null, authenticator, null, transports);
RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
- Collections.EMPTY_SET);
+ new RouteDatabase());
assertTrue(routeSelector.hasNext());
dns.inetAddresses = makeFakeAddresses(255, 2);
@@ -193,7 +189,7 @@ public final class RouteSelectorTest {
proxySelector.proxies.add(proxyA);
proxySelector.proxies.add(proxyB);
RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
- Collections.EMPTY_SET);
+ new RouteDatabase());
proxySelector.assertRequests(uri);
// First try the IP addresses of the first proxy, in sequence.
@@ -226,7 +222,7 @@ public final class RouteSelectorTest {
proxySelector.proxies.add(NO_PROXY);
RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
- Collections.EMPTY_SET);
+ new RouteDatabase());
proxySelector.assertRequests(uri);
// Only the origin server will be attempted.
@@ -245,7 +241,7 @@ public final class RouteSelectorTest {
proxySelector.proxies.add(proxyB);
proxySelector.proxies.add(proxyA);
RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
- Collections.EMPTY_SET);
+ new RouteDatabase());
proxySelector.assertRequests(uri);
assertTrue(routeSelector.hasNext());
@@ -280,27 +276,27 @@ public final class RouteSelectorTest {
@Test public void nonSslErrorAddsAllTlsModesToFailedRoute() throws Exception {
Address address = new Address(uriHost, uriPort, socketFactory, hostnameVerifier, authenticator,
Proxy.NO_PROXY, transports);
- Set<Route> failedRoutes = new LinkedHashSet<Route>();
+ RouteDatabase routeDatabase = new RouteDatabase();
RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
- failedRoutes);
+ routeDatabase);
dns.inetAddresses = makeFakeAddresses(255, 1);
Connection connection = routeSelector.next();
routeSelector.connectFailed(connection, new IOException("Non SSL exception"));
- assertTrue(failedRoutes.size() == 2);
+ assertTrue(routeDatabase.failedRoutesCount() == 2);
}
@Test public void sslErrorAddsOnlyFailedTlsModeToFailedRoute() throws Exception {
Address address = new Address(uriHost, uriPort, socketFactory, hostnameVerifier, authenticator,
Proxy.NO_PROXY, transports);
- Set<Route> failedRoutes = new LinkedHashSet<Route>();
+ RouteDatabase routeDatabase = new RouteDatabase();
RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
- failedRoutes);
+ routeDatabase);
dns.inetAddresses = makeFakeAddresses(255, 1);
Connection connection = routeSelector.next();
routeSelector.connectFailed(connection, new SSLHandshakeException("SSL exception"));
- assertTrue(failedRoutes.size() == 1);
+ assertTrue(routeDatabase.failedRoutesCount() == 1);
}
@Test public void multipleProxiesMultipleInetAddressesMultipleTlsModes() throws Exception {
@@ -309,7 +305,7 @@ public final class RouteSelectorTest {
proxySelector.proxies.add(proxyA);
proxySelector.proxies.add(proxyB);
RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
- Collections.EMPTY_SET);
+ new RouteDatabase());
// Proxy A
dns.inetAddresses = makeFakeAddresses(255, 2);
@@ -346,9 +342,9 @@ public final class RouteSelectorTest {
Address address = new Address(uriHost, uriPort, socketFactory, hostnameVerifier, authenticator,
Proxy.NO_PROXY, transports);
- Set<Route> failedRoutes = new LinkedHashSet<Route>(1);
+ RouteDatabase routeDatabase = new RouteDatabase();
RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
- failedRoutes);
+ routeDatabase);
dns.inetAddresses = makeFakeAddresses(255, 1);
// Extract the regular sequence of routes from selector.
@@ -360,9 +356,9 @@ public final class RouteSelectorTest {
// Check that we do indeed have more than one route.
assertTrue(regularRoutes.size() > 1);
// Add first regular route as failed.
- failedRoutes.add(regularRoutes.get(0).getRoute());
+ routeDatabase.failed(regularRoutes.get(0).getRoute(), new SSLHandshakeException("none"));
// Reset selector
- routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns, failedRoutes);
+ routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns, routeDatabase);
List<Connection> routesWithFailedRoute = new ArrayList<Connection>();
while (routeSelector.hasNext()) {
diff --git a/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java b/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java
index 9266683..29b5cab 100644
--- a/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java
+++ b/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java
@@ -2090,22 +2090,18 @@ public final class URLConnectionTest {
@Test public void responseCacheReturnsNullOutputStream() throws Exception {
final AtomicBoolean aborted = new AtomicBoolean();
client.setResponseCache(new ResponseCache() {
- @Override
- public CacheResponse get(URI uri, String requestMethod,
+ @Override public CacheResponse get(URI uri, String requestMethod,
Map<String, List<String>> requestHeaders) throws IOException {
return null;
}
- @Override
- public CacheRequest put(URI uri, URLConnection connection) throws IOException {
+ @Override public CacheRequest put(URI uri, URLConnection connection) throws IOException {
return new CacheRequest() {
- @Override
- public void abort() {
+ @Override public void abort() {
aborted.set(true);
}
- @Override
- public OutputStream getBody() throws IOException {
+ @Override public OutputStream getBody() throws IOException {
return null;
}
};
@@ -2491,6 +2487,28 @@ public final class URLConnectionTest {
}
}
+ @Test public void veryLargeFixedLengthRequest() throws Exception {
+ server.setBodyLimit(0);
+ server.enqueue(new MockResponse());
+ server.play();
+
+ HttpURLConnection connection = client.open(server.getUrl("/"));
+ connection.setDoOutput(true);
+ long contentLength = Integer.MAX_VALUE + 1L;
+ connection.setFixedLengthStreamingMode(contentLength);
+ OutputStream out = connection.getOutputStream();
+ byte[] buffer = new byte[1024 * 1024];
+ for (long bytesWritten = 0; bytesWritten < contentLength; ) {
+ int byteCount = (int) Math.min(buffer.length, contentLength - bytesWritten);
+ out.write(buffer, 0, byteCount);
+ bytesWritten += byteCount;
+ }
+ assertContent("", connection);
+
+ RecordedRequest request = server.takeRequest();
+ assertEquals(Long.toString(contentLength), request.getHeader("Content-Length"));
+ }
+
/** Returns a gzipped copy of {@code bytes}. */
public byte[] gzip(byte[] bytes) throws IOException {
ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
diff --git a/src/test/java/com/squareup/okhttp/internal/spdy/MockSpdyPeer.java b/src/test/java/com/squareup/okhttp/internal/spdy/MockSpdyPeer.java
index bc2088c..088061b 100644
--- a/src/test/java/com/squareup/okhttp/internal/spdy/MockSpdyPeer.java
+++ b/src/test/java/com/squareup/okhttp/internal/spdy/MockSpdyPeer.java
@@ -185,9 +185,8 @@ public final class MockSpdyPeer implements Closeable {
this.settings = settings;
}
- @Override
- public void synStream(int flags, int streamId, int associatedStreamId, int priority, int slot,
- List<String> nameValueBlock) {
+ @Override public void synStream(int flags, int streamId, int associatedStreamId, int priority,
+ int slot, List<String> nameValueBlock) {
if (this.type != -1) throw new IllegalStateException();
this.type = SpdyConnection.TYPE_SYN_STREAM;
this.flags = flags;