1 /*
2  * Copyright (C) 2020 The Android Open Source Project
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 
17 package com.android.volley.toolbox;
18 
19 import android.os.SystemClock;
20 import androidx.annotation.Nullable;
21 import com.android.volley.AuthFailureError;
22 import com.android.volley.Cache;
23 import com.android.volley.ClientError;
24 import com.android.volley.Header;
25 import com.android.volley.NetworkError;
26 import com.android.volley.NetworkResponse;
27 import com.android.volley.NoConnectionError;
28 import com.android.volley.Request;
29 import com.android.volley.RetryPolicy;
30 import com.android.volley.ServerError;
31 import com.android.volley.TimeoutError;
32 import com.android.volley.VolleyError;
33 import com.android.volley.VolleyLog;
34 import java.io.IOException;
35 import java.io.InputStream;
36 import java.net.HttpURLConnection;
37 import java.net.MalformedURLException;
38 import java.net.SocketTimeoutException;
39 import java.util.List;
40 
41 /**
42  * Utility class for methods that are shared between {@link BasicNetwork} and {@link
43  * BasicAsyncNetwork}
44  */
45 public final class NetworkUtility {
46     private static final int SLOW_REQUEST_THRESHOLD_MS = 3000;
47 
NetworkUtility()48     private NetworkUtility() {}
49 
50     /** Logs requests that took over SLOW_REQUEST_THRESHOLD_MS to complete. */
logSlowRequests( long requestLifetime, Request<?> request, byte[] responseContents, int statusCode)51     static void logSlowRequests(
52             long requestLifetime, Request<?> request, byte[] responseContents, int statusCode) {
53         if (VolleyLog.DEBUG || requestLifetime > SLOW_REQUEST_THRESHOLD_MS) {
54             VolleyLog.d(
55                     "HTTP response for request=<%s> [lifetime=%d], [size=%s], "
56                             + "[rc=%d], [retryCount=%s]",
57                     request,
58                     requestLifetime,
59                     responseContents != null ? responseContents.length : "null",
60                     statusCode,
61                     request.getRetryPolicy().getCurrentRetryCount());
62         }
63     }
64 
getNotModifiedNetworkResponse( Request<?> request, long requestDuration, List<Header> responseHeaders)65     static NetworkResponse getNotModifiedNetworkResponse(
66             Request<?> request, long requestDuration, List<Header> responseHeaders) {
67         Cache.Entry entry = request.getCacheEntry();
68         if (entry == null) {
69             return new NetworkResponse(
70                     HttpURLConnection.HTTP_NOT_MODIFIED,
71                     /* data= */ null,
72                     /* notModified= */ true,
73                     requestDuration,
74                     responseHeaders);
75         }
76         // Combine cached and response headers so the response will be complete.
77         List<Header> combinedHeaders = HttpHeaderParser.combineHeaders(responseHeaders, entry);
78         return new NetworkResponse(
79                 HttpURLConnection.HTTP_NOT_MODIFIED,
80                 entry.data,
81                 /* notModified= */ true,
82                 requestDuration,
83                 combinedHeaders);
84     }
85 
86     /** Reads the contents of an InputStream into a byte[]. */
inputStreamToBytes(InputStream in, int contentLength, ByteArrayPool pool)87     static byte[] inputStreamToBytes(InputStream in, int contentLength, ByteArrayPool pool)
88             throws IOException {
89         PoolingByteArrayOutputStream bytes = new PoolingByteArrayOutputStream(pool, contentLength);
90         byte[] buffer = null;
91         try {
92             buffer = pool.getBuf(1024);
93             int count;
94             while ((count = in.read(buffer)) != -1) {
95                 bytes.write(buffer, 0, count);
96             }
97             return bytes.toByteArray();
98         } finally {
99             try {
100                 // Close the InputStream and release the resources by "consuming the content".
101                 if (in != null) {
102                     in.close();
103                 }
104             } catch (IOException e) {
105                 // This can happen if there was an exception above that left the stream in
106                 // an invalid state.
107                 VolleyLog.v("Error occurred when closing InputStream");
108             }
109             pool.returnBuf(buffer);
110             bytes.close();
111         }
112     }
113 
114     /**
115      * Attempts to prepare the request for a retry. If there are no more attempts remaining in the
116      * request's retry policy, a timeout exception is thrown.
117      *
118      * @param request The request to use.
119      */
attemptRetryOnException( final String logPrefix, final Request<?> request, final VolleyError exception)120     private static void attemptRetryOnException(
121             final String logPrefix, final Request<?> request, final VolleyError exception)
122             throws VolleyError {
123         final RetryPolicy retryPolicy = request.getRetryPolicy();
124         final int oldTimeout = request.getTimeoutMs();
125         try {
126             retryPolicy.retry(exception);
127         } catch (VolleyError e) {
128             request.addMarker(
129                     String.format("%s-timeout-giveup [timeout=%s]", logPrefix, oldTimeout));
130             throw e;
131         }
132         request.addMarker(String.format("%s-retry [timeout=%s]", logPrefix, oldTimeout));
133     }
134 
135     /**
136      * Based on the exception thrown, decides whether to attempt to retry, or to throw the error.
137      * Also handles logging.
138      */
handleException( Request<?> request, IOException exception, long requestStartMs, @Nullable HttpResponse httpResponse, @Nullable byte[] responseContents)139     static void handleException(
140             Request<?> request,
141             IOException exception,
142             long requestStartMs,
143             @Nullable HttpResponse httpResponse,
144             @Nullable byte[] responseContents)
145             throws VolleyError {
146         if (exception instanceof SocketTimeoutException) {
147             attemptRetryOnException("socket", request, new TimeoutError());
148         } else if (exception instanceof MalformedURLException) {
149             throw new RuntimeException("Bad URL " + request.getUrl(), exception);
150         } else {
151             int statusCode;
152             if (httpResponse != null) {
153                 statusCode = httpResponse.getStatusCode();
154             } else {
155                 if (request.shouldRetryConnectionErrors()) {
156                     attemptRetryOnException("connection", request, new NoConnectionError());
157                     return;
158                 } else {
159                     throw new NoConnectionError(exception);
160                 }
161             }
162             VolleyLog.e("Unexpected response code %d for %s", statusCode, request.getUrl());
163             NetworkResponse networkResponse;
164             if (responseContents != null) {
165                 List<Header> responseHeaders;
166                 responseHeaders = httpResponse.getHeaders();
167                 networkResponse =
168                         new NetworkResponse(
169                                 statusCode,
170                                 responseContents,
171                                 /* notModified= */ false,
172                                 SystemClock.elapsedRealtime() - requestStartMs,
173                                 responseHeaders);
174                 if (statusCode == HttpURLConnection.HTTP_UNAUTHORIZED
175                         || statusCode == HttpURLConnection.HTTP_FORBIDDEN) {
176                     attemptRetryOnException("auth", request, new AuthFailureError(networkResponse));
177                 } else if (statusCode >= 400 && statusCode <= 499) {
178                     // Don't retry other client errors.
179                     throw new ClientError(networkResponse);
180                 } else if (statusCode >= 500 && statusCode <= 599) {
181                     if (request.shouldRetryServerErrors()) {
182                         attemptRetryOnException(
183                                 "server", request, new ServerError(networkResponse));
184                     } else {
185                         throw new ServerError(networkResponse);
186                     }
187                 } else {
188                     // 3xx? No reason to retry.
189                     throw new ServerError(networkResponse);
190                 }
191             } else {
192                 attemptRetryOnException("network", request, new NetworkError());
193             }
194         }
195     }
196 }
197