Home | History | Annotate | Download | only in okhttp
      1 /*
      2  * Copyright (C) 2014 Square, Inc.
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.squareup.okhttp;
     17 
     18 import com.squareup.okhttp.mockwebserver.MockResponse;
     19 import com.squareup.okhttp.mockwebserver.MockWebServer;
     20 import com.squareup.okhttp.mockwebserver.RecordedRequest;
     21 import java.io.IOException;
     22 import java.util.Arrays;
     23 import java.util.List;
     24 import java.util.Locale;
     25 import java.util.concurrent.BlockingQueue;
     26 import java.util.concurrent.LinkedBlockingQueue;
     27 import java.util.concurrent.SynchronousQueue;
     28 import java.util.concurrent.ThreadPoolExecutor;
     29 import java.util.concurrent.TimeUnit;
     30 import okio.Buffer;
     31 import okio.BufferedSink;
     32 import okio.ForwardingSink;
     33 import okio.ForwardingSource;
     34 import okio.GzipSink;
     35 import okio.Okio;
     36 import okio.Sink;
     37 import okio.Source;
     38 import org.junit.Rule;
     39 import org.junit.Test;
     40 
     41 import static org.junit.Assert.assertEquals;
     42 import static org.junit.Assert.assertNotNull;
     43 import static org.junit.Assert.assertNull;
     44 import static org.junit.Assert.assertSame;
     45 import static org.junit.Assert.fail;
     46 
     47 public final class InterceptorTest {
     48   @Rule public MockWebServer server = new MockWebServer();
     49 
     50   private OkHttpClient client = new OkHttpClient();
     51   private RecordingCallback callback = new RecordingCallback();
     52 
     53   @Test public void applicationInterceptorsCanShortCircuitResponses() throws Exception {
     54     server.shutdown(); // Accept no connections.
     55 
     56     Request request = new Request.Builder()
     57         .url("https://localhost:1/")
     58         .build();
     59 
     60     final Response interceptorResponse = new Response.Builder()
     61         .request(request)
     62         .protocol(Protocol.HTTP_1_1)
     63         .code(200)
     64         .message("Intercepted!")
     65         .body(ResponseBody.create(MediaType.parse("text/plain; charset=utf-8"), "abc"))
     66         .build();
     67 
     68     client.interceptors().add(new Interceptor() {
     69       @Override public Response intercept(Chain chain) throws IOException {
     70         return interceptorResponse;
     71       }
     72     });
     73 
     74     Response response = client.newCall(request).execute();
     75     assertSame(interceptorResponse, response);
     76   }
     77 
     78   @Test public void networkInterceptorsCannotShortCircuitResponses() throws Exception {
     79     server.enqueue(new MockResponse().setResponseCode(500));
     80 
     81     Interceptor interceptor = new Interceptor() {
     82       @Override public Response intercept(Chain chain) throws IOException {
     83         return new Response.Builder()
     84             .request(chain.request())
     85             .protocol(Protocol.HTTP_1_1)
     86             .code(200)
     87             .message("Intercepted!")
     88             .body(ResponseBody.create(MediaType.parse("text/plain; charset=utf-8"), "abc"))
     89             .build();
     90       }
     91     };
     92     client.networkInterceptors().add(interceptor);
     93 
     94     Request request = new Request.Builder()
     95         .url(server.url("/"))
     96         .build();
     97 
     98     try {
     99       client.newCall(request).execute();
    100       fail();
    101     } catch (IllegalStateException expected) {
    102       assertEquals("network interceptor " + interceptor + " must call proceed() exactly once",
    103           expected.getMessage());
    104     }
    105   }
    106 
    107   @Test public void networkInterceptorsCannotCallProceedMultipleTimes() throws Exception {
    108     server.enqueue(new MockResponse());
    109     server.enqueue(new MockResponse());
    110 
    111     Interceptor interceptor = new Interceptor() {
    112       @Override public Response intercept(Chain chain) throws IOException {
    113         chain.proceed(chain.request());
    114         return chain.proceed(chain.request());
    115       }
    116     };
    117     client.networkInterceptors().add(interceptor);
    118 
    119     Request request = new Request.Builder()
    120         .url(server.url("/"))
    121         .build();
    122 
    123     try {
    124       client.newCall(request).execute();
    125       fail();
    126     } catch (IllegalStateException expected) {
    127       assertEquals("network interceptor " + interceptor + " must call proceed() exactly once",
    128           expected.getMessage());
    129     }
    130   }
    131 
    132   @Test public void networkInterceptorsCannotChangeServerAddress() throws Exception {
    133     server.enqueue(new MockResponse().setResponseCode(500));
    134 
    135     Interceptor interceptor = new Interceptor() {
    136       @Override public Response intercept(Chain chain) throws IOException {
    137         Address address = chain.connection().getRoute().getAddress();
    138         String sameHost = address.getRfc2732Host();
    139         int differentPort = address.getUriPort() + 1;
    140         return chain.proceed(chain.request().newBuilder()
    141             .url(HttpUrl.parse("http://" + sameHost + ":" + differentPort + "/"))
    142             .build());
    143       }
    144     };
    145     client.networkInterceptors().add(interceptor);
    146 
    147     Request request = new Request.Builder()
    148         .url(server.url("/"))
    149         .build();
    150 
    151     try {
    152       client.newCall(request).execute();
    153       fail();
    154     } catch (IllegalStateException expected) {
    155       assertEquals("network interceptor " + interceptor + " must retain the same host and port",
    156           expected.getMessage());
    157     }
    158   }
    159 
    160   @Test public void networkInterceptorsHaveConnectionAccess() throws Exception {
    161     server.enqueue(new MockResponse());
    162 
    163     client.networkInterceptors().add(new Interceptor() {
    164       @Override public Response intercept(Chain chain) throws IOException {
    165         Connection connection = chain.connection();
    166         assertNotNull(connection);
    167         return chain.proceed(chain.request());
    168       }
    169     });
    170 
    171     Request request = new Request.Builder()
    172         .url(server.url("/"))
    173         .build();
    174     client.newCall(request).execute();
    175   }
    176 
    177   @Test public void networkInterceptorsObserveNetworkHeaders() throws Exception {
    178     server.enqueue(new MockResponse()
    179         .setBody(gzip("abcabcabc"))
    180         .addHeader("Content-Encoding: gzip"));
    181 
    182     client.networkInterceptors().add(new Interceptor() {
    183       @Override public Response intercept(Chain chain) throws IOException {
    184         // The network request has everything: User-Agent, Host, Accept-Encoding.
    185         Request networkRequest = chain.request();
    186         assertNotNull(networkRequest.header("User-Agent"));
    187         assertEquals(server.getHostName() + ":" + server.getPort(),
    188             networkRequest.header("Host"));
    189         assertNotNull(networkRequest.header("Accept-Encoding"));
    190 
    191         // The network response also has everything, including the raw gzipped content.
    192         Response networkResponse = chain.proceed(networkRequest);
    193         assertEquals("gzip", networkResponse.header("Content-Encoding"));
    194         return networkResponse;
    195       }
    196     });
    197 
    198     Request request = new Request.Builder()
    199         .url(server.url("/"))
    200         .build();
    201 
    202     // No extra headers in the application's request.
    203     assertNull(request.header("User-Agent"));
    204     assertNull(request.header("Host"));
    205     assertNull(request.header("Accept-Encoding"));
    206 
    207     // No extra headers in the application's response.
    208     Response response = client.newCall(request).execute();
    209     assertNull(request.header("Content-Encoding"));
    210     assertEquals("abcabcabc", response.body().string());
    211   }
    212 
    213   @Test public void applicationInterceptorsRewriteRequestToServer() throws Exception {
    214     rewriteRequestToServer(client.interceptors());
    215   }
    216 
    217   @Test public void networkInterceptorsRewriteRequestToServer() throws Exception {
    218     rewriteRequestToServer(client.networkInterceptors());
    219   }
    220 
    221   private void rewriteRequestToServer(List<Interceptor> interceptors) throws Exception {
    222     server.enqueue(new MockResponse());
    223 
    224     interceptors.add(new Interceptor() {
    225       @Override public Response intercept(Chain chain) throws IOException {
    226         Request originalRequest = chain.request();
    227         return chain.proceed(originalRequest.newBuilder()
    228             .method("POST", uppercase(originalRequest.body()))
    229             .addHeader("OkHttp-Intercepted", "yep")
    230             .build());
    231       }
    232     });
    233 
    234     Request request = new Request.Builder()
    235         .url(server.url("/"))
    236         .addHeader("Original-Header", "foo")
    237         .method("PUT", RequestBody.create(MediaType.parse("text/plain"), "abc"))
    238         .build();
    239 
    240     client.newCall(request).execute();
    241 
    242     RecordedRequest recordedRequest = server.takeRequest();
    243     assertEquals("ABC", recordedRequest.getBody().readUtf8());
    244     assertEquals("foo", recordedRequest.getHeader("Original-Header"));
    245     assertEquals("yep", recordedRequest.getHeader("OkHttp-Intercepted"));
    246     assertEquals("POST", recordedRequest.getMethod());
    247   }
    248 
    249   @Test public void applicationInterceptorsRewriteResponseFromServer() throws Exception {
    250     rewriteResponseFromServer(client.interceptors());
    251   }
    252 
    253   @Test public void networkInterceptorsRewriteResponseFromServer() throws Exception {
    254     rewriteResponseFromServer(client.networkInterceptors());
    255   }
    256 
    257   private void rewriteResponseFromServer(List<Interceptor> interceptors) throws Exception {
    258     server.enqueue(new MockResponse()
    259         .addHeader("Original-Header: foo")
    260         .setBody("abc"));
    261 
    262     interceptors.add(new Interceptor() {
    263       @Override public Response intercept(Chain chain) throws IOException {
    264         Response originalResponse = chain.proceed(chain.request());
    265         return originalResponse.newBuilder()
    266             .body(uppercase(originalResponse.body()))
    267             .addHeader("OkHttp-Intercepted", "yep")
    268             .build();
    269       }
    270     });
    271 
    272     Request request = new Request.Builder()
    273         .url(server.url("/"))
    274         .build();
    275 
    276     Response response = client.newCall(request).execute();
    277     assertEquals("ABC", response.body().string());
    278     assertEquals("yep", response.header("OkHttp-Intercepted"));
    279     assertEquals("foo", response.header("Original-Header"));
    280   }
    281 
    282   @Test public void multipleApplicationInterceptors() throws Exception {
    283     multipleInterceptors(client.interceptors());
    284   }
    285 
    286   @Test public void multipleNetworkInterceptors() throws Exception {
    287     multipleInterceptors(client.networkInterceptors());
    288   }
    289 
    290   private void multipleInterceptors(List<Interceptor> interceptors) throws Exception {
    291     server.enqueue(new MockResponse());
    292 
    293     interceptors.add(new Interceptor() {
    294       @Override public Response intercept(Chain chain) throws IOException {
    295         Request originalRequest = chain.request();
    296         Response originalResponse = chain.proceed(originalRequest.newBuilder()
    297             .addHeader("Request-Interceptor", "Android") // 1. Added first.
    298             .build());
    299         return originalResponse.newBuilder()
    300             .addHeader("Response-Interceptor", "Donut") // 4. Added last.
    301             .build();
    302       }
    303     });
    304     interceptors.add(new Interceptor() {
    305       @Override public Response intercept(Chain chain) throws IOException {
    306         Request originalRequest = chain.request();
    307         Response originalResponse = chain.proceed(originalRequest.newBuilder()
    308             .addHeader("Request-Interceptor", "Bob") // 2. Added second.
    309             .build());
    310         return originalResponse.newBuilder()
    311             .addHeader("Response-Interceptor", "Cupcake") // 3. Added third.
    312             .build();
    313       }
    314     });
    315 
    316     Request request = new Request.Builder()
    317         .url(server.url("/"))
    318         .build();
    319 
    320     Response response = client.newCall(request).execute();
    321     assertEquals(Arrays.asList("Cupcake", "Donut"),
    322         response.headers("Response-Interceptor"));
    323 
    324     RecordedRequest recordedRequest = server.takeRequest();
    325     assertEquals(Arrays.asList("Android", "Bob"),
    326         recordedRequest.getHeaders().values("Request-Interceptor"));
    327   }
    328 
    329   @Test public void asyncApplicationInterceptors() throws Exception {
    330     asyncInterceptors(client.interceptors());
    331   }
    332 
    333   @Test public void asyncNetworkInterceptors() throws Exception {
    334     asyncInterceptors(client.networkInterceptors());
    335   }
    336 
    337   private void asyncInterceptors(List<Interceptor> interceptors) throws Exception {
    338     server.enqueue(new MockResponse());
    339 
    340     interceptors.add(new Interceptor() {
    341       @Override public Response intercept(Chain chain) throws IOException {
    342         Response originalResponse = chain.proceed(chain.request());
    343         return originalResponse.newBuilder()
    344             .addHeader("OkHttp-Intercepted", "yep")
    345             .build();
    346       }
    347     });
    348 
    349     Request request = new Request.Builder()
    350         .url(server.url("/"))
    351         .build();
    352     client.newCall(request).enqueue(callback);
    353 
    354     callback.await(request.httpUrl())
    355         .assertCode(200)
    356         .assertHeader("OkHttp-Intercepted", "yep");
    357   }
    358 
    359   @Test public void applicationInterceptorsCanMakeMultipleRequestsToServer() throws Exception {
    360     server.enqueue(new MockResponse().setBody("a"));
    361     server.enqueue(new MockResponse().setBody("b"));
    362 
    363     client.interceptors().add(new Interceptor() {
    364       @Override public Response intercept(Chain chain) throws IOException {
    365         chain.proceed(chain.request());
    366         return chain.proceed(chain.request());
    367       }
    368     });
    369 
    370     Request request = new Request.Builder()
    371         .url(server.url("/"))
    372         .build();
    373 
    374     Response response = client.newCall(request).execute();
    375     assertEquals(response.body().string(), "b");
    376   }
    377 
    378   /** Make sure interceptors can interact with the OkHttp client. */
    379   @Test public void interceptorMakesAnUnrelatedRequest() throws Exception {
    380     server.enqueue(new MockResponse().setBody("a")); // Fetched by interceptor.
    381     server.enqueue(new MockResponse().setBody("b")); // Fetched directly.
    382 
    383     client.interceptors().add(new Interceptor() {
    384       @Override public Response intercept(Chain chain) throws IOException {
    385         if (chain.request().url().getPath().equals("/b")) {
    386           Request requestA = new Request.Builder()
    387               .url(server.url("/a"))
    388               .build();
    389           Response responseA = client.newCall(requestA).execute();
    390           assertEquals("a", responseA.body().string());
    391         }
    392 
    393         return chain.proceed(chain.request());
    394       }
    395     });
    396 
    397     Request requestB = new Request.Builder()
    398         .url(server.url("/b"))
    399         .build();
    400     Response responseB = client.newCall(requestB).execute();
    401     assertEquals("b", responseB.body().string());
    402   }
    403 
    404   /** Make sure interceptors can interact with the OkHttp client asynchronously. */
    405   @Test public void interceptorMakesAnUnrelatedAsyncRequest() throws Exception {
    406     server.enqueue(new MockResponse().setBody("a")); // Fetched by interceptor.
    407     server.enqueue(new MockResponse().setBody("b")); // Fetched directly.
    408 
    409     client.interceptors().add(new Interceptor() {
    410       @Override public Response intercept(Chain chain) throws IOException {
    411         if (chain.request().url().getPath().equals("/b")) {
    412           Request requestA = new Request.Builder()
    413               .url(server.url("/a"))
    414               .build();
    415 
    416           try {
    417             RecordingCallback callbackA = new RecordingCallback();
    418             client.newCall(requestA).enqueue(callbackA);
    419             callbackA.await(requestA.httpUrl()).assertBody("a");
    420           } catch (Exception e) {
    421             throw new RuntimeException(e);
    422           }
    423         }
    424 
    425         return chain.proceed(chain.request());
    426       }
    427     });
    428 
    429     Request requestB = new Request.Builder()
    430         .url(server.url("/b"))
    431         .build();
    432     RecordingCallback callbackB = new RecordingCallback();
    433     client.newCall(requestB).enqueue(callbackB);
    434     callbackB.await(requestB.httpUrl()).assertBody("b");
    435   }
    436 
    437   @Test public void applicationkInterceptorThrowsRuntimeExceptionSynchronous() throws Exception {
    438     interceptorThrowsRuntimeExceptionSynchronous(client.interceptors());
    439   }
    440 
    441   @Test public void networkInterceptorThrowsRuntimeExceptionSynchronous() throws Exception {
    442     interceptorThrowsRuntimeExceptionSynchronous(client.networkInterceptors());
    443   }
    444 
    445   /**
    446    * When an interceptor throws an unexpected exception, synchronous callers can catch it and deal
    447    * with it.
    448    *
    449    * TODO(jwilson): test that resources are not leaked when this happens.
    450    */
    451   private void interceptorThrowsRuntimeExceptionSynchronous(
    452       List<Interceptor> interceptors) throws Exception {
    453     interceptors.add(new Interceptor() {
    454       @Override public Response intercept(Chain chain) throws IOException {
    455         throw new RuntimeException("boom!");
    456       }
    457     });
    458 
    459     Request request = new Request.Builder()
    460         .url(server.url("/"))
    461         .build();
    462 
    463     try {
    464       client.newCall(request).execute();
    465       fail();
    466     } catch (RuntimeException expected) {
    467       assertEquals("boom!", expected.getMessage());
    468     }
    469   }
    470 
    471   @Test public void applicationInterceptorThrowsRuntimeExceptionAsynchronous() throws Exception {
    472     interceptorThrowsRuntimeExceptionAsynchronous(client.interceptors());
    473   }
    474 
    475   @Test public void networkInterceptorThrowsRuntimeExceptionAsynchronous() throws Exception {
    476     interceptorThrowsRuntimeExceptionAsynchronous(client.networkInterceptors());
    477   }
    478 
    479   @Test public void networkInterceptorModifiedRequestIsReturned() throws IOException {
    480     server.enqueue(new MockResponse());
    481 
    482     Interceptor modifyHeaderInterceptor = new Interceptor() {
    483       @Override public Response intercept(Chain chain) throws IOException {
    484         return chain.proceed(chain.request().newBuilder()
    485           .header("User-Agent", "intercepted request")
    486           .build());
    487       }
    488     };
    489 
    490     client.networkInterceptors().add(modifyHeaderInterceptor);
    491 
    492     Request request = new Request.Builder()
    493         .url(server.url("/"))
    494         .header("User-Agent", "user request")
    495         .build();
    496 
    497     Response response = client.newCall(request).execute();
    498     assertNotNull(response.request().header("User-Agent"));
    499     assertEquals("user request", response.request().header("User-Agent"));
    500     assertEquals("intercepted request", response.networkResponse().request().header("User-Agent"));
    501   }
    502 
    503   /**
    504    * When an interceptor throws an unexpected exception, asynchronous callers are left hanging. The
    505    * exception goes to the uncaught exception handler.
    506    *
    507    * TODO(jwilson): test that resources are not leaked when this happens.
    508    */
    509   private void interceptorThrowsRuntimeExceptionAsynchronous(
    510         List<Interceptor> interceptors) throws Exception {
    511     interceptors.add(new Interceptor() {
    512       @Override public Response intercept(Chain chain) throws IOException {
    513         throw new RuntimeException("boom!");
    514       }
    515     });
    516 
    517     ExceptionCatchingExecutor executor = new ExceptionCatchingExecutor();
    518     client.setDispatcher(new Dispatcher(executor));
    519 
    520     Request request = new Request.Builder()
    521         .url(server.url("/"))
    522         .build();
    523     client.newCall(request).enqueue(callback);
    524 
    525     assertEquals("boom!", executor.takeException().getMessage());
    526   }
    527 
    528   private RequestBody uppercase(final RequestBody original) {
    529     return new RequestBody() {
    530       @Override public MediaType contentType() {
    531         return original.contentType();
    532       }
    533 
    534       @Override public long contentLength() throws IOException {
    535         return original.contentLength();
    536       }
    537 
    538       @Override public void writeTo(BufferedSink sink) throws IOException {
    539         Sink uppercase = uppercase(sink);
    540         BufferedSink bufferedSink = Okio.buffer(uppercase);
    541         original.writeTo(bufferedSink);
    542         bufferedSink.emit();
    543       }
    544     };
    545   }
    546 
    547   private Sink uppercase(final BufferedSink original) {
    548     return new ForwardingSink(original) {
    549       @Override public void write(Buffer source, long byteCount) throws IOException {
    550         original.writeUtf8(source.readUtf8(byteCount).toUpperCase(Locale.US));
    551       }
    552     };
    553   }
    554 
    555   static ResponseBody uppercase(ResponseBody original) throws IOException {
    556     return ResponseBody.create(original.contentType(), original.contentLength(),
    557         Okio.buffer(uppercase(original.source())));
    558   }
    559 
    560   private static Source uppercase(final Source original) {
    561     return new ForwardingSource(original) {
    562       @Override public long read(Buffer sink, long byteCount) throws IOException {
    563         Buffer mixedCase = new Buffer();
    564         long count = original.read(mixedCase, byteCount);
    565         sink.writeUtf8(mixedCase.readUtf8().toUpperCase(Locale.US));
    566         return count;
    567       }
    568     };
    569   }
    570 
    571   private Buffer gzip(String data) throws IOException {
    572     Buffer result = new Buffer();
    573     BufferedSink sink = Okio.buffer(new GzipSink(result));
    574     sink.writeUtf8(data);
    575     sink.close();
    576     return result;
    577   }
    578 
    579   /** Catches exceptions that are otherwise headed for the uncaught exception handler. */
    580   private static class ExceptionCatchingExecutor extends ThreadPoolExecutor {
    581     private final BlockingQueue<Exception> exceptions = new LinkedBlockingQueue<>();
    582 
    583     public ExceptionCatchingExecutor() {
    584       super(1, 1, 0, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
    585     }
    586 
    587     @Override public void execute(final Runnable runnable) {
    588       super.execute(new Runnable() {
    589         @Override public void run() {
    590           try {
    591             runnable.run();
    592           } catch (Exception e) {
    593             exceptions.add(e);
    594           }
    595         }
    596       });
    597     }
    598 
    599     public Exception takeException() throws InterruptedException {
    600       return exceptions.take();
    601     }
    602   }
    603 }
    604