1 /*
2  * Copyright (C) 2016 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 okhttp3.internal.http;
17
18 import java.io.FileNotFoundException;
19 import java.io.IOException;
20 import java.io.InterruptedIOException;
21 import java.net.ProtocolException;
22 import java.net.Proxy;
23 import java.net.SocketTimeoutException;
24 import java.security.cert.CertificateException;
25 import javax.annotation.Nullable;
26 import javax.net.ssl.SSLHandshakeException;
27 import javax.net.ssl.SSLPeerUnverifiedException;
28 import okhttp3.HttpUrl;
29 import okhttp3.Interceptor;
30 import okhttp3.OkHttpClient;
31 import okhttp3.Request;
32 import okhttp3.RequestBody;
33 import okhttp3.Response;
34 import okhttp3.Route;
35 import okhttp3.internal.Internal;
36 import okhttp3.internal.connection.Exchange;
37 import okhttp3.internal.connection.RouteException;
38 import okhttp3.internal.connection.Transmitter;
39 import okhttp3.internal.http2.ConnectionShutdownException;
40
41 import static java.net.HttpURLConnection.HTTP_CLIENT_TIMEOUT;
42 import static java.net.HttpURLConnection.HTTP_MOVED_PERM;
43 import static java.net.HttpURLConnection.HTTP_MOVED_TEMP;
44 import static java.net.HttpURLConnection.HTTP_MULT_CHOICE;
45 import static java.net.HttpURLConnection.HTTP_PROXY_AUTH;
46 import static java.net.HttpURLConnection.HTTP_SEE_OTHER;
47 import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
48 import static java.net.HttpURLConnection.HTTP_UNAVAILABLE;
49 import static okhttp3.internal.Util.closeQuietly;
50 import static okhttp3.internal.Util.sameConnection;
51 import static okhttp3.internal.http.StatusLine.HTTP_PERM_REDIRECT;
52 import static okhttp3.internal.http.StatusLine.HTTP_TEMP_REDIRECT;
53
54 /**
55  * This interceptor recovers from failures and follows redirects as necessary. It may throw an
56  * {@link IOException} if the call was canceled.
57  */

58 public final class RetryAndFollowUpInterceptor implements Interceptor {
59   /**
60    * How many redirects and auth challenges should we attempt? Chrome follows 21 redirects; Firefox,
61    * curl, and wget follow 20; Safari follows 16; and HTTP/1.0 recommends 5.
62    */

63   private static final int MAX_FOLLOW_UPS = 20;
64
65   private final OkHttpClient client;
66
67   public RetryAndFollowUpInterceptor(OkHttpClient client) {
68     this.client = client;
69   }
70
71   @Override public Response intercept(Chain chain) throws IOException {
72     Request request = chain.request();
73     RealInterceptorChain realChain = (RealInterceptorChain) chain;
74     Transmitter transmitter = realChain.transmitter();
75
76     int followUpCount = 0;
77     Response priorResponse = null;
78     while (true) {
79       transmitter.prepareToConnect(request);
80
81       if (transmitter.isCanceled()) {
82         throw new IOException("Canceled");
83       }
84
85       Response response;
86       boolean success = false;
87       try {
88         response = realChain.proceed(request, transmitter, null);
89         success = true;
90       } catch (RouteException e) {
91         // The attempt to connect via a route failed. The request will not have been sent.
92         if (!recover(e.getLastConnectException(), transmitter, false, request)) {
93           throw e.getFirstConnectException();
94         }
95         continue;
96       } catch (IOException e) {
97         // An attempt to communicate with a server failed. The request may have been sent.
98         boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
99         if (!recover(e, transmitter, requestSendStarted, request)) throw e;
100         continue;
101       } finally {
102         // The network call threw an exception. Release any resources.
103         if (!success) {
104           transmitter.exchangeDoneDueToException();
105         }
106       }
107
108       // Attach the prior response if it exists. Such responses never have a body.
109       if (priorResponse != null) {
110         response = response.newBuilder()
111             .priorResponse(priorResponse.newBuilder()
112                     .body(null)
113                     .build())
114             .build();
115       }
116
117       Exchange exchange = Internal.instance.exchange(response);
118       Route route = exchange != null ? exchange.connection().route() : null;
119       Request followUp = followUpRequest(response, route);
120
121       if (followUp == null) {
122         if (exchange != null && exchange.isDuplex()) {
123           transmitter.timeoutEarlyExit();
124         }
125         return response;
126       }
127
128       RequestBody followUpBody = followUp.body();
129       if (followUpBody != null && followUpBody.isOneShot()) {
130         return response;
131       }
132
133       closeQuietly(response.body());
134       if (transmitter.hasExchange()) {
135         exchange.detachWithViolence();
136       }
137
138       if (++followUpCount > MAX_FOLLOW_UPS) {
139         throw new ProtocolException("Too many follow-up requests: " + followUpCount);
140       }
141
142       request = followUp;
143       priorResponse = response;
144     }
145   }
146
147   /**
148    * Report and attempt to recover from a failure to communicate with a server. Returns true if
149    * {@code e} is recoverable, or false if the failure is permanent. Requests with a body can only
150    * be recovered if the body is buffered or if the failure occurred before the request has been
151    * sent.
152    */

153   private boolean recover(IOException e, Transmitter transmitter,
154       boolean requestSendStarted, Request userRequest) {
155     // The application layer has forbidden retries.
156     if (!client.retryOnConnectionFailure()) return false;
157
158     // We can't send the request body again.
159     if (requestSendStarted && requestIsOneShot(e, userRequest)) return false;
160
161     // This exception is fatal.
162     if (!isRecoverable(e, requestSendStarted)) return false;
163
164     // No more routes to attempt.
165     if (!transmitter.canRetry()) return false;
166
167     // For failure recovery, use the same route selector with a new connection.
168     return true;
169   }
170
171   private boolean requestIsOneShot(IOException e, Request userRequest) {
172     RequestBody requestBody = userRequest.body();
173     return (requestBody != null && requestBody.isOneShot())
174         || e instanceof FileNotFoundException;
175   }
176
177   private boolean isRecoverable(IOException e, boolean requestSendStarted) {
178     // If there was a protocol problem, don't recover.
179     if (e instanceof ProtocolException) {
180       return false;
181     }
182
183     // If there was an interruption don't recover, but if there was a timeout connecting to a route
184     // we should try the next route (if there is one).
185     if (e instanceof InterruptedIOException) {
186       return e instanceof SocketTimeoutException && !requestSendStarted;
187     }
188
189     // Look for known client-side or negotiation errors that are unlikely to be fixed by trying
190     // again with a different route.
191     if (e instanceof SSLHandshakeException) {
192       // If the problem was a CertificateException from the X509TrustManager,
193       // do not retry.
194       if (e.getCause() instanceof CertificateException) {
195         return false;
196       }
197     }
198     if (e instanceof SSLPeerUnverifiedException) {
199       // e.g. a certificate pinning error.
200       return false;
201     }
202
203     // An example of one we might want to retry with a different route is a problem connecting to a
204     // proxy and would manifest as a standard IOException. Unless it is one we know we should not
205     // retry, we return true and try a new route.
206     return true;
207   }
208
209   /**
210    * Figures out the HTTP request to make in response to receiving {@code userResponse}. This will
211    * either add authentication headers, follow redirects or handle a client request timeout. If a
212    * follow-up is either unnecessary or not applicable, this returns null.
213    */

214   private Request followUpRequest(Response userResponse, @Nullable Route route) throws IOException {
215     if (userResponse == nullthrow new IllegalStateException();
216     int responseCode = userResponse.code();
217
218     final String method = userResponse.request().method();
219     switch (responseCode) {
220       case HTTP_PROXY_AUTH:
221         Proxy selectedProxy = route != null
222             ? route.proxy()
223             : client.proxy();
224         if (selectedProxy.type() != Proxy.Type.HTTP) {
225           throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy");
226         }
227         return client.proxyAuthenticator().authenticate(route, userResponse);
228
229       case HTTP_UNAUTHORIZED:
230         return client.authenticator().authenticate(route, userResponse);
231
232       case HTTP_PERM_REDIRECT:
233       case HTTP_TEMP_REDIRECT:
234         // "If the 307 or 308 status code is received in response to a request other than GET
235         // or HEAD, the user agent MUST NOT automatically redirect the request"
236         if (!method.equals("GET") && !method.equals("HEAD")) {
237           return null;
238         }
239         // fall-through
240       case HTTP_MULT_CHOICE:
241       case HTTP_MOVED_PERM:
242       case HTTP_MOVED_TEMP:
243       case HTTP_SEE_OTHER:
244         // Does the client allow redirects?
245         if (!client.followRedirects()) return null;
246
247         String location = userResponse.header("Location");
248         if (location == nullreturn null;
249         HttpUrl url = userResponse.request().url().resolve(location);
250
251         // Don't follow redirects to unsupported protocols.
252         if (url == nullreturn null;
253
254         // If configured, don't follow redirects between SSL and non-SSL.
255         boolean sameScheme = url.scheme().equals(userResponse.request().url().scheme());
256         if (!sameScheme && !client.followSslRedirects()) return null;
257
258         // Most redirects don't include a request body.
259         Request.Builder requestBuilder = userResponse.request().newBuilder();
260         if (HttpMethod.permitsRequestBody(method)) {
261           final boolean maintainBody = HttpMethod.redirectsWithBody(method);
262           if (HttpMethod.redirectsToGet(method)) {
263             requestBuilder.method("GET"null);
264           } else {
265             RequestBody requestBody = maintainBody ? userResponse.request().body() : null;
266             requestBuilder.method(method, requestBody);
267           }
268           if (!maintainBody) {
269             requestBuilder.removeHeader("Transfer-Encoding");
270             requestBuilder.removeHeader("Content-Length");
271             requestBuilder.removeHeader("Content-Type");
272           }
273         }
274
275         // When redirecting across hosts, drop all authentication headers. This
276         // is potentially annoying to the application layer since they have no
277         // way to retain them.
278         if (!sameConnection(userResponse.request().url(), url)) {
279           requestBuilder.removeHeader("Authorization");
280         }
281
282         return requestBuilder.url(url).build();
283
284       case HTTP_CLIENT_TIMEOUT:
285         // 408's are rare in practice, but some servers like HAProxy use this response code. The
286         // spec says that we may repeat the request without modifications. Modern browsers also
287         // repeat the request (even non-idempotent ones.)
288         if (!client.retryOnConnectionFailure()) {
289           // The application layer has directed us not to retry the request.
290           return null;
291         }
292
293         RequestBody requestBody = userResponse.request().body();
294         if (requestBody != null && requestBody.isOneShot()) {
295           return null;
296         }
297
298         if (userResponse.priorResponse() != null
299             && userResponse.priorResponse().code() == HTTP_CLIENT_TIMEOUT) {
300           // We attempted to retry and got another timeout. Give up.
301           return null;
302         }
303
304         if (retryAfter(userResponse, 0) > 0) {
305           return null;
306         }
307
308         return userResponse.request();
309
310       case HTTP_UNAVAILABLE:
311         if (userResponse.priorResponse() != null
312             && userResponse.priorResponse().code() == HTTP_UNAVAILABLE) {
313           // We attempted to retry and got another timeout. Give up.
314           return null;
315         }
316
317         if (retryAfter(userResponse, Integer.MAX_VALUE) == 0) {
318           // specifically received an instruction to retry without delay
319           return userResponse.request();
320         }
321
322         return null;
323
324       default:
325         return null;
326     }
327   }
328
329   private int retryAfter(Response userResponse, int defaultDelay) {
330     String header = userResponse.header("Retry-After");
331
332     if (header == null) {
333       return defaultDelay;
334     }
335
336     // https://tools.ietf.org/html/rfc7231#section-7.1.3
337     // currently ignores a HTTP-date, and assumes any non int 0 is a delay
338     if (header.matches("\\d+")) {
339       return Integer.valueOf(header);
340     }
341
342     return Integer.MAX_VALUE;
343   }
344 }
345