blob: c82fc34a564d8c1c2b90a66ca6654ba4fe959cb2 [file] [log] [blame]
Jean-Baptiste Querud56b88a2012-11-07 07:48:57 -08001/*
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
17package com.android.volley.toolbox;
18
19import android.os.SystemClock;
20
21import com.android.volley.AuthFailureError;
22import com.android.volley.Cache;
23import com.android.volley.Network;
24import com.android.volley.NetworkError;
25import com.android.volley.NetworkResponse;
26import com.android.volley.NoConnectionError;
27import com.android.volley.Request;
28import com.android.volley.RetryPolicy;
29import com.android.volley.ServerError;
30import com.android.volley.TimeoutError;
31import com.android.volley.VolleyError;
32import com.android.volley.VolleyLog;
33
34import org.apache.http.Header;
35import org.apache.http.HttpEntity;
36import org.apache.http.HttpResponse;
37import org.apache.http.HttpStatus;
38import org.apache.http.StatusLine;
39import org.apache.http.conn.ConnectTimeoutException;
40import org.apache.http.impl.cookie.DateUtils;
41
42import java.io.IOException;
43import java.io.InputStream;
44import java.net.MalformedURLException;
45import java.net.SocketTimeoutException;
46import java.util.Date;
47import java.util.HashMap;
48import java.util.Map;
49
50/**
51 * A network performing Volley requests over an {@link HttpStack}.
52 */
53public class BasicNetwork implements Network {
54 protected static final boolean DEBUG = VolleyLog.DEBUG;
55
56 private static int SLOW_REQUEST_THRESHOLD_MS = 3000;
57
58 private static int DEFAULT_POOL_SIZE = 4096;
59
60 protected final HttpStack mHttpStack;
61
62 protected final ByteArrayPool mPool;
63
64 /**
65 * @param httpStack HTTP stack to be used
66 */
67 public BasicNetwork(HttpStack httpStack) {
68 // If a pool isn't passed in, then build a small default pool that will give us a lot of
69 // benefit and not use too much memory.
70 this(httpStack, new ByteArrayPool(DEFAULT_POOL_SIZE));
71 }
72
73 /**
74 * @param httpStack HTTP stack to be used
75 * @param pool a buffer pool that improves GC performance in copy operations
76 */
77 public BasicNetwork(HttpStack httpStack, ByteArrayPool pool) {
78 mHttpStack = httpStack;
79 mPool = pool;
80 }
81
82 @Override
83 public NetworkResponse performRequest(Request<?> request) throws VolleyError {
84 long requestStart = SystemClock.elapsedRealtime();
85 while (true) {
86 HttpResponse httpResponse = null;
87 byte[] responseContents = null;
88 Map<String, String> responseHeaders = new HashMap<String, String>();
89 try {
90 // Gather headers.
91 Map<String, String> headers = new HashMap<String, String>();
92 addCacheHeaders(headers, request.getCacheEntry());
93 httpResponse = mHttpStack.performRequest(request, headers);
94 StatusLine statusLine = httpResponse.getStatusLine();
Jon Boekenoogencd8ce542012-12-18 09:21:13 -080095 int statusCode = statusLine.getStatusCode();
Jean-Baptiste Querud56b88a2012-11-07 07:48:57 -080096
97 responseHeaders = convertHeaders(httpResponse.getAllHeaders());
98 // Handle cache validation.
Jon Boekenoogencd8ce542012-12-18 09:21:13 -080099 if (statusCode == HttpStatus.SC_NOT_MODIFIED) {
Jean-Baptiste Querud56b88a2012-11-07 07:48:57 -0800100 return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED,
Max Zhouc7e64232014-02-03 09:20:20 -0800101 request.getCacheEntry() == null ? null : request.getCacheEntry().data,
102 responseHeaders, true);
Jean-Baptiste Querud56b88a2012-11-07 07:48:57 -0800103 }
104
Ray Collinee8f53532013-09-12 12:24:44 -0700105 // Some responses such as 204s do not have content. We must check.
106 if (httpResponse.getEntity() != null) {
107 responseContents = entityToBytes(httpResponse.getEntity());
108 } else {
109 // Add 0 byte response as a way of honestly representing a
110 // no-content request.
111 responseContents = new byte[0];
112 }
113
Jean-Baptiste Querud56b88a2012-11-07 07:48:57 -0800114 // if the request is slow, log it.
115 long requestLifetime = SystemClock.elapsedRealtime() - requestStart;
116 logSlowRequests(requestLifetime, request, responseContents, statusLine);
117
Reed Ellsworthf2cd36f2013-10-09 09:56:17 -0600118 if (statusCode < 200 || statusCode > 299) {
Jean-Baptiste Querud56b88a2012-11-07 07:48:57 -0800119 throw new IOException();
120 }
Jon Boekenoogencd8ce542012-12-18 09:21:13 -0800121 return new NetworkResponse(statusCode, responseContents, responseHeaders, false);
Jean-Baptiste Querud56b88a2012-11-07 07:48:57 -0800122 } catch (SocketTimeoutException e) {
123 attemptRetryOnException("socket", request, new TimeoutError());
124 } catch (ConnectTimeoutException e) {
125 attemptRetryOnException("connection", request, new TimeoutError());
126 } catch (MalformedURLException e) {
127 throw new RuntimeException("Bad URL " + request.getUrl(), e);
128 } catch (IOException e) {
129 int statusCode = 0;
130 NetworkResponse networkResponse = null;
131 if (httpResponse != null) {
132 statusCode = httpResponse.getStatusLine().getStatusCode();
133 } else {
134 throw new NoConnectionError(e);
135 }
136 VolleyLog.e("Unexpected response code %d for %s", statusCode, request.getUrl());
137 if (responseContents != null) {
138 networkResponse = new NetworkResponse(statusCode, responseContents,
139 responseHeaders, false);
140 if (statusCode == HttpStatus.SC_UNAUTHORIZED ||
141 statusCode == HttpStatus.SC_FORBIDDEN) {
142 attemptRetryOnException("auth",
143 request, new AuthFailureError(networkResponse));
144 } else {
145 // TODO: Only throw ServerError for 5xx status codes.
146 throw new ServerError(networkResponse);
147 }
148 } else {
149 throw new NetworkError(networkResponse);
150 }
151 }
152 }
153 }
154
155 /**
156 * Logs requests that took over SLOW_REQUEST_THRESHOLD_MS to complete.
157 */
158 private void logSlowRequests(long requestLifetime, Request<?> request,
159 byte[] responseContents, StatusLine statusLine) {
160 if (DEBUG || requestLifetime > SLOW_REQUEST_THRESHOLD_MS) {
161 VolleyLog.d("HTTP response for request=<%s> [lifetime=%d], [size=%s], " +
162 "[rc=%d], [retryCount=%s]", request, requestLifetime,
163 responseContents != null ? responseContents.length : "null",
164 statusLine.getStatusCode(), request.getRetryPolicy().getCurrentRetryCount());
165 }
166 }
167
168 /**
169 * Attempts to prepare the request for a retry. If there are no more attempts remaining in the
170 * request's retry policy, a timeout exception is thrown.
171 * @param request The request to use.
172 */
173 private static void attemptRetryOnException(String logPrefix, Request<?> request,
174 VolleyError exception) throws VolleyError {
175 RetryPolicy retryPolicy = request.getRetryPolicy();
176 int oldTimeout = request.getTimeoutMs();
177
178 try {
179 retryPolicy.retry(exception);
180 } catch (VolleyError e) {
181 request.addMarker(
182 String.format("%s-timeout-giveup [timeout=%s]", logPrefix, oldTimeout));
183 throw e;
184 }
185 request.addMarker(String.format("%s-retry [timeout=%s]", logPrefix, oldTimeout));
186 }
187
188 private void addCacheHeaders(Map<String, String> headers, Cache.Entry entry) {
189 // If there's no cache entry, we're done.
190 if (entry == null) {
191 return;
192 }
193
194 if (entry.etag != null) {
195 headers.put("If-None-Match", entry.etag);
196 }
197
198 if (entry.serverDate > 0) {
199 Date refTime = new Date(entry.serverDate);
200 headers.put("If-Modified-Since", DateUtils.formatDate(refTime));
201 }
202 }
203
204 protected void logError(String what, String url, long start) {
205 long now = SystemClock.elapsedRealtime();
206 VolleyLog.v("HTTP ERROR(%s) %d ms to fetch %s", what, (now - start), url);
207 }
208
209 /** Reads the contents of HttpEntity into a byte[]. */
210 private byte[] entityToBytes(HttpEntity entity) throws IOException, ServerError {
211 PoolingByteArrayOutputStream bytes =
212 new PoolingByteArrayOutputStream(mPool, (int) entity.getContentLength());
213 byte[] buffer = null;
214 try {
215 InputStream in = entity.getContent();
216 if (in == null) {
217 throw new ServerError();
218 }
219 buffer = mPool.getBuf(1024);
220 int count;
221 while ((count = in.read(buffer)) != -1) {
222 bytes.write(buffer, 0, count);
223 }
224 return bytes.toByteArray();
225 } finally {
226 try {
227 // Close the InputStream and release the resources by "consuming the content".
228 entity.consumeContent();
229 } catch (IOException e) {
230 // This can happen if there was an exception above that left the entity in
231 // an invalid state.
232 VolleyLog.v("Error occured when calling consumingContent");
233 }
234 mPool.returnBuf(buffer);
235 bytes.close();
236 }
237 }
238
239 /**
240 * Converts Headers[] to Map<String, String>.
241 */
242 private static Map<String, String> convertHeaders(Header[] headers) {
243 Map<String, String> result = new HashMap<String, String>();
244 for (int i = 0; i < headers.length; i++) {
245 result.put(headers[i].getName(), headers[i].getValue());
246 }
247 return result;
248 }
249}