diff options
author | Elliott Hughes <enh@google.com> | 2013-07-18 16:33:40 +0000 |
---|---|---|
committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2013-07-18 16:33:40 +0000 |
commit | 4d31eb4667a86cca81ecf5943b0b61cd1e8d1fae (patch) | |
tree | e508857d2203d128b780e594f3a6a63d52f18cbf | |
parent | 00834c9a00d53b29c6e486cb0b6f80fd505416e1 (diff) | |
parent | a82f42bbeedd0b07f3892f3b0efaa8122dc8f264 (diff) | |
download | okhttp-4d31eb4667a86cca81ecf5943b0b61cd1e8d1fae.tar.gz |
Merge "Update okhttp to commit abc8c9a30bc0c5a9a"
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; |