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