diff --git a/okcurl/src/main/java/com/squareup/okhttp/curl/Main.java b/okcurl/src/main/java/com/squareup/okhttp/curl/Main.java index 699402fc2..9a45f2073 100644 --- a/okcurl/src/main/java/com/squareup/okhttp/curl/Main.java +++ b/okcurl/src/main/java/com/squareup/okhttp/curl/Main.java @@ -19,7 +19,6 @@ import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.collect.Lists; import com.squareup.okhttp.ConnectionPool; -import com.squareup.okhttp.Failure; import com.squareup.okhttp.Headers; import com.squareup.okhttp.MediaType; import com.squareup.okhttp.OkHttpClient; @@ -46,7 +45,7 @@ import javax.net.ssl.X509TrustManager; import static java.util.concurrent.TimeUnit.SECONDS; @Command(name = Main.NAME, description = "A curl for the next-generation web.") -public class Main extends HelpOption implements Runnable, Response.Receiver { +public class Main extends HelpOption implements Runnable { static final String NAME = "okcurl"; static final int DEFAULT_TIMEOUT = -1; @@ -130,11 +129,32 @@ public class Main extends HelpOption implements Runnable, Response.Receiver { client = createClient(); Request request = createRequest(); - client.enqueue(request, this); + try { + Response response = client.execute(request); + if (showHeaders) { + System.out.println(response.statusLine()); + Headers headers = response.headers(); + for (int i = 0, count = headers.size(); i < count; i++) { + System.out.println(headers.name(i) + ": " + headers.value(i)); + } + System.out.println(); + } - // Immediately begin triggering an executor shutdown so that after execution of the above - // request the threads do not stick around until timeout. - client.getDispatcher().getExecutorService().shutdown(); + Response.Body body = response.body(); + byte[] buffer = new byte[1024]; + while (body.ready()) { + int c = body.byteStream().read(buffer); + if (c == -1) { + return; + } + System.out.write(buffer, 0, c); + } + body.close(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + close(); + } } private OkHttpClient createClient() { @@ -205,36 +225,6 @@ public class Main extends HelpOption implements Runnable, Response.Receiver { return request.build(); } - @Override public void onFailure(Failure failure) { - failure.exception().printStackTrace(); - close(); - } - - @Override public boolean onResponse(Response response) throws IOException { - if (showHeaders) { - System.out.println(response.statusLine()); - Headers headers = response.headers(); - for (int i = 0, count = headers.size(); i < count; i++) { - System.out.println(headers.name(i) + ": " + headers.value(i)); - } - System.out.println(); - } - - Response.Body body = response.body(); - byte[] buffer = new byte[1024]; - while (body.ready()) { - int c = body.byteStream().read(buffer); - if (c == -1) { - close(); - return true; - } - - System.out.write(buffer, 0, c); - } - close(); - return false; - } - private void close() { client.getConnectionPool().evictAll(); // Close any persistent connections. } diff --git a/okhttp/src/main/java/com/squareup/okhttp/Job.java b/okhttp/src/main/java/com/squareup/okhttp/Job.java index f22512afe..be08cb99a 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/Job.java +++ b/okhttp/src/main/java/com/squareup/okhttp/Job.java @@ -47,7 +47,7 @@ final class Job extends NamedRunnable { /** The request; possibly a consequence of redirects or auth headers. */ private Request request; - private HttpEngine engine; + HttpEngine engine; public Job(Dispatcher dispatcher, OkHttpClient client, Request request, Response.Receiver responseReceiver) { @@ -91,7 +91,7 @@ final class Job extends NamedRunnable { * Performs the request and returns the response. May return null if this job * was canceled. */ - private Response getResponse() throws IOException { + Response getResponse() throws IOException { Response redirectedBy = null; // Copy body metadata to the appropriate request headers. @@ -146,7 +146,10 @@ final class Job extends NamedRunnable { if (redirect == null) { engine.releaseConnection(); return response.newBuilder() - .body(new RealResponseBody(response, engine.getResponseBody())) + // Cache body includes original content-length and content-type data. + .body(engine.responseSource().usesCache() + ? engine.getResponse().body() + : new RealResponseBody(response, engine.getResponseBody())) .redirectedBy(redirectedBy) .build(); } diff --git a/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java b/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java index fafd48aeb..5b8a62355 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java +++ b/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java @@ -371,6 +371,46 @@ public final class OkHttpClient implements URLStreamHandlerFactory, Cloneable { return protocols; } + /** + * Invokes {@code request} immediately, and blocks until the response can be + * processed or is in error. + * + *
The caller may read the response body with the response's + * {@link Response#body} method. To facilitate connection recycling, callers + * should always {@link Response.Body#close() close the response body}. + * + *
Note that transport-layer success (receiving a HTTP response code, + * headers and body) does not necessarily indicate application-layer + * success: {@code response} may still indicate an unhappy HTTP response + * code like 404 or 500. + * + *
Receivers do not need to block while waiting for the response body to + * download. Instead, they can get called back as data arrives. Use {@link + * Response.Body#ready} to check if bytes should be read immediately. While + * there is data ready, read it. + * + *
The current implementation of {@link Response.Body#ready} always + * returns true when the underlying transport is HTTP/1. This results in + * blocking on that transport. For effective non-blocking your server must + * support {@link Protocol#SPDY_3} or {@link Protocol#HTTP_2}. + * + * @throws IOException when the request could not be executed due to a + * connectivity problem or timeout. Because networks can fail during an + * exchange, it is possible that the remote server accepted the request + * before the failure. + */ + public Response execute(Request request) throws IOException { + // Copy the client. Otherwise changes (socket factory, redirect policy, + // etc.) may incorrectly be reflected in the request when it is executed. + OkHttpClient client = copyWithDefaults(); + Job job = new Job(dispatcher, client, request, null); + Response result = job.getResponse(); // Since we don't cancel, this won't be null. + job.engine.releaseConnection(); // Transfer ownership of the body to the caller. + return result; + } + /** * Schedules {@code request} to be executed at some point in the future. The * {@link #getDispatcher dispatcher} defines when the request will run: diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java index 3bec0b608..e053d8844 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java +++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java @@ -266,6 +266,10 @@ public class HttpEngine { return response != null; } + public final ResponseSource responseSource() { + return responseSource; + } + public final Request getRequest() { return request; } diff --git a/okhttp/src/test/java/com/squareup/okhttp/SyncApiTest.java b/okhttp/src/test/java/com/squareup/okhttp/SyncApiTest.java new file mode 100644 index 000000000..ae3c7434f --- /dev/null +++ b/okhttp/src/test/java/com/squareup/okhttp/SyncApiTest.java @@ -0,0 +1,268 @@ +/* + * Copyright (C) 2014 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.RecordingHostnameVerifier; +import com.squareup.okhttp.internal.SslContextBuilder; +import com.squareup.okhttp.mockwebserver.MockResponse; +import com.squareup.okhttp.mockwebserver.MockWebServer; +import com.squareup.okhttp.mockwebserver.RecordedRequest; +import com.squareup.okhttp.mockwebserver.SocketPolicy; +import java.io.File; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.util.UUID; +import javax.net.ssl.SSLContext; +import org.junit.After; +import org.junit.Before; +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; + +public final class SyncApiTest { + private MockWebServer server = new MockWebServer(); + private OkHttpClient client = new OkHttpClient(); + + private static final SSLContext sslContext = SslContextBuilder.localhost(); + private HttpResponseCache cache; + + @Before public void setUp() throws Exception { + String tmp = System.getProperty("java.io.tmpdir"); + File cacheDir = new File(tmp, "HttpCache-" + UUID.randomUUID()); + cache = new HttpResponseCache(cacheDir, Integer.MAX_VALUE); + } + + @After public void tearDown() throws Exception { + server.shutdown(); + cache.delete(); + } + + @Test public void get() throws Exception { + server.enqueue(new MockResponse() + .setBody("abc") + .addHeader("Content-Type: text/plain")); + server.play(); + + Request request = new Request.Builder() + .url(server.getUrl("/")) + .header("User-Agent", "SyncApiTest") + .build(); + + onSuccess(request) + .assertCode(200) + .assertContainsHeaders("Content-Type: text/plain") + .assertBody("abc"); + + assertTrue(server.takeRequest().getHeaders().contains("User-Agent: SyncApiTest")); + } + + @Test public void connectionPooling() throws Exception { + server.enqueue(new MockResponse().setBody("abc")); + server.enqueue(new MockResponse().setBody("def")); + server.enqueue(new MockResponse().setBody("ghi")); + server.play(); + + onSuccess(new Request.Builder().url(server.getUrl("/a")).build()) + .assertBody("abc"); + + onSuccess(new Request.Builder().url(server.getUrl("/b")).build()) + .assertBody("def"); + + onSuccess(new Request.Builder().url(server.getUrl("/c")).build()) + .assertBody("ghi"); + + assertEquals(0, server.takeRequest().getSequenceNumber()); + assertEquals(1, server.takeRequest().getSequenceNumber()); + assertEquals(2, server.takeRequest().getSequenceNumber()); + } + + @Test public void tls() throws Exception { + server.useHttps(sslContext.getSocketFactory(), false); + server.enqueue(new MockResponse() + .setBody("abc") + .addHeader("Content-Type: text/plain")); + server.play(); + + client.setSslSocketFactory(sslContext.getSocketFactory()); + client.setHostnameVerifier(new RecordingHostnameVerifier()); + + onSuccess(new Request.Builder().url(server.getUrl("/")).build()) + .assertHandshake(); + } + + @Test public void recoverFromTlsHandshakeFailure() throws Exception { + server.useHttps(sslContext.getSocketFactory(), false); + server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE)); + server.enqueue(new MockResponse().setBody("abc")); + server.play(); + + client.setSslSocketFactory(sslContext.getSocketFactory()); + client.setHostnameVerifier(new RecordingHostnameVerifier()); + + onSuccess(new Request.Builder().url(server.getUrl("/")).build()) + .assertBody("abc"); + } + + @Test public void post() throws Exception { + server.enqueue(new MockResponse().setBody("abc")); + server.play(); + + Request request = new Request.Builder() + .url(server.getUrl("/")) + .post(Request.Body.create(MediaType.parse("text/plain"), "def")) + .build(); + + onSuccess(request) + .assertCode(200) + .assertBody("abc"); + + RecordedRequest recordedRequest = server.takeRequest(); + assertEquals("def", recordedRequest.getUtf8Body()); + assertEquals("3", recordedRequest.getHeader("Content-Length")); + assertEquals("text/plain; charset=utf-8", recordedRequest.getHeader("Content-Type")); + } + + @Test public void cache() throws Exception { + server.enqueue(new MockResponse().setBody("A").addHeader("ETag: v1")); + server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); + server.play(); + + client.setOkResponseCache(cache); + + onSuccess(new Request.Builder().url(server.getUrl("/")).build()) + .assertCode(200).assertBody("A"); + assertNull(server.takeRequest().getHeader("If-None-Match")); + + onSuccess(new Request.Builder().url(server.getUrl("/")).build()) + .assertCode(200).assertBody("A"); + assertEquals("v1", server.takeRequest().getHeader("If-None-Match")); + } + + @Test public void redirect() throws Exception { + server.enqueue(new MockResponse() + .setResponseCode(301) + .addHeader("Location: /b") + .addHeader("Test", "Redirect from /a to /b") + .setBody("/a has moved!")); + server.enqueue(new MockResponse() + .setResponseCode(302) + .addHeader("Location: /c") + .addHeader("Test", "Redirect from /b to /c") + .setBody("/b has moved!")); + server.enqueue(new MockResponse().setBody("C")); + server.play(); + + onSuccess(new Request.Builder().url(server.getUrl("/a")).build()) + .assertCode(200) + .assertBody("C") + .redirectedBy() + .assertCode(302) + .assertContainsHeaders("Test: Redirect from /b to /c") + .redirectedBy() + .assertCode(301) + .assertContainsHeaders("Test: Redirect from /a to /b"); + + assertEquals(0, server.takeRequest().getSequenceNumber()); // New connection. + assertEquals(1, server.takeRequest().getSequenceNumber()); // Connection reused. + assertEquals(2, server.takeRequest().getSequenceNumber()); // Connection reused again! + } + + @Test public void redirectWithRedirectsDisabled() throws Exception { + client.setFollowProtocolRedirects(false); + server.enqueue(new MockResponse() + .setResponseCode(301) + .addHeader("Location: /b") + .addHeader("Test", "Redirect from /a to /b") + .setBody("/a has moved!")); + server.play(); + + onSuccess(new Request.Builder().url(server.getUrl("/a")).build()) + .assertCode(301) + .assertBody("/a has moved!") + .assertContainsHeaders("Location: /b"); + } + + @Test public void follow20Redirects() throws Exception { + for (int i = 0; i < 20; i++) { + server.enqueue(new MockResponse() + .setResponseCode(301) + .addHeader("Location: /" + (i + 1)) + .setBody("Redirecting to /" + (i + 1))); + } + server.enqueue(new MockResponse().setBody("Success!")); + server.play(); + + onSuccess(new Request.Builder().url(server.getUrl("/0")).build()) + .assertCode(200) + .assertBody("Success!"); + } + + @Test public void doesNotFollow21Redirects() throws Exception { + for (int i = 0; i < 21; i++) { + server.enqueue(new MockResponse() + .setResponseCode(301) + .addHeader("Location: /" + (i + 1)) + .setBody("Redirecting to /" + (i + 1))); + } + server.play(); + + try { + client.execute(new Request.Builder().url(server.getUrl("/0")).build()); + fail(); + } catch (IOException e) { + assertEquals("Too many redirects: 21", e.getMessage()); + } + } + + @Test public void postBodyRetransmittedOnRedirect() throws Exception { + server.enqueue(new MockResponse() + .setResponseCode(302) + .addHeader("Location: /b") + .setBody("Moved to /b !")); + server.enqueue(new MockResponse() + .setBody("This is b.")); + server.play(); + + Request request = new Request.Builder() + .url(server.getUrl("/")) + .post(Request.Body.create(MediaType.parse("text/plain"), "body!")) + .build(); + + onSuccess(request) + .assertCode(200) + .assertBody("This is b."); + + RecordedRequest request1 = server.takeRequest(); + assertEquals("body!", request1.getUtf8Body()); + assertEquals("5", request1.getHeader("Content-Length")); + assertEquals("text/plain; charset=utf-8", request1.getHeader("Content-Type")); + assertEquals(0, request1.getSequenceNumber()); + + RecordedRequest request2 = server.takeRequest(); + assertEquals("body!", request2.getUtf8Body()); + assertEquals("5", request2.getHeader("Content-Length")); + assertEquals("text/plain; charset=utf-8", request2.getHeader("Content-Type")); + assertEquals(1, request2.getSequenceNumber()); + } + + private RecordedResponse onSuccess(Request request) throws IOException { + Response response = client.execute(request); + return new RecordedResponse(request, response, response.body().string(), null); + } +}