• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2014 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.net.urlconnection;
6 
7 import android.net.TrafficStats;
8 import android.os.Build;
9 import android.util.Log;
10 import android.util.Pair;
11 
12 import org.chromium.net.CronetEngine;
13 import org.chromium.net.CronetException;
14 import org.chromium.net.ExperimentalUrlRequest;
15 import org.chromium.net.UrlRequest;
16 import org.chromium.net.UrlResponseInfo;
17 
18 import androidx.annotation.VisibleForTesting;
19 
20 import java.io.FileNotFoundException;
21 import java.io.IOException;
22 import java.io.InputStream;
23 import java.io.OutputStream;
24 import java.net.HttpURLConnection;
25 import java.net.MalformedURLException;
26 import java.net.ProtocolException;
27 import java.net.URL;
28 import java.net.URLConnection;
29 import java.nio.ByteBuffer;
30 import java.util.AbstractMap;
31 import java.util.ArrayList;
32 import java.util.Collections;
33 import java.util.List;
34 import java.util.Map;
35 import java.util.TreeMap;
36 
37 /**
38  * An implementation of {@link HttpURLConnection} that uses Cronet to send
39  * requests and receive responses.
40  * {@hide}
41  */
42 public class CronetHttpURLConnection extends HttpURLConnection {
43     private static final String TAG = CronetHttpURLConnection.class.getSimpleName();
44     private static final String CONTENT_LENGTH = "Content-Length";
45     private final CronetEngine mCronetEngine;
46     private final MessageLoop mMessageLoop;
47     private UrlRequest mRequest;
48     private final List<Pair<String, String>> mRequestHeaders;
49     private boolean mTrafficStatsTagSet;
50     private int mTrafficStatsTag;
51     private boolean mTrafficStatsUidSet;
52     private int mTrafficStatsUid;
53 
54     private CronetInputStream mInputStream;
55     private CronetOutputStream mOutputStream;
56     private UrlResponseInfo mResponseInfo;
57     private IOException mException;
58     private boolean mOnRedirectCalled;
59     // Whether response headers are received, the request is failed, or the request is canceled.
60     private boolean mHasResponseHeadersOrCompleted;
61     private List<Map.Entry<String, String>> mResponseHeadersList;
62     private Map<String, List<String>> mResponseHeadersMap;
63 
CronetHttpURLConnection(URL url, CronetEngine cronetEngine)64     public CronetHttpURLConnection(URL url, CronetEngine cronetEngine) {
65         super(url);
66         mCronetEngine = cronetEngine;
67         mMessageLoop = new MessageLoop();
68         mInputStream = new CronetInputStream(this);
69         mRequestHeaders = new ArrayList<Pair<String, String>>();
70     }
71 
72     /**
73      * Opens a connection to the resource. If the connect method is called when
74      * the connection has already been opened (indicated by the connected field
75      * having the value {@code true}), the call is ignored.
76      */
77     @Override
connect()78     public void connect() throws IOException {
79         getOutputStream();
80         // If request is started in getOutputStream, calling startRequest()
81         // again has no effect.
82         startRequest();
83     }
84 
85     /**
86      * Releases this connection so that its resources may be either reused or
87      * closed.
88      */
89     @Override
disconnect()90     public void disconnect() {
91         // Disconnect before connection is made should have no effect.
92         if (connected) {
93             mRequest.cancel();
94         }
95     }
96 
97     /** Returns the response message returned by the remote HTTP server. */
98     @Override
getResponseMessage()99     public String getResponseMessage() throws IOException {
100         getResponse();
101         return mResponseInfo.getHttpStatusText();
102     }
103 
104     /** Returns the response code returned by the remote HTTP server. */
105     @Override
getResponseCode()106     public int getResponseCode() throws IOException {
107         getResponse();
108         return mResponseInfo.getHttpStatusCode();
109     }
110 
111     /** Returns an unmodifiable map of the response-header fields and values. */
112     @Override
getHeaderFields()113     public Map<String, List<String>> getHeaderFields() {
114         try {
115             getResponse();
116         } catch (IOException e) {
117             return Collections.emptyMap();
118         }
119         return getAllHeaders();
120     }
121 
122     /**
123      * Returns the value of the named header field. If called on a connection
124      * that sets the same header multiple times with possibly different values,
125      * only the last value is returned.
126      */
127     @Override
getHeaderField(String fieldName)128     public final String getHeaderField(String fieldName) {
129         try {
130             getResponse();
131         } catch (IOException e) {
132             return null;
133         }
134         Map<String, List<String>> map = getAllHeaders();
135         if (!map.containsKey(fieldName)) {
136             return null;
137         }
138         List<String> values = map.get(fieldName);
139         return values.get(values.size() - 1);
140     }
141 
142     /**
143      * Returns the name of the header field at the given position {@code pos}, or {@code null}
144      * if there are fewer than {@code pos} fields.
145      */
146     @Override
getHeaderFieldKey(int pos)147     public final String getHeaderFieldKey(int pos) {
148         Map.Entry<String, String> header = getHeaderFieldEntry(pos);
149         if (header == null) {
150             return null;
151         }
152         return header.getKey();
153     }
154 
155     /**
156      * Returns the header value at the field position {@code pos} or {@code null} if the header
157      * has fewer than {@code pos} fields.
158      */
159     @Override
getHeaderField(int pos)160     public final String getHeaderField(int pos) {
161         Map.Entry<String, String> header = getHeaderFieldEntry(pos);
162         if (header == null) {
163             return null;
164         }
165         return header.getValue();
166     }
167 
168     /**
169      * Returns an InputStream for reading data from the resource pointed by this
170      * {@link java.net.URLConnection}.
171      * @throws FileNotFoundException if http response code is equal or greater
172      *             than {@link HTTP_BAD_REQUEST}.
173      * @throws IOException If the request gets a network error or HTTP error
174      *             status code, or if the caller tried to read the response body
175      *             of a redirect when redirects are disabled.
176      */
177     @Override
getInputStream()178     public InputStream getInputStream() throws IOException {
179         getResponse();
180         if (!instanceFollowRedirects && mOnRedirectCalled) {
181             throw new IOException("Cannot read response body of a redirect.");
182         }
183         // Emulate default implementation's behavior to throw
184         // FileNotFoundException when we get a 400 and above.
185         if (mResponseInfo.getHttpStatusCode() >= HTTP_BAD_REQUEST) {
186             throw new FileNotFoundException(url.toString());
187         }
188         return mInputStream;
189     }
190 
191     /**
192      * Returns an {@link OutputStream} for writing data to this {@link URLConnection}.
193      * @throws IOException if no {@code OutputStream} could be created.
194      */
195     @Override
getOutputStream()196     public OutputStream getOutputStream() throws IOException {
197         if (mOutputStream == null && doOutput) {
198             if (connected) {
199                 throw new ProtocolException(
200                         "Cannot write to OutputStream after receiving response.");
201             }
202             if (isChunkedUpload()) {
203                 mOutputStream = new CronetChunkedOutputStream(this, chunkLength, mMessageLoop);
204                 // Start the request now since all headers can be sent.
205                 startRequest();
206             } else {
207                 long fixedStreamingModeContentLength = getStreamingModeContentLength();
208                 if (fixedStreamingModeContentLength != -1) {
209                     mOutputStream =
210                             new CronetFixedModeOutputStream(
211                                     this, fixedStreamingModeContentLength, mMessageLoop);
212                     // Start the request now since all headers can be sent.
213                     startRequest();
214                 } else {
215                     // For the buffered case, start the request only when
216                     // content-length bytes are received, or when a
217                     // connect action is initiated by the consumer.
218                     Log.d(TAG, "Outputstream is being buffered in memory.");
219                     String length = getRequestProperty(CONTENT_LENGTH);
220                     if (length == null) {
221                         mOutputStream = new CronetBufferedOutputStream(this);
222                     } else {
223                         long lengthParsed = Long.parseLong(length);
224                         mOutputStream = new CronetBufferedOutputStream(this, lengthParsed);
225                     }
226                 }
227             }
228         }
229         return mOutputStream;
230     }
231 
232     /**
233      * Helper method to get content length passed in by
234      * {@link #setFixedLengthStreamingMode}
235      */
getStreamingModeContentLength()236     private long getStreamingModeContentLength() {
237         // Calling setFixedLengthStreamingMode(long) does not seem to set fixedContentLength (same
238         // for setFixedLengthStreamingMode(int) and fixedContentLengthLong). Check both to get this
239         // right.
240         long contentLength = fixedContentLength;
241         if (fixedContentLengthLong != -1) {
242             contentLength = fixedContentLengthLong;
243         }
244 
245         return contentLength;
246     }
247 
248     /** Starts the request if {@code connected} is false. */
startRequest()249     private void startRequest() throws IOException {
250         if (connected) {
251             return;
252         }
253         final ExperimentalUrlRequest.Builder requestBuilder =
254                 (ExperimentalUrlRequest.Builder)
255                         mCronetEngine.newUrlRequestBuilder(
256                                 getURL().toString(), new CronetUrlRequestCallback(), mMessageLoop);
257         if (doOutput) {
258             if (method.equals("GET")) {
259                 method = "POST";
260             }
261             if (mOutputStream != null) {
262                 requestBuilder.setUploadDataProvider(
263                         mOutputStream.getUploadDataProvider(), mMessageLoop);
264                 if (getRequestProperty(CONTENT_LENGTH) == null && !isChunkedUpload()) {
265                     addRequestProperty(
266                             CONTENT_LENGTH,
267                             Long.toString(mOutputStream.getUploadDataProvider().getLength()));
268                 }
269                 // Tells mOutputStream that startRequest() has been called, so
270                 // the underlying implementation can prepare for reading if needed.
271                 mOutputStream.setConnected();
272             } else {
273                 if (getRequestProperty(CONTENT_LENGTH) == null) {
274                     addRequestProperty(CONTENT_LENGTH, "0");
275                 }
276             }
277             // Default Content-Type to application/x-www-form-urlencoded
278             if (getRequestProperty("Content-Type") == null) {
279                 addRequestProperty("Content-Type", "application/x-www-form-urlencoded");
280             }
281         }
282         for (Pair<String, String> requestHeader : mRequestHeaders) {
283             requestBuilder.addHeader(requestHeader.first, requestHeader.second);
284         }
285         if (!getUseCaches()) {
286             requestBuilder.disableCache();
287         }
288         // Set HTTP method.
289         requestBuilder.setHttpMethod(method);
290         if (checkTrafficStatsTag()) {
291             requestBuilder.setTrafficStatsTag(mTrafficStatsTag);
292         }
293         if (checkTrafficStatsUid()) {
294             requestBuilder.setTrafficStatsUid(mTrafficStatsUid);
295         }
296 
297         mRequest = requestBuilder.build();
298         // Start the request.
299         mRequest.start();
300         connected = true;
301     }
302 
checkTrafficStatsTag()303     private boolean checkTrafficStatsTag() {
304         if (mTrafficStatsTagSet) {
305             return true;
306         }
307 
308         int tag = TrafficStats.getThreadStatsTag();
309         if (tag != -1) {
310             mTrafficStatsTag = tag;
311             mTrafficStatsTagSet = true;
312         }
313 
314         return mTrafficStatsTagSet;
315     }
316 
checkTrafficStatsUid()317     private boolean checkTrafficStatsUid() {
318         if (mTrafficStatsUidSet) {
319             return true;
320         }
321 
322         // TrafficStats#getThreadStatsUid() is available on API level 28+.
323         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
324             return false;
325         }
326 
327         int uid = TrafficStats.getThreadStatsUid();
328         if (uid != -1) {
329             mTrafficStatsUid = uid;
330             mTrafficStatsUidSet = true;
331         }
332 
333         return mTrafficStatsUidSet;
334     }
335 
336     /**
337      * Returns an input stream from the server in the case of an error such as
338      * the requested file has not been found on the remote server.
339      */
340     @Override
getErrorStream()341     public InputStream getErrorStream() {
342         try {
343             getResponse();
344         } catch (IOException e) {
345             return null;
346         }
347         if (mResponseInfo.getHttpStatusCode() >= HTTP_BAD_REQUEST) {
348             return mInputStream;
349         }
350         return null;
351     }
352 
353     /** Adds the given property to the request header. */
354     @Override
addRequestProperty(String key, String value)355     public final void addRequestProperty(String key, String value) {
356         setRequestPropertyInternal(key, value, false);
357     }
358 
359     /** Sets the value of the specified request header field. */
360     @Override
setRequestProperty(String key, String value)361     public final void setRequestProperty(String key, String value) {
362         setRequestPropertyInternal(key, value, true);
363     }
364 
setRequestPropertyInternal(String key, String value, boolean overwrite)365     private final void setRequestPropertyInternal(String key, String value, boolean overwrite) {
366         if (connected) {
367             throw new IllegalStateException(
368                     "Cannot modify request property after connection is made.");
369         }
370         int index = findRequestProperty(key);
371         if (index >= 0) {
372             if (overwrite) {
373                 mRequestHeaders.remove(index);
374             } else {
375                 // Cronet does not support adding multiple headers
376                 // of the same key, see crbug.com/432719 for more details.
377                 throw new UnsupportedOperationException(
378                         "Cannot add multiple headers of the same key, "
379                                 + key
380                                 + ". crbug.com/432719.");
381             }
382         }
383         // Adds the new header at the end of mRequestHeaders.
384         mRequestHeaders.add(Pair.create(key, value));
385     }
386 
387     /**
388      * Returns an unmodifiable map of general request properties used by this
389      * connection.
390      */
391     @Override
getRequestProperties()392     public Map<String, List<String>> getRequestProperties() {
393         if (connected) {
394             throw new IllegalStateException(
395                     "Cannot access request headers after connection is set.");
396         }
397         Map<String, List<String>> map =
398                 new TreeMap<String, List<String>>(String.CASE_INSENSITIVE_ORDER);
399         for (Pair<String, String> entry : mRequestHeaders) {
400             if (map.containsKey(entry.first)) {
401                 // This should not happen due to setRequestPropertyInternal.
402                 throw new IllegalStateException("Should not have multiple values.");
403             } else {
404                 List<String> values = new ArrayList<String>();
405                 values.add(entry.second);
406                 map.put(entry.first, Collections.unmodifiableList(values));
407             }
408         }
409         return Collections.unmodifiableMap(map);
410     }
411 
412     /**
413      * Returns the value of the request header property specified by {@code
414      * key} or {@code null} if there is no key with this name.
415      */
416     @Override
getRequestProperty(String key)417     public String getRequestProperty(String key) {
418         int index = findRequestProperty(key);
419         if (index >= 0) {
420             return mRequestHeaders.get(index).second;
421         }
422         return null;
423     }
424 
425     /** Returns whether this connection uses a proxy server. */
426     @Override
usingProxy()427     public boolean usingProxy() {
428         // TODO(xunjieli): implement this.
429         return false;
430     }
431 
432     @Override
setConnectTimeout(int timeout)433     public void setConnectTimeout(int timeout) {
434         // Per-request connect timeout is not supported because of late binding.
435         // Sockets are assigned to requests according to request priorities
436         // when sockets are connected. This requires requests with the same host,
437         // domain and port to have same timeout.
438         Log.d(TAG, "setConnectTimeout is not supported by CronetHttpURLConnection");
439     }
440 
441     /**
442      * Used by {@link CronetInputStream} to get more data from the network
443      * stack. This should only be called after the request has started. Note
444      * that this call might block if there isn't any more data to be read.
445      * Since byteBuffer is passed to the UrlRequest, it must be a direct
446      * ByteBuffer.
447      */
448     @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
getMoreData(ByteBuffer byteBuffer)449     public void getMoreData(ByteBuffer byteBuffer) throws IOException {
450         mRequest.read(byteBuffer);
451         mMessageLoop.loop(getReadTimeout());
452     }
453 
454     /**
455      * Sets {@link android.net.TrafficStats} tag to use when accounting socket traffic caused by
456      * this request. See {@link android.net.TrafficStats} for more information. If no tag is
457      * set (e.g. this method isn't called), then Android accounts for the socket traffic caused
458      * by this request as if the tag value were set to 0.
459      * <p>
460      * <b>NOTE:</b>Setting a tag disallows sharing of sockets with requests
461      * with other tags, which may adversely effect performance by prohibiting
462      * connection sharing. In other words use of multiplexed sockets (e.g. HTTP/2
463      * and QUIC) will only be allowed if all requests have the same socket tag.
464      *
465      * @param tag the tag value used to when accounting for socket traffic caused by this
466      *            request. Tags between 0xFFFFFF00 and 0xFFFFFFFF are reserved and used
467      *            internally by system services like {@link android.app.DownloadManager} when
468      *            performing traffic on behalf of an application.
469      */
setTrafficStatsTag(int tag)470     public void setTrafficStatsTag(int tag) {
471         if (connected) {
472             throw new IllegalStateException(
473                     "Cannot modify traffic stats tag after connection is made.");
474         }
475         mTrafficStatsTagSet = true;
476         mTrafficStatsTag = tag;
477     }
478 
479     /**
480      * Sets specific UID to use when accounting socket traffic caused by this request. See
481      * {@link android.net.TrafficStats} for more information. Designed for use when performing
482      * an operation on behalf of another application. Caller must hold
483      * {@link android.Manifest.permission#MODIFY_NETWORK_ACCOUNTING} permission. By default
484      * traffic is attributed to UID of caller.
485      * <p>
486      * <b>NOTE:</b>Setting a UID disallows sharing of sockets with requests
487      * with other UIDs, which may adversely effect performance by prohibiting
488      * connection sharing. In other words use of multiplexed sockets (e.g. HTTP/2
489      * and QUIC) will only be allowed if all requests have the same UID set.
490      *
491      * @param uid the UID to attribute socket traffic caused by this request.
492      */
setTrafficStatsUid(int uid)493     public void setTrafficStatsUid(int uid) {
494         if (connected) {
495             throw new IllegalStateException(
496                     "Cannot modify traffic stats UID after connection is made.");
497         }
498         mTrafficStatsUidSet = true;
499         mTrafficStatsUid = uid;
500     }
501 
502     /**
503      * Returns the index of request header in {@link #mRequestHeaders} or
504      * -1 if not found.
505      */
findRequestProperty(String key)506     private int findRequestProperty(String key) {
507         for (int i = 0; i < mRequestHeaders.size(); i++) {
508             Pair<String, String> entry = mRequestHeaders.get(i);
509             if (entry.first.equalsIgnoreCase(key)) {
510                 return i;
511             }
512         }
513         return -1;
514     }
515 
516     private class CronetUrlRequestCallback extends UrlRequest.Callback {
CronetUrlRequestCallback()517         public CronetUrlRequestCallback() {}
518 
519         @Override
onResponseStarted(UrlRequest request, UrlResponseInfo info)520         public void onResponseStarted(UrlRequest request, UrlResponseInfo info) {
521             mResponseInfo = info;
522             mHasResponseHeadersOrCompleted = true;
523             // Quits the message loop since we have the headers now.
524             mMessageLoop.quit();
525         }
526 
527         @Override
onReadCompleted( UrlRequest request, UrlResponseInfo info, ByteBuffer byteBuffer)528         public void onReadCompleted(
529                 UrlRequest request, UrlResponseInfo info, ByteBuffer byteBuffer) {
530             mResponseInfo = info;
531             mMessageLoop.quit();
532         }
533 
534         @Override
onRedirectReceived( UrlRequest request, UrlResponseInfo info, String newLocationUrl)535         public void onRedirectReceived(
536                 UrlRequest request, UrlResponseInfo info, String newLocationUrl) {
537             mOnRedirectCalled = true;
538             try {
539                 URL newUrl = new URL(newLocationUrl);
540                 boolean sameProtocol = newUrl.getProtocol().equals(url.getProtocol());
541                 if (instanceFollowRedirects) {
542                     // Update the url variable even if the redirect will not be
543                     // followed due to different protocols.
544                     url = newUrl;
545                 }
546                 if (instanceFollowRedirects && sameProtocol) {
547                     mRequest.followRedirect();
548                     return;
549                 }
550             } catch (MalformedURLException e) {
551                 // Ignored. Just cancel the request and not follow the redirect.
552             }
553             mResponseInfo = info;
554             mRequest.cancel();
555             setResponseDataCompleted(null);
556         }
557 
558         @Override
onSucceeded(UrlRequest request, UrlResponseInfo info)559         public void onSucceeded(UrlRequest request, UrlResponseInfo info) {
560             mResponseInfo = info;
561             setResponseDataCompleted(null);
562         }
563 
564         @Override
onFailed(UrlRequest request, UrlResponseInfo info, CronetException exception)565         public void onFailed(UrlRequest request, UrlResponseInfo info, CronetException exception) {
566             if (exception == null) {
567                 throw new IllegalStateException("Exception cannot be null in onFailed.");
568             }
569             mResponseInfo = info;
570             setResponseDataCompleted(exception);
571         }
572 
573         @Override
onCanceled(UrlRequest request, UrlResponseInfo info)574         public void onCanceled(UrlRequest request, UrlResponseInfo info) {
575             mResponseInfo = info;
576             setResponseDataCompleted(new IOException("disconnect() called"));
577         }
578 
579         /**
580          * Notifies {@link #mInputStream} that transferring of response data has
581          * completed.
582          * @param exception if not {@code null}, it is the exception to report when
583          *            caller tries to read more data.
584          */
setResponseDataCompleted(IOException exception)585         private void setResponseDataCompleted(IOException exception) {
586             mException = exception;
587             if (mInputStream != null) {
588                 mInputStream.setResponseDataCompleted(exception);
589             }
590             if (mOutputStream != null) {
591                 mOutputStream.setRequestCompleted(exception);
592             }
593             mHasResponseHeadersOrCompleted = true;
594             mMessageLoop.quit();
595         }
596     }
597 
598     /** Blocks until the response headers are received. */
getResponse()599     private void getResponse() throws IOException {
600         // Check to see if enough data has been received.
601         if (mOutputStream != null) {
602             mOutputStream.checkReceivedEnoughContent();
603             if (isChunkedUpload()) {
604                 // Write last chunk.
605                 mOutputStream.close();
606             }
607         }
608         if (!mHasResponseHeadersOrCompleted) {
609             startRequest();
610             // Blocks until onResponseStarted or onFailed is called.
611             mMessageLoop.loop();
612         }
613         checkHasResponseHeaders();
614     }
615 
616     /**
617      * Checks whether response headers are received, and throws an exception if
618      * an exception occurred before headers received. This method should only be
619      * called after onResponseStarted or onFailed.
620      */
checkHasResponseHeaders()621     private void checkHasResponseHeaders() throws IOException {
622         if (!mHasResponseHeadersOrCompleted) throw new IllegalStateException("No response.");
623         if (mException != null) {
624             throw mException;
625         } else if (mResponseInfo == null) {
626             throw new NullPointerException("Response info is null when there is no exception.");
627         }
628     }
629 
630     /** Helper method to return the response header field at position pos. */
getHeaderFieldEntry(int pos)631     private Map.Entry<String, String> getHeaderFieldEntry(int pos) {
632         try {
633             getResponse();
634         } catch (IOException e) {
635             return null;
636         }
637         List<Map.Entry<String, String>> headers = getAllHeadersAsList();
638         if (pos >= headers.size()) {
639             return null;
640         }
641         return headers.get(pos);
642     }
643 
644     /**
645      * Returns whether the client has used {@link #setChunkedStreamingMode} to
646      * set chunked encoding for upload.
647      */
isChunkedUpload()648     private boolean isChunkedUpload() {
649         return chunkLength > 0;
650     }
651 
652     // TODO(xunjieli): Refactor to reuse code in UrlResponseInfo.
getAllHeaders()653     private Map<String, List<String>> getAllHeaders() {
654         if (mResponseHeadersMap != null) {
655             return mResponseHeadersMap;
656         }
657         Map<String, List<String>> map = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
658         for (Map.Entry<String, String> entry : getAllHeadersAsList()) {
659             List<String> values = new ArrayList<String>();
660             if (map.containsKey(entry.getKey())) {
661                 values.addAll(map.get(entry.getKey()));
662             }
663             values.add(entry.getValue());
664             map.put(entry.getKey(), Collections.unmodifiableList(values));
665         }
666         mResponseHeadersMap = Collections.unmodifiableMap(map);
667         return mResponseHeadersMap;
668     }
669 
getAllHeadersAsList()670     private List<Map.Entry<String, String>> getAllHeadersAsList() {
671         if (mResponseHeadersList != null) {
672             return mResponseHeadersList;
673         }
674         mResponseHeadersList = new ArrayList<Map.Entry<String, String>>();
675         for (Map.Entry<String, String> entry : mResponseInfo.getAllHeadersAsList()) {
676             // Strips Content-Encoding response header. See crbug.com/592700.
677             if (!entry.getKey().equalsIgnoreCase("Content-Encoding")) {
678                 mResponseHeadersList.add(
679                         new AbstractMap.SimpleImmutableEntry<String, String>(entry));
680             }
681         }
682         mResponseHeadersList = Collections.unmodifiableList(mResponseHeadersList);
683         return mResponseHeadersList;
684     }
685 }
686