1 /* 2 * Copyright (C) 2011 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 21 import com.android.volley.AuthFailureError; 22 import com.android.volley.Cache; 23 import com.android.volley.Cache.Entry; 24 import com.android.volley.ClientError; 25 import com.android.volley.Header; 26 import com.android.volley.Network; 27 import com.android.volley.NetworkError; 28 import com.android.volley.NetworkResponse; 29 import com.android.volley.NoConnectionError; 30 import com.android.volley.Request; 31 import com.android.volley.RetryPolicy; 32 import com.android.volley.ServerError; 33 import com.android.volley.TimeoutError; 34 import com.android.volley.VolleyError; 35 import com.android.volley.VolleyLog; 36 37 import java.io.IOException; 38 import java.io.InputStream; 39 import java.net.HttpURLConnection; 40 import java.net.MalformedURLException; 41 import java.net.SocketTimeoutException; 42 import java.util.ArrayList; 43 import java.util.Collections; 44 import java.util.HashMap; 45 import java.util.List; 46 import java.util.Map; 47 import java.util.Set; 48 import java.util.TreeMap; 49 import java.util.TreeSet; 50 51 /** 52 * A network performing Volley requests over an {@link HttpStack}. 53 */ 54 public class BasicNetwork implements Network { 55 protected static final boolean DEBUG = VolleyLog.DEBUG; 56 57 private static final int SLOW_REQUEST_THRESHOLD_MS = 3000; 58 59 private static final int DEFAULT_POOL_SIZE = 4096; 60 61 /** 62 * @deprecated Should never have been exposed in the API. This field may be removed in a future 63 * release of Volley. 64 */ 65 @Deprecated 66 protected final HttpStack mHttpStack; 67 68 private final BaseHttpStack mBaseHttpStack; 69 70 protected final ByteArrayPool mPool; 71 72 /** 73 * @param httpStack HTTP stack to be used 74 * @deprecated use {@link #BasicNetwork(BaseHttpStack)} instead to avoid depending on Apache 75 * HTTP. This method may be removed in a future release of Volley. 76 */ 77 @Deprecated 78 public BasicNetwork(HttpStack httpStack) { 79 // If a pool isn't passed in, then build a small default pool that will give us a lot of 80 // benefit and not use too much memory. 81 this(httpStack, new ByteArrayPool(DEFAULT_POOL_SIZE)); 82 } 83 84 /** 85 * @param httpStack HTTP stack to be used 86 * @param pool a buffer pool that improves GC performance in copy operations 87 * @deprecated use {@link #BasicNetwork(BaseHttpStack, ByteArrayPool)} instead to avoid 88 * depending on Apache HTTP. This method may be removed in a future release of 89 * Volley. 90 */ 91 @Deprecated 92 public BasicNetwork(HttpStack httpStack, ByteArrayPool pool) { 93 mHttpStack = httpStack; 94 mBaseHttpStack = new AdaptedHttpStack(httpStack); 95 mPool = pool; 96 } 97 98 /** 99 * @param httpStack HTTP stack to be used 100 */ 101 public BasicNetwork(BaseHttpStack httpStack) { 102 // If a pool isn't passed in, then build a small default pool that will give us a lot of 103 // benefit and not use too much memory. 104 this(httpStack, new ByteArrayPool(DEFAULT_POOL_SIZE)); 105 } 106 107 /** 108 * @param httpStack HTTP stack to be used 109 * @param pool a buffer pool that improves GC performance in copy operations 110 */ 111 public BasicNetwork(BaseHttpStack httpStack, ByteArrayPool pool) { 112 mBaseHttpStack = httpStack; 113 // Populate mHttpStack for backwards compatibility, since it is a protected field. However, 114 // we won't use it directly here, so clients which don't access it directly won't need to 115 // depend on Apache HTTP. 116 mHttpStack = httpStack; 117 mPool = pool; 118 } 119 120 @Override 121 public NetworkResponse performRequest(Request<?> request) throws VolleyError { 122 long requestStart = SystemClock.elapsedRealtime(); 123 while (true) { 124 HttpResponse httpResponse = null; 125 byte[] responseContents = null; 126 List<Header> responseHeaders = Collections.emptyList(); 127 try { 128 // Gather headers. 129 Map<String, String> additionalRequestHeaders = 130 getCacheHeaders(request.getCacheEntry()); 131 httpResponse = mBaseHttpStack.executeRequest(request, additionalRequestHeaders); 132 int statusCode = httpResponse.getStatusCode(); 133 134 responseHeaders = httpResponse.getHeaders(); 135 // Handle cache validation. 136 if (statusCode == HttpURLConnection.HTTP_NOT_MODIFIED) { 137 Entry entry = request.getCacheEntry(); 138 if (entry == null) { 139 return new NetworkResponse(HttpURLConnection.HTTP_NOT_MODIFIED, null, true, 140 SystemClock.elapsedRealtime() - requestStart, responseHeaders); 141 } 142 // Combine cached and response headers so the response will be complete. 143 List<Header> combinedHeaders = combineHeaders(responseHeaders, entry); 144 return new NetworkResponse(HttpURLConnection.HTTP_NOT_MODIFIED, entry.data, 145 true, SystemClock.elapsedRealtime() - requestStart, combinedHeaders); 146 } 147 148 // Some responses such as 204s do not have content. We must check. 149 InputStream inputStream = httpResponse.getContent(); 150 if (inputStream != null) { 151 responseContents = 152 inputStreamToBytes(inputStream, httpResponse.getContentLength()); 153 } else { 154 // Add 0 byte response as a way of honestly representing a 155 // no-content request. 156 responseContents = new byte[0]; 157 } 158 159 // if the request is slow, log it. 160 long requestLifetime = SystemClock.elapsedRealtime() - requestStart; 161 logSlowRequests(requestLifetime, request, responseContents, statusCode); 162 163 if (statusCode < 200 || statusCode > 299) { 164 throw new IOException(); 165 } 166 return new NetworkResponse(statusCode, responseContents, false, 167 SystemClock.elapsedRealtime() - requestStart, responseHeaders); 168 } catch (SocketTimeoutException e) { 169 attemptRetryOnException("socket", request, new TimeoutError()); 170 } catch (MalformedURLException e) { 171 throw new RuntimeException("Bad URL " + request.getUrl(), e); 172 } catch (IOException e) { 173 int statusCode; 174 if (httpResponse != null) { 175 statusCode = httpResponse.getStatusCode(); 176 } else { 177 throw new NoConnectionError(e); 178 } 179 VolleyLog.e("Unexpected response code %d for %s", statusCode, request.getUrl()); 180 NetworkResponse networkResponse; 181 if (responseContents != null) { 182 networkResponse = new NetworkResponse(statusCode, responseContents, false, 183 SystemClock.elapsedRealtime() - requestStart, responseHeaders); 184 if (statusCode == HttpURLConnection.HTTP_UNAUTHORIZED || 185 statusCode == HttpURLConnection.HTTP_FORBIDDEN) { 186 attemptRetryOnException("auth", 187 request, new AuthFailureError(networkResponse)); 188 } else if (statusCode >= 400 && statusCode <= 499) { 189 // Don't retry other client errors. 190 throw new ClientError(networkResponse); 191 } else if (statusCode >= 500 && statusCode <= 599) { 192 if (request.shouldRetryServerErrors()) { 193 attemptRetryOnException("server", 194 request, new ServerError(networkResponse)); 195 } else { 196 throw new ServerError(networkResponse); 197 } 198 } else { 199 // 3xx? No reason to retry. 200 throw new ServerError(networkResponse); 201 } 202 } else { 203 attemptRetryOnException("network", request, new NetworkError()); 204 } 205 } 206 } 207 } 208 209 /** 210 * Logs requests that took over SLOW_REQUEST_THRESHOLD_MS to complete. 211 */ 212 private void logSlowRequests(long requestLifetime, Request<?> request, 213 byte[] responseContents, int statusCode) { 214 if (DEBUG || requestLifetime > SLOW_REQUEST_THRESHOLD_MS) { 215 VolleyLog.d("HTTP response for request=<%s> [lifetime=%d], [size=%s], " + 216 "[rc=%d], [retryCount=%s]", request, requestLifetime, 217 responseContents != null ? responseContents.length : "null", 218 statusCode, request.getRetryPolicy().getCurrentRetryCount()); 219 } 220 } 221 222 /** 223 * Attempts to prepare the request for a retry. If there are no more attempts remaining in the 224 * request's retry policy, a timeout exception is thrown. 225 * @param request The request to use. 226 */ 227 private static void attemptRetryOnException(String logPrefix, Request<?> request, 228 VolleyError exception) throws VolleyError { 229 RetryPolicy retryPolicy = request.getRetryPolicy(); 230 int oldTimeout = request.getTimeoutMs(); 231 232 try { 233 retryPolicy.retry(exception); 234 } catch (VolleyError e) { 235 request.addMarker( 236 String.format("%s-timeout-giveup [timeout=%s]", logPrefix, oldTimeout)); 237 throw e; 238 } 239 request.addMarker(String.format("%s-retry [timeout=%s]", logPrefix, oldTimeout)); 240 } 241 242 private Map<String, String> getCacheHeaders(Cache.Entry entry) { 243 // If there's no cache entry, we're done. 244 if (entry == null) { 245 return Collections.emptyMap(); 246 } 247 248 Map<String, String> headers = new HashMap<>(); 249 250 if (entry.etag != null) { 251 headers.put("If-None-Match", entry.etag); 252 } 253 254 if (entry.lastModified > 0) { 255 headers.put("If-Modified-Since", 256 HttpHeaderParser.formatEpochAsRfc1123(entry.lastModified)); 257 } 258 259 return headers; 260 } 261 262 protected void logError(String what, String url, long start) { 263 long now = SystemClock.elapsedRealtime(); 264 VolleyLog.v("HTTP ERROR(%s) %d ms to fetch %s", what, (now - start), url); 265 } 266 267 /** Reads the contents of an InputStream into a byte[]. */ 268 private byte[] inputStreamToBytes(InputStream in, int contentLength) 269 throws IOException, ServerError { 270 PoolingByteArrayOutputStream bytes = 271 new PoolingByteArrayOutputStream(mPool, contentLength); 272 byte[] buffer = null; 273 try { 274 if (in == null) { 275 throw new ServerError(); 276 } 277 buffer = mPool.getBuf(1024); 278 int count; 279 while ((count = in.read(buffer)) != -1) { 280 bytes.write(buffer, 0, count); 281 } 282 return bytes.toByteArray(); 283 } finally { 284 try { 285 // Close the InputStream and release the resources by "consuming the content". 286 if (in != null) { 287 in.close(); 288 } 289 } catch (IOException e) { 290 // This can happen if there was an exception above that left the stream in 291 // an invalid state. 292 VolleyLog.v("Error occurred when closing InputStream"); 293 } 294 mPool.returnBuf(buffer); 295 bytes.close(); 296 } 297 } 298 299 /** 300 * Converts Headers[] to Map<String, String>. 301 * 302 * @deprecated Should never have been exposed in the API. This method may be removed in a future 303 * release of Volley. 304 */ 305 @Deprecated 306 protected static Map<String, String> convertHeaders(Header[] headers) { 307 Map<String, String> result = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); 308 for (int i = 0; i < headers.length; i++) { 309 result.put(headers[i].getName(), headers[i].getValue()); 310 } 311 return result; 312 } 313 314 /** 315 * Combine cache headers with network response headers for an HTTP 304 response. 316 * 317 * <p>An HTTP 304 response does not have all header fields. We have to use the header fields 318 * from the cache entry plus the new ones from the response. See also: 319 * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5 320 * 321 * @param responseHeaders Headers from the network response. 322 * @param entry The cached response. 323 * @return The combined list of headers. 324 */ 325 private static List<Header> combineHeaders(List<Header> responseHeaders, Entry entry) { 326 // First, create a case-insensitive set of header names from the network 327 // response. 328 Set<String> headerNamesFromNetworkResponse = 329 new TreeSet<>(String.CASE_INSENSITIVE_ORDER); 330 if (!responseHeaders.isEmpty()) { 331 for (Header header : responseHeaders) { 332 headerNamesFromNetworkResponse.add(header.getName()); 333 } 334 } 335 336 // Second, add headers from the cache entry to the network response as long as 337 // they didn't appear in the network response, which should take precedence. 338 List<Header> combinedHeaders = new ArrayList<>(responseHeaders); 339 if (entry.allResponseHeaders != null) { 340 if (!entry.allResponseHeaders.isEmpty()) { 341 for (Header header : entry.allResponseHeaders) { 342 if (!headerNamesFromNetworkResponse.contains(header.getName())) { 343 combinedHeaders.add(header); 344 } 345 } 346 } 347 } else { 348 // Legacy caches only have entry.responseHeaders. 349 if (!entry.responseHeaders.isEmpty()) { 350 for (Map.Entry<String, String> header : entry.responseHeaders.entrySet()) { 351 if (!headerNamesFromNetworkResponse.contains(header.getKey())) { 352 combinedHeaders.add(new Header(header.getKey(), header.getValue())); 353 } 354 } 355 } 356 } 357 return combinedHeaders; 358 } 359 } 360