Home | History | Annotate | Download | only in toolbox
      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&lt;String, String&gt;.
    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