InterceptorTest.java revision cdf3c4bc9e853c99d82d4c1dfc907ef2694f2ed7
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 */
16package com.squareup.okhttp;
17
18import com.squareup.okhttp.mockwebserver.MockResponse;
19import com.squareup.okhttp.mockwebserver.MockWebServer;
20import com.squareup.okhttp.mockwebserver.RecordedRequest;
21import java.io.IOException;
22import java.util.Arrays;
23import java.util.List;
24import java.util.Locale;
25import java.util.concurrent.BlockingQueue;
26import java.util.concurrent.LinkedBlockingQueue;
27import java.util.concurrent.SynchronousQueue;
28import java.util.concurrent.ThreadPoolExecutor;
29import java.util.concurrent.TimeUnit;
30import okio.Buffer;
31import okio.BufferedSink;
32import okio.ForwardingSink;
33import okio.ForwardingSource;
34import okio.GzipSink;
35import okio.Okio;
36import okio.Sink;
37import okio.Source;
38import org.junit.Rule;
39import org.junit.Test;
40
41import static org.junit.Assert.assertEquals;
42import static org.junit.Assert.assertNotNull;
43import static org.junit.Assert.assertNull;
44import static org.junit.Assert.assertSame;
45import static org.junit.Assert.fail;
46
47public 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