1
0
mirror of https://github.com/square/okhttp.git synced 2026-01-21 03:41:07 +03:00

Merge pull request #552 from square/adrian.sync-api

Added blocking alternative to HttpUrlConnection
This commit is contained in:
Jesse Wilson
2014-02-22 16:15:38 -05:00
5 changed files with 344 additions and 39 deletions

View File

@@ -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.
}

View File

@@ -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();
}

View File

@@ -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.
*
* <p>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}.
*
* <p>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.
*
* <h3>Non-blocking responses</h3>
*
* <p>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.
*
* <p>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:

View File

@@ -266,6 +266,10 @@ public class HttpEngine {
return response != null;
}
public final ResponseSource responseSource() {
return responseSource;
}
public final Request getRequest() {
return request;
}

View File

@@ -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);
}
}