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:
@@ -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.
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -266,6 +266,10 @@ public class HttpEngine {
|
||||
return response != null;
|
||||
}
|
||||
|
||||
public final ResponseSource responseSource() {
|
||||
return responseSource;
|
||||
}
|
||||
|
||||
public final Request getRequest() {
|
||||
return request;
|
||||
}
|
||||
|
||||
268
okhttp/src/test/java/com/squareup/okhttp/SyncApiTest.java
Normal file
268
okhttp/src/test/java/com/squareup/okhttp/SyncApiTest.java
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user