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.getUriHost();
    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 networkInterceptorsCanChangeRequestMethodFromGetToPost() throws Exception {
    214     server.enqueue(new MockResponse());
    215 
    216     client.networkInterceptors().add(new Interceptor() {
    217       @Override
    218       public Response intercept(Chain chain) throws IOException {
    219         Request originalRequest = chain.request();
    220         MediaType mediaType = MediaType.parse("text/plain");
    221         RequestBody body = RequestBody.create(mediaType, "abc");
    222         return chain.proceed(originalRequest.newBuilder()
    223             .method("POST", body)
    224             .header("Content-Type", mediaType.toString())
    225             .header("Content-Length", Long.toString(body.contentLength()))
    226             .build());
    227       }
    228     });
    229 
    230     Request request = new Request.Builder()
    231         .url(server.url("/"))
    232         .get()
    233         .build();
    234 
    235     client.newCall(request).execute();
    236 
    237     RecordedRequest recordedRequest = server.takeRequest();
    238     assertEquals("POST", recordedRequest.getMethod());
    239     assertEquals("abc", recordedRequest.getBody().readUtf8());
    240   }
    241 
    242   @Test public void applicationInterceptorsRewriteRequestToServer() throws Exception {
    243     rewriteRequestToServer(client.interceptors());
    244   }
    245 
    246   @Test public void networkInterceptorsRewriteRequestToServer() throws Exception {
    247     rewriteRequestToServer(client.networkInterceptors());
    248   }
    249 
    250   private void rewriteRequestToServer(List<Interceptor> interceptors) throws Exception {
    251     server.enqueue(new MockResponse());
    252 
    253     interceptors.add(new Interceptor() {
    254       @Override public Response intercept(Chain chain) throws IOException {
    255         Request originalRequest = chain.request();
    256         return chain.proceed(originalRequest.newBuilder()
    257             .method("POST", uppercase(originalRequest.body()))
    258             .addHeader("OkHttp-Intercepted", "yep")
    259             .build());
    260       }
    261     });
    262 
    263     Request request = new Request.Builder()
    264         .url(server.url("/"))
    265         .addHeader("Original-Header", "foo")
    266         .method("PUT", RequestBody.create(MediaType.parse("text/plain"), "abc"))
    267         .build();
    268 
    269     client.newCall(request).execute();
    270 
    271     RecordedRequest recordedRequest = server.takeRequest();
    272     assertEquals("ABC", recordedRequest.getBody().readUtf8());
    273     assertEquals("foo", recordedRequest.getHeader("Original-Header"));
    274     assertEquals("yep", recordedRequest.getHeader("OkHttp-Intercepted"));
    275     assertEquals("POST", recordedRequest.getMethod());
    276   }
    277 
    278   @Test public void applicationInterceptorsRewriteResponseFromServer() throws Exception {
    279     rewriteResponseFromServer(client.interceptors());
    280   }
    281 
    282   @Test public void networkInterceptorsRewriteResponseFromServer() throws Exception {
    283     rewriteResponseFromServer(client.networkInterceptors());
    284   }
    285 
    286   private void rewriteResponseFromServer(List<Interceptor> interceptors) throws Exception {
    287     server.enqueue(new MockResponse()
    288         .addHeader("Original-Header: foo")
    289         .setBody("abc"));
    290 
    291     interceptors.add(new Interceptor() {
    292       @Override public Response intercept(Chain chain) throws IOException {
    293         Response originalResponse = chain.proceed(chain.request());
    294         return originalResponse.newBuilder()
    295             .body(uppercase(originalResponse.body()))
    296             .addHeader("OkHttp-Intercepted", "yep")
    297             .build();
    298       }
    299     });
    300 
    301     Request request = new Request.Builder()
    302         .url(server.url("/"))
    303         .build();
    304 
    305     Response response = client.newCall(request).execute();
    306     assertEquals("ABC", response.body().string());
    307     assertEquals("yep", response.header("OkHttp-Intercepted"));
    308     assertEquals("foo", response.header("Original-Header"));
    309   }
    310 
    311   @Test public void multipleApplicationInterceptors() throws Exception {
    312     multipleInterceptors(client.interceptors());
    313   }
    314 
    315   @Test public void multipleNetworkInterceptors() throws Exception {
    316     multipleInterceptors(client.networkInterceptors());
    317   }
    318 
    319   private void multipleInterceptors(List<Interceptor> interceptors) throws Exception {
    320     server.enqueue(new MockResponse());
    321 
    322     interceptors.add(new Interceptor() {
    323       @Override public Response intercept(Chain chain) throws IOException {
    324         Request originalRequest = chain.request();
    325         Response originalResponse = chain.proceed(originalRequest.newBuilder()
    326             .addHeader("Request-Interceptor", "Android") // 1. Added first.
    327             .build());
    328         return originalResponse.newBuilder()
    329             .addHeader("Response-Interceptor", "Donut") // 4. Added last.
    330             .build();
    331       }
    332     });
    333     interceptors.add(new Interceptor() {
    334       @Override public Response intercept(Chain chain) throws IOException {
    335         Request originalRequest = chain.request();
    336         Response originalResponse = chain.proceed(originalRequest.newBuilder()
    337             .addHeader("Request-Interceptor", "Bob") // 2. Added second.
    338             .build());
    339         return originalResponse.newBuilder()
    340             .addHeader("Response-Interceptor", "Cupcake") // 3. Added third.
    341             .build();
    342       }
    343     });
    344 
    345     Request request = new Request.Builder()
    346         .url(server.url("/"))
    347         .build();
    348 
    349     Response response = client.newCall(request).execute();
    350     assertEquals(Arrays.asList("Cupcake", "Donut"),
    351         response.headers("Response-Interceptor"));
    352 
    353     RecordedRequest recordedRequest = server.takeRequest();
    354     assertEquals(Arrays.asList("Android", "Bob"),
    355         recordedRequest.getHeaders().values("Request-Interceptor"));
    356   }
    357 
    358   @Test public void asyncApplicationInterceptors() throws Exception {
    359     asyncInterceptors(client.interceptors());
    360   }
    361 
    362   @Test public void asyncNetworkInterceptors() throws Exception {
    363     asyncInterceptors(client.networkInterceptors());
    364   }
    365 
    366   private void asyncInterceptors(List<Interceptor> interceptors) throws Exception {
    367     server.enqueue(new MockResponse());
    368 
    369     interceptors.add(new Interceptor() {
    370       @Override public Response intercept(Chain chain) throws IOException {
    371         Response originalResponse = chain.proceed(chain.request());
    372         return originalResponse.newBuilder()
    373             .addHeader("OkHttp-Intercepted", "yep")
    374             .build();
    375       }
    376     });
    377 
    378     Request request = new Request.Builder()
    379         .url(server.url("/"))
    380         .build();
    381     client.newCall(request).enqueue(callback);
    382 
    383     callback.await(request.httpUrl())
    384         .assertCode(200)
    385         .assertHeader("OkHttp-Intercepted", "yep");
    386   }
    387 
    388   @Test public void applicationInterceptorsCanMakeMultipleRequestsToServer() throws Exception {
    389     server.enqueue(new MockResponse().setBody("a"));
    390     server.enqueue(new MockResponse().setBody("b"));
    391 
    392     client.interceptors().add(new Interceptor() {
    393       @Override public Response intercept(Chain chain) throws IOException {
    394         Response response1 = chain.proceed(chain.request());
    395         response1.body().close();
    396         return chain.proceed(chain.request());
    397       }
    398     });
    399 
    400     Request request = new Request.Builder()
    401         .url(server.url("/"))
    402         .build();
    403 
    404     Response response = client.newCall(request).execute();
    405     assertEquals(response.body().string(), "b");
    406   }
    407 
    408   /** Make sure interceptors can interact with the OkHttp client. */
    409   @Test public void interceptorMakesAnUnrelatedRequest() throws Exception {
    410     server.enqueue(new MockResponse().setBody("a")); // Fetched by interceptor.
    411     server.enqueue(new MockResponse().setBody("b")); // Fetched directly.
    412 
    413     client.interceptors().add(new Interceptor() {
    414       @Override public Response intercept(Chain chain) throws IOException {
    415         if (chain.request().url().getPath().equals("/b")) {
    416           Request requestA = new Request.Builder()
    417               .url(server.url("/a"))
    418               .build();
    419           Response responseA = client.newCall(requestA).execute();
    420           assertEquals("a", responseA.body().string());
    421         }
    422 
    423         return chain.proceed(chain.request());
    424       }
    425     });
    426 
    427     Request requestB = new Request.Builder()
    428         .url(server.url("/b"))
    429         .build();
    430     Response responseB = client.newCall(requestB).execute();
    431     assertEquals("b", responseB.body().string());
    432   }
    433 
    434   /** Make sure interceptors can interact with the OkHttp client asynchronously. */
    435   @Test public void interceptorMakesAnUnrelatedAsyncRequest() throws Exception {
    436     server.enqueue(new MockResponse().setBody("a")); // Fetched by interceptor.
    437     server.enqueue(new MockResponse().setBody("b")); // Fetched directly.
    438 
    439     client.interceptors().add(new Interceptor() {
    440       @Override public Response intercept(Chain chain) throws IOException {
    441         if (chain.request().url().getPath().equals("/b")) {
    442           Request requestA = new Request.Builder()
    443               .url(server.url("/a"))
    444               .build();
    445 
    446           try {
    447             RecordingCallback callbackA = new RecordingCallback();
    448             client.newCall(requestA).enqueue(callbackA);
    449             callbackA.await(requestA.httpUrl()).assertBody("a");
    450           } catch (Exception e) {
    451             throw new RuntimeException(e);
    452           }
    453         }
    454 
    455         return chain.proceed(chain.request());
    456       }
    457     });
    458 
    459     Request requestB = new Request.Builder()
    460         .url(server.url("/b"))
    461         .build();
    462     RecordingCallback callbackB = new RecordingCallback();
    463     client.newCall(requestB).enqueue(callbackB);
    464     callbackB.await(requestB.httpUrl()).assertBody("b");
    465   }
    466 
    467   @Test public void applicationkInterceptorThrowsRuntimeExceptionSynchronous() throws Exception {
    468     interceptorThrowsRuntimeExceptionSynchronous(client.interceptors());
    469   }
    470 
    471   @Test public void networkInterceptorThrowsRuntimeExceptionSynchronous() throws Exception {
    472     interceptorThrowsRuntimeExceptionSynchronous(client.networkInterceptors());
    473   }
    474 
    475   /**
    476    * When an interceptor throws an unexpected exception, synchronous callers can catch it and deal
    477    * with it.
    478    *
    479    * TODO(jwilson): test that resources are not leaked when this happens.
    480    */
    481   private void interceptorThrowsRuntimeExceptionSynchronous(
    482       List<Interceptor> interceptors) throws Exception {
    483     interceptors.add(new Interceptor() {
    484       @Override public Response intercept(Chain chain) throws IOException {
    485         throw new RuntimeException("boom!");
    486       }
    487     });
    488 
    489     Request request = new Request.Builder()
    490         .url(server.url("/"))
    491         .build();
    492 
    493     try {
    494       client.newCall(request).execute();
    495       fail();
    496     } catch (RuntimeException expected) {
    497       assertEquals("boom!", expected.getMessage());
    498     }
    499   }
    500 
    501   @Test public void networkInterceptorModifiedRequestIsReturned() throws IOException {
    502     server.enqueue(new MockResponse());
    503 
    504     Interceptor modifyHeaderInterceptor = new Interceptor() {
    505       @Override public Response intercept(Chain chain) throws IOException {
    506         return chain.proceed(chain.request().newBuilder()
    507           .header("User-Agent", "intercepted request")
    508           .build());
    509       }
    510     };
    511 
    512     client.networkInterceptors().add(modifyHeaderInterceptor);
    513 
    514     Request request = new Request.Builder()
    515         .url(server.url("/"))
    516         .header("User-Agent", "user request")
    517         .build();
    518 
    519     Response response = client.newCall(request).execute();
    520     assertNotNull(response.request().header("User-Agent"));
    521     assertEquals("user request", response.request().header("User-Agent"));
    522     assertEquals("intercepted request", response.networkResponse().request().header("User-Agent"));
    523   }
    524 
    525   @Test public void applicationInterceptorThrowsRuntimeExceptionAsynchronous() throws Exception {
    526     interceptorThrowsRuntimeExceptionAsynchronous(client.interceptors());
    527   }
    528 
    529   @Test public void networkInterceptorThrowsRuntimeExceptionAsynchronous() throws Exception {
    530     interceptorThrowsRuntimeExceptionAsynchronous(client.networkInterceptors());
    531   }
    532 
    533   /**
    534    * When an interceptor throws an unexpected exception, asynchronous callers are left hanging. The
    535    * exception goes to the uncaught exception handler.
    536    *
    537    * TODO(jwilson): test that resources are not leaked when this happens.
    538    */
    539   private void interceptorThrowsRuntimeExceptionAsynchronous(
    540         List<Interceptor> interceptors) throws Exception {
    541     interceptors.add(new Interceptor() {
    542       @Override public Response intercept(Chain chain) throws IOException {
    543         throw new RuntimeException("boom!");
    544       }
    545     });
    546 
    547     ExceptionCatchingExecutor executor = new ExceptionCatchingExecutor();
    548     client.setDispatcher(new Dispatcher(executor));
    549 
    550     Request request = new Request.Builder()
    551         .url(server.url("/"))
    552         .build();
    553     client.newCall(request).enqueue(callback);
    554 
    555     assertEquals("boom!", executor.takeException().getMessage());
    556   }
    557 
    558   @Test public void applicationInterceptorReturnsNull() throws Exception {
    559     server.enqueue(new MockResponse());
    560 
    561     Interceptor interceptor = new Interceptor() {
    562       @Override public Response intercept(Chain chain) throws IOException {
    563         chain.proceed(chain.request());
    564         return null;
    565       }
    566     };
    567     client.interceptors().add(interceptor);
    568 
    569     ExceptionCatchingExecutor executor = new ExceptionCatchingExecutor();
    570     client.setDispatcher(new Dispatcher(executor));
    571 
    572     Request request = new Request.Builder()
    573         .url(server.url("/"))
    574         .build();
    575     try {
    576       client.newCall(request).execute();
    577       fail();
    578     } catch (NullPointerException expected) {
    579       assertEquals("application interceptor " + interceptor
    580           + " returned null", expected.getMessage());
    581     }
    582   }
    583 
    584   @Test public void networkInterceptorReturnsNull() throws Exception {
    585     server.enqueue(new MockResponse());
    586 
    587     Interceptor interceptor = new Interceptor() {
    588       @Override public Response intercept(Chain chain) throws IOException {
    589         chain.proceed(chain.request());
    590         return null;
    591       }
    592     };
    593     client.networkInterceptors().add(interceptor);
    594 
    595     ExceptionCatchingExecutor executor = new ExceptionCatchingExecutor();
    596     client.setDispatcher(new Dispatcher(executor));
    597 
    598     Request request = new Request.Builder()
    599         .url(server.url("/"))
    600         .build();
    601     try {
    602       client.newCall(request).execute();
    603       fail();
    604     } catch (NullPointerException expected) {
    605       assertEquals("network interceptor " + interceptor + " returned null", expected.getMessage());
    606     }
    607   }
    608 
    609   private RequestBody uppercase(final RequestBody original) {
    610     return new RequestBody() {
    611       @Override public MediaType contentType() {
    612         return original.contentType();
    613       }
    614 
    615       @Override public long contentLength() throws IOException {
    616         return original.contentLength();
    617       }
    618 
    619       @Override public void writeTo(BufferedSink sink) throws IOException {
    620         Sink uppercase = uppercase(sink);
    621         BufferedSink bufferedSink = Okio.buffer(uppercase);
    622         original.writeTo(bufferedSink);
    623         bufferedSink.emit();
    624       }
    625     };
    626   }
    627 
    628   private Sink uppercase(final BufferedSink original) {
    629     return new ForwardingSink(original) {
    630       @Override public void write(Buffer source, long byteCount) throws IOException {
    631         original.writeUtf8(source.readUtf8(byteCount).toUpperCase(Locale.US));
    632       }
    633     };
    634   }
    635 
    636   static ResponseBody uppercase(ResponseBody original) throws IOException {
    637     return ResponseBody.create(original.contentType(), original.contentLength(),
    638         Okio.buffer(uppercase(original.source())));
    639   }
    640 
    641   private static Source uppercase(final Source original) {
    642     return new ForwardingSource(original) {
    643       @Override public long read(Buffer sink, long byteCount) throws IOException {
    644         Buffer mixedCase = new Buffer();
    645         long count = original.read(mixedCase, byteCount);
    646         sink.writeUtf8(mixedCase.readUtf8().toUpperCase(Locale.US));
    647         return count;
    648       }
    649     };
    650   }
    651 
    652   private Buffer gzip(String data) throws IOException {
    653     Buffer result = new Buffer();
    654     BufferedSink sink = Okio.buffer(new GzipSink(result));
    655     sink.writeUtf8(data);
    656     sink.close();
    657     return result;
    658   }
    659 
    660   /** Catches exceptions that are otherwise headed for the uncaught exception handler. */
    661   private static class ExceptionCatchingExecutor extends ThreadPoolExecutor {
    662     private final BlockingQueue<Exception> exceptions = new LinkedBlockingQueue<>();
    663 
    664     public ExceptionCatchingExecutor() {
    665       super(1, 1, 0, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
    666     }
    667 
    668     @Override public void execute(final Runnable runnable) {
    669       super.execute(new Runnable() {
    670         @Override public void run() {
    671           try {
    672             runnable.run();
    673           } catch (Exception e) {
    674             exceptions.add(e);
    675           }
    676         }
    677       });
    678     }
    679 
    680     public Exception takeException() throws InterruptedException {
    681       return exceptions.take();
    682     }
    683   }
    684 }
    685