Import of Volley from GitHub to AOSP.
am: ac8d9a1d94

Change-Id: Ia722422898be6f9631f1ba7160d64f6548c75f21
diff --git a/bintray.gradle b/bintray.gradle
index 8914c1d..df0e49b 100644
--- a/bintray.gradle
+++ b/bintray.gradle
@@ -55,6 +55,9 @@
             groupId 'com.android.volley'
             artifactId 'volley'
             version project.ext.version
+            pom {
+                packaging 'aar'
+            }
 
             // Release AAR, Sources, and JavaDoc
             artifact "$buildDir/outputs/aar/volley-release.aar"
@@ -84,4 +87,4 @@
     resolve {
         repoKey = 'jcenter'
     }
-}
\ No newline at end of file
+}
diff --git a/src/main/java/com/android/volley/Cache.java b/src/main/java/com/android/volley/Cache.java
index 8482c22..fd7eea1 100644
--- a/src/main/java/com/android/volley/Cache.java
+++ b/src/main/java/com/android/volley/Cache.java
@@ -17,6 +17,7 @@
 package com.android.volley;
 
 import java.util.Collections;
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -83,9 +84,22 @@
         /** Soft TTL for this record. */
         public long softTtl;
 
-        /** Immutable response headers as received from server; must be non-null. */
+        /**
+         * Response headers as received from server; must be non-null. Should not be mutated
+         * directly.
+         *
+         * <p>Note that if the server returns two headers with the same (case-insensitive) name,
+         * this map will only contain the one of them. {@link #allResponseHeaders} may contain all
+         * headers if the {@link Cache} implementation supports it.
+         */
         public Map<String, String> responseHeaders = Collections.emptyMap();
 
+        /**
+         * All response headers. May be null depending on the {@link Cache} implementation. Should
+         * not be mutated directly.
+         */
+        public List<Header> allResponseHeaders;
+
         /** True if the entry is expired. */
         public boolean isExpired() {
             return this.ttl < System.currentTimeMillis();
diff --git a/src/main/java/com/android/volley/CacheDispatcher.java b/src/main/java/com/android/volley/CacheDispatcher.java
index 1e7dfc4..51dfd9c 100644
--- a/src/main/java/com/android/volley/CacheDispatcher.java
+++ b/src/main/java/com/android/volley/CacheDispatcher.java
@@ -18,6 +18,10 @@
 
 import android.os.Process;
 
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 import java.util.concurrent.BlockingQueue;
 
 /**
@@ -48,6 +52,9 @@
     /** Used for telling us to die. */
     private volatile boolean mQuit = false;
 
+    /** Manage list of waiting requests and de-duplicate requests with same cache key. */
+    private final WaitingRequestManager mWaitingRequestManager;
+
     /**
      * Creates a new cache triage dispatcher thread.  You must call {@link #start()}
      * in order to begin processing.
@@ -64,6 +71,7 @@
         mNetworkQueue = networkQueue;
         mCache = cache;
         mDelivery = delivery;
+        mWaitingRequestManager = new WaitingRequestManager(this);
     }
 
     /**
@@ -101,7 +109,9 @@
                 if (entry == null) {
                     request.addMarker("cache-miss");
                     // Cache miss; send off to the network dispatcher.
-                    mNetworkQueue.put(request);
+                    if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
+                        mNetworkQueue.put(request);
+                    }
                     continue;
                 }
 
@@ -109,7 +119,9 @@
                 if (entry.isExpired()) {
                     request.addMarker("cache-hit-expired");
                     request.setCacheEntry(entry);
-                    mNetworkQueue.put(request);
+                    if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
+                        mNetworkQueue.put(request);
+                    }
                     continue;
                 }
 
@@ -128,22 +140,28 @@
                     // refreshing.
                     request.addMarker("cache-hit-refresh-needed");
                     request.setCacheEntry(entry);
-
                     // Mark the response as intermediate.
                     response.intermediate = true;
 
-                    // Post the intermediate response back to the user and have
-                    // the delivery then forward the request along to the network.
-                    mDelivery.postResponse(request, response, new Runnable() {
-                        @Override
-                        public void run() {
-                            try {
-                                mNetworkQueue.put(request);
-                            } catch (InterruptedException e) {
-                                // Not much we can do about this.
+                    if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
+                        // Post the intermediate response back to the user and have
+                        // the delivery then forward the request along to the network.
+                        mDelivery.postResponse(request, response, new Runnable() {
+                            @Override
+                            public void run() {
+                                try {
+                                    mNetworkQueue.put(request);
+                                } catch (InterruptedException e) {
+                                    // Restore the interrupted status
+                                    Thread.currentThread().interrupt();
+                                }
                             }
-                        }
-                    });
+                        });
+                    } else {
+                        // request has been added to list of waiting requests
+                        // to receive the network response from the first request once it returns.
+                        mDelivery.postResponse(request, response);
+                    }
                 }
 
             } catch (InterruptedException e) {
@@ -154,4 +172,109 @@
             }
         }
     }
+
+    private static class WaitingRequestManager implements Request.NetworkRequestCompleteListener {
+
+        /**
+         * Staging area for requests that already have a duplicate request in flight.
+         *
+         * <ul>
+         *     <li>containsKey(cacheKey) indicates that there is a request in flight for the given cache
+         *          key.</li>
+         *     <li>get(cacheKey) returns waiting requests for the given cache key. The in flight request
+         *          is <em>not</em> contained in that list. Is null if no requests are staged.</li>
+         * </ul>
+         */
+        private final Map<String, List<Request<?>>> mWaitingRequests = new HashMap<>();
+
+        private final CacheDispatcher mCacheDispatcher;
+
+        WaitingRequestManager(CacheDispatcher cacheDispatcher) {
+            mCacheDispatcher = cacheDispatcher;
+        }
+
+        /** Request received a valid response that can be used by other waiting requests. */
+        @Override
+        public void onResponseReceived(Request<?> request, Response<?> response) {
+            if (response.cacheEntry == null || response.cacheEntry.isExpired()) {
+                onNoUsableResponseReceived(request);
+                return;
+            }
+            String cacheKey = request.getCacheKey();
+            List<Request<?>> waitingRequests;
+            synchronized (this) {
+                waitingRequests = mWaitingRequests.remove(cacheKey);
+            }
+            if (waitingRequests != null) {
+                if (VolleyLog.DEBUG) {
+                    VolleyLog.v("Releasing %d waiting requests for cacheKey=%s.",
+                            waitingRequests.size(), cacheKey);
+                }
+                // Process all queued up requests.
+                for (Request<?> waiting : waitingRequests) {
+                    mCacheDispatcher.mDelivery.postResponse(waiting, response);
+                }
+            }
+        }
+
+        /** No valid response received from network, release waiting requests. */
+        @Override
+        public synchronized void onNoUsableResponseReceived(Request<?> request) {
+            String cacheKey = request.getCacheKey();
+            List<Request<?>> waitingRequests = mWaitingRequests.remove(cacheKey);
+            if (waitingRequests != null && !waitingRequests.isEmpty()) {
+                if (VolleyLog.DEBUG) {
+                    VolleyLog.v("%d waiting requests for cacheKey=%s; resend to network",
+                            waitingRequests.size(), cacheKey);
+                }
+                Request<?> nextInLine = waitingRequests.remove(0);
+                mWaitingRequests.put(cacheKey, waitingRequests);
+                try {
+                    mCacheDispatcher.mNetworkQueue.put(nextInLine);
+                } catch (InterruptedException iex) {
+                    VolleyLog.e("Couldn't add request to queue. %s", iex.toString());
+                    // Restore the interrupted status of the calling thread (i.e. NetworkDispatcher)
+                    Thread.currentThread().interrupt();
+                    // Quit the current CacheDispatcher thread.
+                    mCacheDispatcher.quit();
+                }
+            }
+        }
+
+        /**
+         * For cacheable requests, if a request for the same cache key is already in flight,
+         * add it to a queue to wait for that in-flight request to finish.
+         * @return whether the request was queued. If false, we should continue issuing the request
+         * over the network. If true, we should put the request on hold to be processed when
+         * the in-flight request finishes.
+         */
+        private synchronized boolean maybeAddToWaitingRequests(Request<?> request) {
+            String cacheKey = request.getCacheKey();
+            // Insert request into stage if there's already a request with the same cache key
+            // in flight.
+            if (mWaitingRequests.containsKey(cacheKey)) {
+                // There is already a request in flight. Queue up.
+                List<Request<?>> stagedRequests = mWaitingRequests.get(cacheKey);
+                if (stagedRequests == null) {
+                    stagedRequests = new ArrayList<Request<?>>();
+                }
+                request.addMarker("waiting-for-response");
+                stagedRequests.add(request);
+                mWaitingRequests.put(cacheKey, stagedRequests);
+                if (VolleyLog.DEBUG) {
+                    VolleyLog.d("Request for cacheKey=%s is in flight, putting on hold.", cacheKey);
+                }
+                return true;
+            } else {
+                // Insert 'null' queue for this cacheKey, indicating there is now a request in
+                // flight.
+                mWaitingRequests.put(cacheKey, null);
+                request.setNetworkRequestCompleteListener(this);
+                if (VolleyLog.DEBUG) {
+                    VolleyLog.d("new request, sending to network %s", cacheKey);
+                }
+                return false;
+            }
+        }
+    }
 }
diff --git a/src/main/java/com/android/volley/Header.java b/src/main/java/com/android/volley/Header.java
new file mode 100644
index 0000000..ac8aa11
--- /dev/null
+++ b/src/main/java/com/android/volley/Header.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.volley;
+
+import android.text.TextUtils;
+
+/** An HTTP header. */
+public final class Header {
+    private final String mName;
+    private final String mValue;
+
+    public Header(String name, String value) {
+        mName = name;
+        mValue = value;
+    }
+
+    public final String getName() {
+        return mName;
+    }
+
+    public final String getValue() {
+        return mValue;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        Header header = (Header) o;
+
+        return TextUtils.equals(mName, header.mName)
+                && TextUtils.equals(mValue, header.mValue);
+    }
+
+    @Override
+    public int hashCode() {
+        int result = mName.hashCode();
+        result = 31 * result + mValue.hashCode();
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        return "Header[name=" + mName + ",value=" + mValue + "]";
+    }
+}
diff --git a/src/main/java/com/android/volley/NetworkDispatcher.java b/src/main/java/com/android/volley/NetworkDispatcher.java
index beb7861..0384429 100644
--- a/src/main/java/com/android/volley/NetworkDispatcher.java
+++ b/src/main/java/com/android/volley/NetworkDispatcher.java
@@ -33,6 +33,7 @@
  * errors are posted back to the caller via a {@link ResponseDelivery}.
  */
 public class NetworkDispatcher extends Thread {
+
     /** The queue of requests to service. */
     private final BlockingQueue<Request<?>> mQueue;
     /** The network interface for processing requests. */
@@ -54,8 +55,7 @@
      * @param delivery Delivery interface to use for posting responses
      */
     public NetworkDispatcher(BlockingQueue<Request<?>> queue,
-            Network network, Cache cache,
-            ResponseDelivery delivery) {
+            Network network, Cache cache, ResponseDelivery delivery) {
         mQueue = queue;
         mNetwork = network;
         mCache = cache;
@@ -103,6 +103,7 @@
                 // network request.
                 if (request.isCanceled()) {
                     request.finish("network-discard-cancelled");
+                    request.notifyListenerResponseNotUsable();
                     continue;
                 }
 
@@ -116,6 +117,7 @@
                 // we're done -- don't deliver a second identical response.
                 if (networkResponse.notModified && request.hasHadResponseDelivered()) {
                     request.finish("not-modified");
+                    request.notifyListenerResponseNotUsable();
                     continue;
                 }
 
@@ -133,14 +135,17 @@
                 // Post the response back.
                 request.markDelivered();
                 mDelivery.postResponse(request, response);
+                request.notifyListenerResponseReceived(response);
             } catch (VolleyError volleyError) {
                 volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
                 parseAndDeliverNetworkError(request, volleyError);
+                request.notifyListenerResponseNotUsable();
             } catch (Exception e) {
                 VolleyLog.e(e, "Unhandled exception %s", e.toString());
                 VolleyError volleyError = new VolleyError(e);
                 volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
                 mDelivery.postError(request, volleyError);
+                request.notifyListenerResponseNotUsable();
             }
         }
     }
diff --git a/src/main/java/com/android/volley/NetworkResponse.java b/src/main/java/com/android/volley/NetworkResponse.java
index a787fa7..f0fded3 100644
--- a/src/main/java/com/android/volley/NetworkResponse.java
+++ b/src/main/java/com/android/volley/NetworkResponse.java
@@ -16,15 +16,18 @@
 
 package com.android.volley;
 
-import org.apache.http.HttpStatus;
-
+import java.net.HttpURLConnection;
+import java.util.ArrayList;
 import java.util.Collections;
+import java.util.List;
 import java.util.Map;
+import java.util.TreeMap;
 
 /**
  * Data and headers returned from {@link Network#performRequest(Request)}.
  */
 public class NetworkResponse {
+
     /**
      * Creates a new network response.
      * @param statusCode the HTTP status code
@@ -32,27 +35,78 @@
      * @param headers Headers returned with this response, or null for none
      * @param notModified True if the server returned a 304 and the data was already in cache
      * @param networkTimeMs Round-trip network time to receive network response
+     * @deprecated see {@link #NetworkResponse(int, byte[], boolean, long, List)}. This constructor
+     *             cannot handle server responses containing multiple headers with the same name.
+     *             This constructor may be removed in a future release of Volley.
      */
+    @Deprecated
     public NetworkResponse(int statusCode, byte[] data, Map<String, String> headers,
             boolean notModified, long networkTimeMs) {
-        this.statusCode = statusCode;
-        this.data = data;
-        this.headers = headers;
-        this.notModified = notModified;
-        this.networkTimeMs = networkTimeMs;
+        this(statusCode, data, headers, toAllHeaderList(headers), notModified, networkTimeMs);
     }
 
+    /**
+     * Creates a new network response.
+     * @param statusCode the HTTP status code
+     * @param data Response body
+     * @param notModified True if the server returned a 304 and the data was already in cache
+     * @param networkTimeMs Round-trip network time to receive network response
+     * @param allHeaders All headers returned with this response, or null for none
+     */
+    public NetworkResponse(int statusCode, byte[] data, boolean notModified, long networkTimeMs,
+            List<Header> allHeaders) {
+        this(statusCode, data, toHeaderMap(allHeaders), allHeaders, notModified, networkTimeMs);
+    }
+
+    /**
+     * Creates a new network response.
+     * @param statusCode the HTTP status code
+     * @param data Response body
+     * @param headers Headers returned with this response, or null for none
+     * @param notModified True if the server returned a 304 and the data was already in cache
+     * @deprecated see {@link #NetworkResponse(int, byte[], boolean, long, List)}. This constructor
+     *             cannot handle server responses containing multiple headers with the same name.
+     *             This constructor may be removed in a future release of Volley.
+     */
+    @Deprecated
     public NetworkResponse(int statusCode, byte[] data, Map<String, String> headers,
             boolean notModified) {
         this(statusCode, data, headers, notModified, 0);
     }
 
+    /**
+     * Creates a new network response for an OK response with no headers.
+     * @param data Response body
+     */
     public NetworkResponse(byte[] data) {
-        this(HttpStatus.SC_OK, data, Collections.<String, String>emptyMap(), false, 0);
+        this(HttpURLConnection.HTTP_OK, data, false, 0, Collections.<Header>emptyList());
     }
 
+    /**
+     * Creates a new network response for an OK response.
+     * @param data Response body
+     * @param headers Headers returned with this response, or null for none
+     * @deprecated see {@link #NetworkResponse(int, byte[], boolean, long, List)}. This constructor
+     *             cannot handle server responses containing multiple headers with the same name.
+     *             This constructor may be removed in a future release of Volley.
+     */
+    @Deprecated
     public NetworkResponse(byte[] data, Map<String, String> headers) {
-        this(HttpStatus.SC_OK, data, headers, false, 0);
+        this(HttpURLConnection.HTTP_OK, data, headers, false, 0);
+    }
+
+    private NetworkResponse(int statusCode, byte[] data, Map<String, String> headers,
+            List<Header> allHeaders, boolean notModified, long networkTimeMs) {
+        this.statusCode = statusCode;
+        this.data = data;
+        this.headers = headers;
+        if (allHeaders == null) {
+            this.allHeaders = null;
+        } else {
+            this.allHeaders = Collections.unmodifiableList(allHeaders);
+        }
+        this.notModified = notModified;
+        this.networkTimeMs = networkTimeMs;
     }
 
     /** The HTTP status code. */
@@ -61,13 +115,53 @@
     /** Raw data from this response. */
     public final byte[] data;
 
-    /** Response headers. */
+    /**
+     * Response headers.
+     *
+     * <p>This map is case-insensitive. It should not be mutated directly.
+     *
+     * <p>Note that if the server returns two headers with the same (case-insensitive) name, this
+     * map will only contain the last one. Use {@link #allHeaders} to inspect all headers returned
+     * by the server.
+     */
     public final Map<String, String> headers;
 
+    /** All response headers. Must not be mutated directly. */
+    public final List<Header> allHeaders;
+
     /** True if the server returned a 304 (Not Modified). */
     public final boolean notModified;
 
     /** Network roundtrip time in milliseconds. */
     public final long networkTimeMs;
+
+    private static Map<String, String> toHeaderMap(List<Header> allHeaders) {
+        if (allHeaders == null) {
+            return null;
+        }
+        if (allHeaders.isEmpty()) {
+            return Collections.emptyMap();
+        }
+        Map<String, String> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+        // Later elements in the list take precedence.
+        for (Header header : allHeaders) {
+            headers.put(header.getName(), header.getValue());
+        }
+        return headers;
+    }
+
+    private static List<Header> toAllHeaderList(Map<String, String> headers) {
+        if (headers == null) {
+            return null;
+        }
+        if (headers.isEmpty()) {
+            return Collections.emptyList();
+        }
+        List<Header> allHeaders = new ArrayList<>(headers.size());
+        for (Map.Entry<String, String> header : headers.entrySet()) {
+            allHeaders.add(new Header(header.getKey(), header.getValue()));
+        }
+        return allHeaders;
+    }
 }
 
diff --git a/src/main/java/com/android/volley/Request.java b/src/main/java/com/android/volley/Request.java
index 8200f6e..a98277e 100644
--- a/src/main/java/com/android/volley/Request.java
+++ b/src/main/java/com/android/volley/Request.java
@@ -56,6 +56,18 @@
         int PATCH = 7;
     }
 
+    /**
+     * Callback to notify when the network request returns.
+     */
+    /* package */ interface NetworkRequestCompleteListener {
+
+        /** Callback when a network response has been received. */
+        void onResponseReceived(Request<?> request, Response<?> response);
+
+        /** Callback when request returns from network without valid response. */
+        void onNoUsableResponseReceived(Request<?> request);
+    }
+
     /** An event log tracing the lifetime of this request; for debugging. */
     private final MarkerLog mEventLog = MarkerLog.ENABLED ? new MarkerLog() : null;
 
@@ -105,6 +117,12 @@
     /** An opaque token tagging this request; used for bulk cancellation. */
     private Object mTag;
 
+    /** Listener that will be notified when a response has been delivered. */
+    private NetworkRequestCompleteListener mRequestCompleteListener;
+
+    /** Object to guard access to mRequestCompleteListener. */
+    private final Object mLock = new Object();
+
     /**
      * Creates a new request with the given URL and error listener.  Note that
      * the normal response listener is not provided here as delivery of responses
@@ -585,6 +603,42 @@
     }
 
     /**
+     * {@link NetworkRequestCompleteListener} that will receive callbacks when the request
+     * returns from the network.
+     */
+    /* package */ void setNetworkRequestCompleteListener(
+            NetworkRequestCompleteListener requestCompleteListener) {
+        synchronized (mLock) {
+            mRequestCompleteListener = requestCompleteListener;
+        }
+    }
+
+    /**
+     * Notify NetworkRequestCompleteListener that a valid response has been received
+     * which can be used for other, waiting requests.
+     * @param response received from the network
+     */
+    /* package */ void notifyListenerResponseReceived(Response<?> response) {
+        synchronized (mLock) {
+            if (mRequestCompleteListener != null) {
+               mRequestCompleteListener.onResponseReceived(this, response);
+            }
+        }
+    }
+
+    /**
+     * Notify NetworkRequestCompleteListener that the network request did not result in
+     * a response which can be used for other, waiting requests.
+     */
+    /* package */ void notifyListenerResponseNotUsable() {
+        synchronized (mLock) {
+            if (mRequestCompleteListener != null) {
+                mRequestCompleteListener.onNoUsableResponseReceived(this);
+            }
+        }
+    }
+
+    /**
      * Our comparator sorts from high to low priority, and secondarily by
      * sequence number to provide FIFO ordering.
      */
diff --git a/src/main/java/com/android/volley/RequestQueue.java b/src/main/java/com/android/volley/RequestQueue.java
index 0f2e756..45679a5 100644
--- a/src/main/java/com/android/volley/RequestQueue.java
+++ b/src/main/java/com/android/volley/RequestQueue.java
@@ -20,12 +20,8 @@
 import android.os.Looper;
 
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.HashSet;
-import java.util.LinkedList;
 import java.util.List;
-import java.util.Map;
-import java.util.Queue;
 import java.util.Set;
 import java.util.concurrent.PriorityBlockingQueue;
 import java.util.concurrent.atomic.AtomicInteger;
@@ -49,19 +45,6 @@
     private final AtomicInteger mSequenceGenerator = new AtomicInteger();
 
     /**
-     * Staging area for requests that already have a duplicate request in flight.
-     *
-     * <ul>
-     *     <li>containsKey(cacheKey) indicates that there is a request in flight for the given cache
-     *          key.</li>
-     *     <li>get(cacheKey) returns waiting requests for the given cache key. The in flight request
-     *          is <em>not</em> contained in that list. Is null if no requests are staged.</li>
-     * </ul>
-     */
-    private final Map<String, Queue<Request<?>>> mWaitingRequests =
-            new HashMap<>();
-
-    /**
      * The set of all requests currently being processed by this RequestQueue. A Request
      * will be in this set if it is waiting in any queue or currently being processed by
      * any dispatcher.
@@ -240,37 +223,13 @@
             mNetworkQueue.add(request);
             return request;
         }
-
-        // Insert request into stage if there's already a request with the same cache key in flight.
-        synchronized (mWaitingRequests) {
-            String cacheKey = request.getCacheKey();
-            if (mWaitingRequests.containsKey(cacheKey)) {
-                // There is already a request in flight. Queue up.
-                Queue<Request<?>> stagedRequests = mWaitingRequests.get(cacheKey);
-                if (stagedRequests == null) {
-                    stagedRequests = new LinkedList<>();
-                }
-                stagedRequests.add(request);
-                mWaitingRequests.put(cacheKey, stagedRequests);
-                if (VolleyLog.DEBUG) {
-                    VolleyLog.v("Request for cacheKey=%s is in flight, putting on hold.", cacheKey);
-                }
-            } else {
-                // Insert 'null' queue for this cacheKey, indicating there is now a request in
-                // flight.
-                mWaitingRequests.put(cacheKey, null);
-                mCacheQueue.add(request);
-            }
-            return request;
-        }
-    }
+        mCacheQueue.add(request);
+        return request;
+     }
 
     /**
      * Called from {@link Request#finish(String)}, indicating that processing of the given request
      * has finished.
-     *
-     * <p>Releases waiting requests for <code>request.getCacheKey()</code> if
-     *      <code>request.shouldCache()</code>.</p>
      */
     <T> void finish(Request<T> request) {
         // Remove from the set of requests currently being processed.
@@ -283,21 +242,6 @@
           }
         }
 
-        if (request.shouldCache()) {
-            synchronized (mWaitingRequests) {
-                String cacheKey = request.getCacheKey();
-                Queue<Request<?>> waitingRequests = mWaitingRequests.remove(cacheKey);
-                if (waitingRequests != null) {
-                    if (VolleyLog.DEBUG) {
-                        VolleyLog.v("Releasing %d waiting requests for cacheKey=%s.",
-                                waitingRequests.size(), cacheKey);
-                    }
-                    // Process all queued up requests. They won't be considered as in flight, but
-                    // that's not a problem as the cache has been primed by 'request'.
-                    mCacheQueue.addAll(waitingRequests);
-                }
-            }
-        }
     }
 
     public  <T> void addRequestFinishedListener(RequestFinishedListener<T> listener) {
diff --git a/src/main/java/com/android/volley/toolbox/AdaptedHttpStack.java b/src/main/java/com/android/volley/toolbox/AdaptedHttpStack.java
new file mode 100644
index 0000000..e5dc62b
--- /dev/null
+++ b/src/main/java/com/android/volley/toolbox/AdaptedHttpStack.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.volley.toolbox;
+
+import com.android.volley.AuthFailureError;
+import com.android.volley.Header;
+import com.android.volley.Request;
+
+import org.apache.http.conn.ConnectTimeoutException;
+
+import java.io.IOException;
+import java.net.SocketTimeoutException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * {@link BaseHttpStack} implementation wrapping a {@link HttpStack}.
+ *
+ * <p>{@link BasicNetwork} uses this if it is provided a {@link HttpStack} at construction time,
+ * allowing it to have one implementation based atop {@link BaseHttpStack}.
+ */
+@SuppressWarnings("deprecation")
+class AdaptedHttpStack extends BaseHttpStack {
+
+    private final HttpStack mHttpStack;
+
+    AdaptedHttpStack(HttpStack httpStack) {
+        mHttpStack = httpStack;
+    }
+
+    @Override
+    public HttpResponse executeRequest(
+            Request<?> request, Map<String, String> additionalHeaders)
+            throws IOException, AuthFailureError {
+        org.apache.http.HttpResponse apacheResp;
+        try {
+             apacheResp = mHttpStack.performRequest(request, additionalHeaders);
+        } catch (ConnectTimeoutException e) {
+            // BasicNetwork won't know that this exception should be retried like a timeout, since
+            // it's an Apache-specific error, so wrap it in a standard timeout exception.
+            throw new SocketTimeoutException(e.getMessage());
+        }
+
+        int statusCode = apacheResp.getStatusLine().getStatusCode();
+
+        org.apache.http.Header[] headers = apacheResp.getAllHeaders();
+        List<Header> headerList = new ArrayList<>(headers.length);
+        for (org.apache.http.Header header : headers) {
+            headerList.add(new Header(header.getName(), header.getValue()));
+        }
+
+        if (apacheResp.getEntity() == null) {
+            return new HttpResponse(statusCode, headerList);
+        }
+
+        long contentLength = apacheResp.getEntity().getContentLength();
+        if ((int) contentLength != contentLength) {
+            throw new IOException("Response too large: " + contentLength);
+        }
+
+        return new HttpResponse(
+                statusCode,
+                headerList,
+                (int) apacheResp.getEntity().getContentLength(),
+                apacheResp.getEntity().getContent());
+    }
+}
diff --git a/src/main/java/com/android/volley/toolbox/BaseHttpStack.java b/src/main/java/com/android/volley/toolbox/BaseHttpStack.java
new file mode 100644
index 0000000..257f75c
--- /dev/null
+++ b/src/main/java/com/android/volley/toolbox/BaseHttpStack.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.volley.toolbox;
+
+import com.android.volley.AuthFailureError;
+import com.android.volley.Header;
+import com.android.volley.Request;
+
+import org.apache.http.ProtocolVersion;
+import org.apache.http.StatusLine;
+import org.apache.http.entity.BasicHttpEntity;
+import org.apache.http.message.BasicHeader;
+import org.apache.http.message.BasicHttpResponse;
+import org.apache.http.message.BasicStatusLine;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.SocketTimeoutException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/** An HTTP stack abstraction. */
+@SuppressWarnings("deprecation") // for HttpStack
+public abstract class BaseHttpStack implements HttpStack {
+
+    /**
+     * Performs an HTTP request with the given parameters.
+     *
+     * <p>A GET request is sent if request.getPostBody() == null. A POST request is sent otherwise,
+     * and the Content-Type header is set to request.getPostBodyContentType().
+     *
+     * @param request the request to perform
+     * @param additionalHeaders additional headers to be sent together with
+     *         {@link Request#getHeaders()}
+     * @return the {@link HttpResponse}
+     * @throws SocketTimeoutException if the request times out
+     * @throws IOException if another I/O error occurs during the request
+     * @throws AuthFailureError if an authentication failure occurs during the request
+     */
+    public abstract HttpResponse executeRequest(
+            Request<?> request, Map<String, String> additionalHeaders)
+            throws IOException, AuthFailureError;
+
+    /**
+     * @deprecated use {@link #executeRequest} instead to avoid a dependency on the deprecated
+     * Apache HTTP library. Nothing in Volley's own source calls this method. However, since
+     * {@link BasicNetwork#mHttpStack} is exposed to subclasses, we provide this implementation in
+     * case legacy client apps are dependent on that field. This method may be removed in a future
+     * release of Volley.
+     */
+    @Deprecated
+    @Override
+    public final org.apache.http.HttpResponse performRequest(
+            Request<?> request, Map<String, String> additionalHeaders)
+            throws IOException, AuthFailureError {
+        HttpResponse response = executeRequest(request, additionalHeaders);
+
+        ProtocolVersion protocolVersion = new ProtocolVersion("HTTP", 1, 1);
+        StatusLine statusLine = new BasicStatusLine(
+                protocolVersion, response.getStatusCode(), "" /* reasonPhrase */);
+        BasicHttpResponse apacheResponse = new BasicHttpResponse(statusLine);
+
+        List<org.apache.http.Header> headers = new ArrayList<>();
+        for (Header header : response.getHeaders()) {
+            headers.add(new BasicHeader(header.getName(), header.getValue()));
+        }
+        apacheResponse.setHeaders(headers.toArray(new org.apache.http.Header[headers.size()]));
+
+        InputStream responseStream = response.getContent();
+        if (responseStream != null) {
+            BasicHttpEntity entity = new BasicHttpEntity();
+            entity.setContent(responseStream);
+            entity.setContentLength(response.getContentLength());
+            apacheResponse.setEntity(entity);
+        }
+
+        return apacheResponse;
+    }
+}
diff --git a/src/main/java/com/android/volley/toolbox/BasicNetwork.java b/src/main/java/com/android/volley/toolbox/BasicNetwork.java
index 96fb66e..5330733 100644
--- a/src/main/java/com/android/volley/toolbox/BasicNetwork.java
+++ b/src/main/java/com/android/volley/toolbox/BasicNetwork.java
@@ -22,6 +22,7 @@
 import com.android.volley.Cache;
 import com.android.volley.Cache.Entry;
 import com.android.volley.ClientError;
+import com.android.volley.Header;
 import com.android.volley.Network;
 import com.android.volley.NetworkError;
 import com.android.volley.NetworkResponse;
@@ -33,23 +34,19 @@
 import com.android.volley.VolleyError;
 import com.android.volley.VolleyLog;
 
-import org.apache.http.Header;
-import org.apache.http.HttpEntity;
-import org.apache.http.HttpResponse;
-import org.apache.http.HttpStatus;
-import org.apache.http.StatusLine;
-import org.apache.http.conn.ConnectTimeoutException;
-import org.apache.http.impl.cookie.DateUtils;
-
 import java.io.IOException;
 import java.io.InputStream;
+import java.net.HttpURLConnection;
 import java.net.MalformedURLException;
 import java.net.SocketTimeoutException;
+import java.util.ArrayList;
 import java.util.Collections;
-import java.util.Date;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.TreeMap;
+import java.util.TreeSet;
 
 /**
  * A network performing Volley requests over an {@link HttpStack}.
@@ -61,13 +58,23 @@
 
     private static final int DEFAULT_POOL_SIZE = 4096;
 
+    /**
+     * @deprecated Should never have been exposed in the API. This field may be removed in a future
+     *             release of Volley.
+     */
+    @Deprecated
     protected final HttpStack mHttpStack;
 
+    private final BaseHttpStack mBaseHttpStack;
+
     protected final ByteArrayPool mPool;
 
     /**
      * @param httpStack HTTP stack to be used
+     * @deprecated use {@link #BasicNetwork(BaseHttpStack)} instead to avoid depending on Apache
+     *             HTTP. This method may be removed in a future release of Volley.
      */
+    @Deprecated
     public BasicNetwork(HttpStack httpStack) {
         // If a pool isn't passed in, then build a small default pool that will give us a lot of
         // benefit and not use too much memory.
@@ -77,9 +84,36 @@
     /**
      * @param httpStack HTTP stack to be used
      * @param pool a buffer pool that improves GC performance in copy operations
+     * @deprecated use {@link #BasicNetwork(BaseHttpStack, ByteArrayPool)} instead to avoid
+     *             depending on Apache HTTP. This method may be removed in a future release of
+     *             Volley.
      */
+    @Deprecated
     public BasicNetwork(HttpStack httpStack, ByteArrayPool pool) {
         mHttpStack = httpStack;
+        mBaseHttpStack = new AdaptedHttpStack(httpStack);
+        mPool = pool;
+    }
+
+    /**
+     * @param httpStack HTTP stack to be used
+     */
+    public BasicNetwork(BaseHttpStack httpStack) {
+        // If a pool isn't passed in, then build a small default pool that will give us a lot of
+        // benefit and not use too much memory.
+        this(httpStack, new ByteArrayPool(DEFAULT_POOL_SIZE));
+    }
+
+    /**
+     * @param httpStack HTTP stack to be used
+     * @param pool a buffer pool that improves GC performance in copy operations
+     */
+    public BasicNetwork(BaseHttpStack httpStack, ByteArrayPool pool) {
+        mBaseHttpStack = httpStack;
+        // Populate mHttpStack for backwards compatibility, since it is a protected field. However,
+        // we won't use it directly here, so clients which don't access it directly won't need to
+        // depend on Apache HTTP.
+        mHttpStack = httpStack;
         mPool = pool;
     }
 
@@ -89,39 +123,33 @@
         while (true) {
             HttpResponse httpResponse = null;
             byte[] responseContents = null;
-            Map<String, String> responseHeaders = Collections.emptyMap();
+            List<Header> responseHeaders = Collections.emptyList();
             try {
                 // Gather headers.
-                Map<String, String> headers = new HashMap<String, String>();
-                addCacheHeaders(headers, request.getCacheEntry());
-                httpResponse = mHttpStack.performRequest(request, headers);
-                StatusLine statusLine = httpResponse.getStatusLine();
-                int statusCode = statusLine.getStatusCode();
+                Map<String, String> additionalRequestHeaders =
+                        getCacheHeaders(request.getCacheEntry());
+                httpResponse = mBaseHttpStack.executeRequest(request, additionalRequestHeaders);
+                int statusCode = httpResponse.getStatusCode();
 
-                responseHeaders = convertHeaders(httpResponse.getAllHeaders());
+                responseHeaders = httpResponse.getHeaders();
                 // Handle cache validation.
-                if (statusCode == HttpStatus.SC_NOT_MODIFIED) {
-
+                if (statusCode == HttpURLConnection.HTTP_NOT_MODIFIED) {
                     Entry entry = request.getCacheEntry();
                     if (entry == null) {
-                        return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED, null,
-                                responseHeaders, true,
-                                SystemClock.elapsedRealtime() - requestStart);
+                        return new NetworkResponse(HttpURLConnection.HTTP_NOT_MODIFIED, null, true,
+                                SystemClock.elapsedRealtime() - requestStart, responseHeaders);
                     }
-
-                    // A HTTP 304 response does not have all header fields. We
-                    // have to use the header fields from the cache entry plus
-                    // the new ones from the response.
-                    // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
-                    entry.responseHeaders.putAll(responseHeaders);
-                    return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED, entry.data,
-                            entry.responseHeaders, true,
-                            SystemClock.elapsedRealtime() - requestStart);
+                    // Combine cached and response headers so the response will be complete.
+                    List<Header> combinedHeaders = combineHeaders(responseHeaders, entry);
+                    return new NetworkResponse(HttpURLConnection.HTTP_NOT_MODIFIED, entry.data,
+                            true, SystemClock.elapsedRealtime() - requestStart, combinedHeaders);
                 }
 
                 // Some responses such as 204s do not have content.  We must check.
-                if (httpResponse.getEntity() != null) {
-                  responseContents = entityToBytes(httpResponse.getEntity());
+                InputStream inputStream = httpResponse.getContent();
+                if (inputStream != null) {
+                  responseContents =
+                          inputStreamToBytes(inputStream, httpResponse.getContentLength());
                 } else {
                   // Add 0 byte response as a way of honestly representing a
                   // no-content request.
@@ -130,33 +158,31 @@
 
                 // if the request is slow, log it.
                 long requestLifetime = SystemClock.elapsedRealtime() - requestStart;
-                logSlowRequests(requestLifetime, request, responseContents, statusLine);
+                logSlowRequests(requestLifetime, request, responseContents, statusCode);
 
                 if (statusCode < 200 || statusCode > 299) {
                     throw new IOException();
                 }
-                return new NetworkResponse(statusCode, responseContents, responseHeaders, false,
-                        SystemClock.elapsedRealtime() - requestStart);
+                return new NetworkResponse(statusCode, responseContents, false,
+                        SystemClock.elapsedRealtime() - requestStart, responseHeaders);
             } catch (SocketTimeoutException e) {
                 attemptRetryOnException("socket", request, new TimeoutError());
-            } catch (ConnectTimeoutException e) {
-                attemptRetryOnException("connection", request, new TimeoutError());
             } catch (MalformedURLException e) {
                 throw new RuntimeException("Bad URL " + request.getUrl(), e);
             } catch (IOException e) {
                 int statusCode;
                 if (httpResponse != null) {
-                    statusCode = httpResponse.getStatusLine().getStatusCode();
+                    statusCode = httpResponse.getStatusCode();
                 } else {
                     throw new NoConnectionError(e);
                 }
                 VolleyLog.e("Unexpected response code %d for %s", statusCode, request.getUrl());
                 NetworkResponse networkResponse;
                 if (responseContents != null) {
-                    networkResponse = new NetworkResponse(statusCode, responseContents,
-                            responseHeaders, false, SystemClock.elapsedRealtime() - requestStart);
-                    if (statusCode == HttpStatus.SC_UNAUTHORIZED ||
-                            statusCode == HttpStatus.SC_FORBIDDEN) {
+                    networkResponse = new NetworkResponse(statusCode, responseContents, false,
+                            SystemClock.elapsedRealtime() - requestStart, responseHeaders);
+                    if (statusCode == HttpURLConnection.HTTP_UNAUTHORIZED ||
+                            statusCode == HttpURLConnection.HTTP_FORBIDDEN) {
                         attemptRetryOnException("auth",
                                 request, new AuthFailureError(networkResponse));
                     } else if (statusCode >= 400 && statusCode <= 499) {
@@ -184,12 +210,12 @@
      * Logs requests that took over SLOW_REQUEST_THRESHOLD_MS to complete.
      */
     private void logSlowRequests(long requestLifetime, Request<?> request,
-            byte[] responseContents, StatusLine statusLine) {
+            byte[] responseContents, int statusCode) {
         if (DEBUG || requestLifetime > SLOW_REQUEST_THRESHOLD_MS) {
             VolleyLog.d("HTTP response for request=<%s> [lifetime=%d], [size=%s], " +
                     "[rc=%d], [retryCount=%s]", request, requestLifetime,
                     responseContents != null ? responseContents.length : "null",
-                    statusLine.getStatusCode(), request.getRetryPolicy().getCurrentRetryCount());
+                    statusCode, request.getRetryPolicy().getCurrentRetryCount());
         }
     }
 
@@ -213,20 +239,24 @@
         request.addMarker(String.format("%s-retry [timeout=%s]", logPrefix, oldTimeout));
     }
 
-    private void addCacheHeaders(Map<String, String> headers, Cache.Entry entry) {
+    private Map<String, String> getCacheHeaders(Cache.Entry entry) {
         // If there's no cache entry, we're done.
         if (entry == null) {
-            return;
+            return Collections.emptyMap();
         }
 
+        Map<String, String> headers = new HashMap<>();
+
         if (entry.etag != null) {
             headers.put("If-None-Match", entry.etag);
         }
 
         if (entry.lastModified > 0) {
-            Date refTime = new Date(entry.lastModified);
-            headers.put("If-Modified-Since", DateUtils.formatDate(refTime));
+            headers.put("If-Modified-Since",
+                    HttpHeaderParser.formatEpochAsRfc1123(entry.lastModified));
         }
+
+        return headers;
     }
 
     protected void logError(String what, String url, long start) {
@@ -234,13 +264,13 @@
         VolleyLog.v("HTTP ERROR(%s) %d ms to fetch %s", what, (now - start), url);
     }
 
-    /** Reads the contents of HttpEntity into a byte[]. */
-    private byte[] entityToBytes(HttpEntity entity) throws IOException, ServerError {
+    /** Reads the contents of an InputStream into a byte[]. */
+    private byte[] inputStreamToBytes(InputStream in, int contentLength)
+            throws IOException, ServerError {
         PoolingByteArrayOutputStream bytes =
-                new PoolingByteArrayOutputStream(mPool, (int) entity.getContentLength());
+                new PoolingByteArrayOutputStream(mPool, contentLength);
         byte[] buffer = null;
         try {
-            InputStream in = entity.getContent();
             if (in == null) {
                 throw new ServerError();
             }
@@ -253,11 +283,13 @@
         } finally {
             try {
                 // Close the InputStream and release the resources by "consuming the content".
-                entity.consumeContent();
+                if (in != null) {
+                    in.close();
+                }
             } catch (IOException e) {
-                // This can happen if there was an exception above that left the entity in
+                // This can happen if there was an exception above that left the stream in
                 // an invalid state.
-                VolleyLog.v("Error occurred when calling consumingContent");
+                VolleyLog.v("Error occurred when closing InputStream");
             }
             mPool.returnBuf(buffer);
             bytes.close();
@@ -266,12 +298,62 @@
 
     /**
      * Converts Headers[] to Map&lt;String, String&gt;.
+     *
+     * @deprecated Should never have been exposed in the API. This method may be removed in a future
+     *             release of Volley.
      */
+    @Deprecated
     protected static Map<String, String> convertHeaders(Header[] headers) {
-        Map<String, String> result = new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER);
+        Map<String, String> result = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
         for (int i = 0; i < headers.length; i++) {
             result.put(headers[i].getName(), headers[i].getValue());
         }
         return result;
     }
+
+    /**
+     * Combine cache headers with network response headers for an HTTP 304 response.
+     *
+     * <p>An HTTP 304 response does not have all header fields. We have to use the header fields
+     * from the cache entry plus the new ones from the response. See also:
+     * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
+     *
+     * @param responseHeaders Headers from the network response.
+     * @param entry The cached response.
+     * @return The combined list of headers.
+     */
+    private static List<Header> combineHeaders(List<Header> responseHeaders, Entry entry) {
+        // First, create a case-insensitive set of header names from the network
+        // response.
+        Set<String> headerNamesFromNetworkResponse =
+                new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
+        if (!responseHeaders.isEmpty()) {
+            for (Header header : responseHeaders) {
+                headerNamesFromNetworkResponse.add(header.getName());
+            }
+        }
+
+        // Second, add headers from the cache entry to the network response as long as
+        // they didn't appear in the network response, which should take precedence.
+        List<Header> combinedHeaders = new ArrayList<>(responseHeaders);
+        if (entry.allResponseHeaders != null) {
+            if (!entry.allResponseHeaders.isEmpty()) {
+                for (Header header : entry.allResponseHeaders) {
+                    if (!headerNamesFromNetworkResponse.contains(header.getName())) {
+                        combinedHeaders.add(header);
+                    }
+                }
+            }
+        } else {
+            // Legacy caches only have entry.responseHeaders.
+            if (!entry.responseHeaders.isEmpty()) {
+                for (Map.Entry<String, String> header : entry.responseHeaders.entrySet()) {
+                    if (!headerNamesFromNetworkResponse.contains(header.getKey())) {
+                        combinedHeaders.add(new Header(header.getKey(), header.getValue()));
+                    }
+                }
+            }
+        }
+        return combinedHeaders;
+    }
 }
diff --git a/src/main/java/com/android/volley/toolbox/DiskBasedCache.java b/src/main/java/com/android/volley/toolbox/DiskBasedCache.java
index 0e65183..a6cd960 100644
--- a/src/main/java/com/android/volley/toolbox/DiskBasedCache.java
+++ b/src/main/java/com/android/volley/toolbox/DiskBasedCache.java
@@ -20,6 +20,7 @@
 import android.text.TextUtils;
 
 import com.android.volley.Cache;
+import com.android.volley.Header;
 import com.android.volley.VolleyLog;
 
 import java.io.BufferedInputStream;
@@ -34,15 +35,18 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.util.ArrayList;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
+import java.util.List;
 import java.util.Map;
 
 /**
  * Cache implementation that caches files directly onto the hard disk in the specified
  * directory. The default disk usage size is 5MB, but is configurable.
+ *
+ * <p>This cache supports the {@link Entry#allResponseHeaders} headers field.
  */
 public class DiskBasedCache implements Cache {
 
@@ -379,30 +383,40 @@
         final long softTtl;
 
         /** Headers from the response resulting in this cache entry. */
-        final Map<String, String> responseHeaders;
+        final List<Header> allResponseHeaders;
 
         private CacheHeader(String key, String etag, long serverDate, long lastModified, long ttl,
-                           long softTtl, Map<String, String> responseHeaders) {
+                           long softTtl, List<Header> allResponseHeaders) {
             this.key = key;
             this.etag = ("".equals(etag)) ? null : etag;
             this.serverDate = serverDate;
             this.lastModified = lastModified;
             this.ttl = ttl;
             this.softTtl = softTtl;
-            this.responseHeaders = responseHeaders;
+            this.allResponseHeaders = allResponseHeaders;
         }
 
         /**
-         * Instantiates a new CacheHeader object
+         * Instantiates a new CacheHeader object.
          * @param key The key that identifies the cache entry
          * @param entry The cache entry.
          */
         CacheHeader(String key, Entry entry) {
             this(key, entry.etag, entry.serverDate, entry.lastModified, entry.ttl, entry.softTtl,
-                    entry.responseHeaders);
+                    getAllResponseHeaders(entry));
             size = entry.data.length;
         }
 
+        private static List<Header> getAllResponseHeaders(Entry entry) {
+            // If the entry contains all the response headers, use that field directly.
+            if (entry.allResponseHeaders != null) {
+                return entry.allResponseHeaders;
+            }
+
+            // Legacy fallback - copy headers from the map.
+            return HttpHeaderParser.toAllHeaderList(entry.responseHeaders);
+        }
+
         /**
          * Reads the header from a CountingInputStream and returns a CacheHeader object.
          * @param is The InputStream to read from.
@@ -420,9 +434,9 @@
             long lastModified = readLong(is);
             long ttl = readLong(is);
             long softTtl = readLong(is);
-            Map<String, String> responseHeaders = readStringStringMap(is);
+            List<Header> allResponseHeaders = readHeaderList(is);
             return new CacheHeader(
-                    key, etag, serverDate, lastModified, ttl, softTtl, responseHeaders);
+                    key, etag, serverDate, lastModified, ttl, softTtl, allResponseHeaders);
         }
 
         /**
@@ -436,11 +450,11 @@
             e.lastModified = lastModified;
             e.ttl = ttl;
             e.softTtl = softTtl;
-            e.responseHeaders = responseHeaders;
+            e.responseHeaders = HttpHeaderParser.toHeaderMap(allResponseHeaders);
+            e.allResponseHeaders = Collections.unmodifiableList(allResponseHeaders);
             return e;
         }
 
-
         /**
          * Writes the contents of this CacheHeader to the specified OutputStream.
          */
@@ -453,7 +467,7 @@
                 writeLong(os, lastModified);
                 writeLong(os, ttl);
                 writeLong(os, softTtl);
-                writeStringStringMap(responseHeaders, os);
+                writeHeaderList(allResponseHeaders, os);
                 os.flush();
                 return true;
             } catch (IOException e) {
@@ -574,27 +588,27 @@
         return new String(b, "UTF-8");
     }
 
-    static void writeStringStringMap(Map<String, String> map, OutputStream os) throws IOException {
-        if (map != null) {
-            writeInt(os, map.size());
-            for (Map.Entry<String, String> entry : map.entrySet()) {
-                writeString(os, entry.getKey());
-                writeString(os, entry.getValue());
+    static void writeHeaderList(List<Header> headers, OutputStream os) throws IOException {
+        if (headers != null) {
+            writeInt(os, headers.size());
+            for (Header header : headers) {
+                writeString(os, header.getName());
+                writeString(os, header.getValue());
             }
         } else {
             writeInt(os, 0);
         }
     }
 
-    static Map<String, String> readStringStringMap(CountingInputStream cis) throws IOException {
+    static List<Header> readHeaderList(CountingInputStream cis) throws IOException {
         int size = readInt(cis);
-        Map<String, String> result = (size == 0)
-                ? Collections.<String, String>emptyMap()
-                : new HashMap<String, String>(size);
+        List<Header> result = (size == 0)
+                ? Collections.<Header>emptyList()
+                : new ArrayList<Header>(size);
         for (int i = 0; i < size; i++) {
-            String key = readString(cis).intern();
+            String name = readString(cis).intern();
             String value = readString(cis).intern();
-            result.put(key, value);
+            result.add(new Header(name, value));
         }
         return result;
     }
diff --git a/src/main/java/com/android/volley/toolbox/HttpClientStack.java b/src/main/java/com/android/volley/toolbox/HttpClientStack.java
index 377110e..023ee21 100644
--- a/src/main/java/com/android/volley/toolbox/HttpClientStack.java
+++ b/src/main/java/com/android/volley/toolbox/HttpClientStack.java
@@ -46,7 +46,11 @@
 
 /**
  * An HttpStack that performs request over an {@link HttpClient}.
+ *
+ * @deprecated The Apache HTTP library on Android is deprecated. Use {@link HurlStack} or another
+ *             {@link BaseHttpStack} implementation.
  */
+@Deprecated
 public class HttpClientStack implements HttpStack {
     protected final HttpClient mClient;
 
diff --git a/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java b/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java
index f53063c..211c329 100644
--- a/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java
+++ b/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java
@@ -17,19 +17,31 @@
 package com.android.volley.toolbox;
 
 import com.android.volley.Cache;
+import com.android.volley.Header;
 import com.android.volley.NetworkResponse;
+import com.android.volley.VolleyLog;
 
-import org.apache.http.impl.cookie.DateParseException;
-import org.apache.http.impl.cookie.DateUtils;
-import org.apache.http.protocol.HTTP;
-
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
 import java.util.Map;
+import java.util.TimeZone;
+import java.util.TreeMap;
 
 /**
  * Utility methods for parsing HTTP headers.
  */
 public class HttpHeaderParser {
 
+    static final String HEADER_CONTENT_TYPE = "Content-Type";
+
+    private static final String DEFAULT_CONTENT_CHARSET = "ISO-8859-1";
+
+    private static final String RFC1123_FORMAT = "EEE, dd MMM yyyy HH:mm:ss zzz";
+
     /**
      * Extracts a {@link com.android.volley.Cache.Entry} from a {@link NetworkResponse}.
      *
@@ -116,6 +128,7 @@
         entry.serverDate = serverDate;
         entry.lastModified = lastModified;
         entry.responseHeaders = headers;
+        entry.allResponseHeaders = response.allHeaders;
 
         return entry;
     }
@@ -126,13 +139,26 @@
     public static long parseDateAsEpoch(String dateStr) {
         try {
             // Parse date in RFC1123 format if this header contains one
-            return DateUtils.parseDate(dateStr).getTime();
-        } catch (DateParseException e) {
+            return newRfc1123Formatter().parse(dateStr).getTime();
+        } catch (ParseException e) {
             // Date in invalid format, fallback to 0
+            VolleyLog.e(e, "Unable to parse dateStr: %s, falling back to 0", dateStr);
             return 0;
         }
     }
 
+    /** Format an epoch date in RFC1123 format. */
+    static String formatEpochAsRfc1123(long epoch) {
+        return newRfc1123Formatter().format(new Date(epoch));
+    }
+
+    private static SimpleDateFormat newRfc1123Formatter() {
+        SimpleDateFormat formatter =
+                new SimpleDateFormat(RFC1123_FORMAT, Locale.US);
+        formatter.setTimeZone(TimeZone.getTimeZone("GMT"));
+        return formatter;
+    }
+
     /**
      * Retrieve a charset from headers
      *
@@ -142,7 +168,7 @@
      * or the defaultCharset if none can be found.
      */
     public static String parseCharset(Map<String, String> headers, String defaultCharset) {
-        String contentType = headers.get(HTTP.CONTENT_TYPE);
+        String contentType = headers.get(HEADER_CONTENT_TYPE);
         if (contentType != null) {
             String[] params = contentType.split(";");
             for (int i = 1; i < params.length; i++) {
@@ -163,6 +189,28 @@
      * or the HTTP default (ISO-8859-1) if none can be found.
      */
     public static String parseCharset(Map<String, String> headers) {
-        return parseCharset(headers, HTTP.DEFAULT_CONTENT_CHARSET);
+        return parseCharset(headers, DEFAULT_CONTENT_CHARSET);
+    }
+
+    // Note - these are copied from NetworkResponse to avoid making them public (as needed to access
+    // them from the .toolbox package), which would mean they'd become part of the Volley API.
+    // TODO: Consider obfuscating official releases so we can share utility methods between Volley
+    // and Toolbox without making them public APIs.
+
+    static Map<String, String> toHeaderMap(List<Header> allHeaders) {
+        Map<String, String> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+        // Later elements in the list take precedence.
+        for (Header header : allHeaders) {
+            headers.put(header.getName(), header.getValue());
+        }
+        return headers;
+    }
+
+    static List<Header> toAllHeaderList(Map<String, String> headers) {
+        List<Header> allHeaders = new ArrayList<>(headers.size());
+        for (Map.Entry<String, String> header : headers.entrySet()) {
+            allHeaders.add(new Header(header.getKey(), header.getValue()));
+        }
+        return allHeaders;
     }
 }
diff --git a/src/main/java/com/android/volley/toolbox/HttpResponse.java b/src/main/java/com/android/volley/toolbox/HttpResponse.java
new file mode 100644
index 0000000..db719bc
--- /dev/null
+++ b/src/main/java/com/android/volley/toolbox/HttpResponse.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.volley.toolbox;
+
+import com.android.volley.Header;
+
+import java.io.InputStream;
+import java.util.Collections;
+import java.util.List;
+
+/** A response from an HTTP server. */
+public final class HttpResponse {
+
+    private final int mStatusCode;
+    private final List<Header> mHeaders;
+    private final int mContentLength;
+    private final InputStream mContent;
+
+    /**
+     * Construct a new HttpResponse for an empty response body.
+     *
+     * @param statusCode the HTTP status code of the response
+     * @param headers the response headers
+     */
+    public HttpResponse(int statusCode, List<Header> headers) {
+        this(statusCode, headers, -1 /* contentLength */, null /* content */);
+    }
+
+    /**
+     * Construct a new HttpResponse.
+     *
+     * @param statusCode the HTTP status code of the response
+     * @param headers the response headers
+     * @param contentLength the length of the response content. Ignored if there is no content.
+     * @param content an {@link InputStream} of the response content. May be null to indicate that
+     *     the response has no content.
+     */
+    public HttpResponse(
+            int statusCode, List<Header> headers, int contentLength, InputStream content) {
+        mStatusCode = statusCode;
+        mHeaders = headers;
+        mContentLength = contentLength;
+        mContent = content;
+    }
+
+    /** Returns the HTTP status code of the response. */
+    public final int getStatusCode() {
+        return mStatusCode;
+    }
+
+    /** Returns the response headers. Must not be mutated directly. */
+    public final List<Header> getHeaders() {
+        return Collections.unmodifiableList(mHeaders);
+    }
+
+    /** Returns the length of the content. Only valid if {@link #getContent} is non-null. */
+    public final int getContentLength() {
+        return mContentLength;
+    }
+
+    /**
+     * Returns an {@link InputStream} of the response content. May be null to indicate that the
+     * response has no content.
+     */
+    public final InputStream getContent() {
+        return mContent;
+    }
+}
diff --git a/src/main/java/com/android/volley/toolbox/HttpStack.java b/src/main/java/com/android/volley/toolbox/HttpStack.java
index 06f6017..5d34b44 100644
--- a/src/main/java/com/android/volley/toolbox/HttpStack.java
+++ b/src/main/java/com/android/volley/toolbox/HttpStack.java
@@ -26,7 +26,12 @@
 
 /**
  * An HTTP stack abstraction.
+ *
+ * @deprecated This interface should be avoided as it depends on the deprecated Apache HTTP library.
+ *     Use {@link BaseHttpStack} to avoid this dependency. This class may be removed in a future
+ *     release of Volley.
  */
+@Deprecated
 public interface HttpStack {
     /**
      * Performs an HTTP request with the given parameters.
diff --git a/src/main/java/com/android/volley/toolbox/HurlStack.java b/src/main/java/com/android/volley/toolbox/HurlStack.java
index 66f441d..a975a71 100644
--- a/src/main/java/com/android/volley/toolbox/HurlStack.java
+++ b/src/main/java/com/android/volley/toolbox/HurlStack.java
@@ -17,29 +17,19 @@
 package com.android.volley.toolbox;
 
 import com.android.volley.AuthFailureError;
+import com.android.volley.Header;
 import com.android.volley.Request;
 import com.android.volley.Request.Method;
 
-import org.apache.http.Header;
-import org.apache.http.HttpEntity;
-import org.apache.http.HttpResponse;
-import org.apache.http.HttpStatus;
-import org.apache.http.ProtocolVersion;
-import org.apache.http.StatusLine;
-import org.apache.http.entity.BasicHttpEntity;
-import org.apache.http.message.BasicHeader;
-import org.apache.http.message.BasicHttpResponse;
-import org.apache.http.message.BasicStatusLine;
-
 import java.io.DataOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.HttpURLConnection;
 import java.net.URL;
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Map.Entry;
 
 import javax.net.ssl.HttpsURLConnection;
 import javax.net.ssl.SSLSocketFactory;
@@ -47,9 +37,9 @@
 /**
  * An {@link HttpStack} based on {@link HttpURLConnection}.
  */
-public class HurlStack implements HttpStack {
+public class HurlStack extends BaseHttpStack {
 
-    private static final String HEADER_CONTENT_TYPE = "Content-Type";
+    private static final int HTTP_CONTINUE = 100;
 
     /**
      * An interface for transforming URLs before use.
@@ -86,10 +76,10 @@
     }
 
     @Override
-    public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
+    public HttpResponse executeRequest(Request<?> request, Map<String, String> additionalHeaders)
             throws IOException, AuthFailureError {
         String url = request.getUrl();
-        HashMap<String, String> map = new HashMap<String, String>();
+        HashMap<String, String> map = new HashMap<>();
         map.putAll(request.getHeaders());
         map.putAll(additionalHeaders);
         if (mUrlRewriter != null) {
@@ -106,26 +96,34 @@
         }
         setConnectionParametersForRequest(connection, request);
         // Initialize HttpResponse with data from the HttpURLConnection.
-        ProtocolVersion protocolVersion = new ProtocolVersion("HTTP", 1, 1);
         int responseCode = connection.getResponseCode();
         if (responseCode == -1) {
             // -1 is returned by getResponseCode() if the response code could not be retrieved.
             // Signal to the caller that something was wrong with the connection.
             throw new IOException("Could not retrieve response code from HttpUrlConnection.");
         }
-        StatusLine responseStatus = new BasicStatusLine(protocolVersion,
-                connection.getResponseCode(), connection.getResponseMessage());
-        BasicHttpResponse response = new BasicHttpResponse(responseStatus);
-        if (hasResponseBody(request.getMethod(), responseStatus.getStatusCode())) {
-            response.setEntity(entityFromConnection(connection));
+
+        if (!hasResponseBody(request.getMethod(), responseCode)) {
+            return new HttpResponse(responseCode, convertHeaders(connection.getHeaderFields()));
         }
-        for (Entry<String, List<String>> header : connection.getHeaderFields().entrySet()) {
-            if (header.getKey() != null) {
-                Header h = new BasicHeader(header.getKey(), header.getValue().get(0));
-                response.addHeader(h);
+
+        return new HttpResponse(responseCode, convertHeaders(connection.getHeaderFields()),
+                connection.getContentLength(), inputStreamFromConnection(connection));
+    }
+
+    // VisibleForTesting
+    static List<Header> convertHeaders(Map<String, List<String>> responseHeaders) {
+        List<Header> headerList = new ArrayList<>(responseHeaders.size());
+        for (Map.Entry<String, List<String>> entry : responseHeaders.entrySet()) {
+            // HttpUrlConnection includes the status line as a header with a null key; omit it here
+            // since it's not really a header and the rest of Volley assumes non-null keys.
+            if (entry.getKey() != null) {
+                for (String value : entry.getValue()) {
+                    headerList.add(new Header(entry.getKey(), value));
+                }
             }
         }
-        return response;
+        return headerList;
     }
 
     /**
@@ -137,29 +135,24 @@
      */
     private static boolean hasResponseBody(int requestMethod, int responseCode) {
         return requestMethod != Request.Method.HEAD
-            && !(HttpStatus.SC_CONTINUE <= responseCode && responseCode < HttpStatus.SC_OK)
-            && responseCode != HttpStatus.SC_NO_CONTENT
-            && responseCode != HttpStatus.SC_NOT_MODIFIED;
+            && !(HTTP_CONTINUE <= responseCode && responseCode < HttpURLConnection.HTTP_OK)
+            && responseCode != HttpURLConnection.HTTP_NO_CONTENT
+            && responseCode != HttpURLConnection.HTTP_NOT_MODIFIED;
     }
 
     /**
-     * Initializes an {@link HttpEntity} from the given {@link HttpURLConnection}.
+     * Initializes an {@link InputStream} from the given {@link HttpURLConnection}.
      * @param connection
      * @return an HttpEntity populated with data from <code>connection</code>.
      */
-    private static HttpEntity entityFromConnection(HttpURLConnection connection) {
-        BasicHttpEntity entity = new BasicHttpEntity();
+    private static InputStream inputStreamFromConnection(HttpURLConnection connection) {
         InputStream inputStream;
         try {
             inputStream = connection.getInputStream();
         } catch (IOException ioe) {
             inputStream = connection.getErrorStream();
         }
-        entity.setContent(inputStream);
-        entity.setContentLength(connection.getContentLength());
-        entity.setContentEncoding(connection.getContentEncoding());
-        entity.setContentType(connection.getContentType());
-        return entity;
+        return inputStream;
     }
 
     /**
@@ -261,7 +254,8 @@
         // since this is handled by HttpURLConnection using the size of the prepared
         // output stream.
         connection.setDoOutput(true);
-        connection.addRequestProperty(HEADER_CONTENT_TYPE, request.getBodyContentType());
+        connection.addRequestProperty(
+                HttpHeaderParser.HEADER_CONTENT_TYPE, request.getBodyContentType());
         DataOutputStream out = new DataOutputStream(connection.getOutputStream());
         out.write(body);
         out.close();
diff --git a/src/main/java/com/android/volley/toolbox/Volley.java b/src/main/java/com/android/volley/toolbox/Volley.java
index 0e04e87..6ec08b1 100644
--- a/src/main/java/com/android/volley/toolbox/Volley.java
+++ b/src/main/java/com/android/volley/toolbox/Volley.java
@@ -36,35 +36,59 @@
      * Creates a default instance of the worker pool and calls {@link RequestQueue#start()} on it.
      *
      * @param context A {@link Context} to use for creating the cache dir.
-     * @param stack An {@link HttpStack} to use for the network, or null for default.
+     * @param stack A {@link BaseHttpStack} to use for the network, or null for default.
      * @return A started {@link RequestQueue} instance.
      */
-    public static RequestQueue newRequestQueue(Context context, HttpStack stack) {
-        File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);
-
-        String userAgent = "volley/0";
-        try {
-            String packageName = context.getPackageName();
-            PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);
-            userAgent = packageName + "/" + info.versionCode;
-        } catch (NameNotFoundException e) {
-        }
-
+    public static RequestQueue newRequestQueue(Context context, BaseHttpStack stack) {
+        BasicNetwork network;
         if (stack == null) {
             if (Build.VERSION.SDK_INT >= 9) {
-                stack = new HurlStack();
+                network = new BasicNetwork(new HurlStack());
             } else {
                 // Prior to Gingerbread, HttpUrlConnection was unreliable.
                 // See: http://android-developers.blogspot.com/2011/09/androids-http-clients.html
-                stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent));
+                // At some point in the future we'll move our minSdkVersion past Froyo and can
+                // delete this fallback (along with all Apache HTTP code).
+                String userAgent = "volley/0";
+                try {
+                    String packageName = context.getPackageName();
+                    PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);
+                    userAgent = packageName + "/" + info.versionCode;
+                } catch (NameNotFoundException e) {
+                }
+
+                network = new BasicNetwork(
+                        new HttpClientStack(AndroidHttpClient.newInstance(userAgent)));
             }
+        } else {
+            network = new BasicNetwork(stack);
         }
 
-        Network network = new BasicNetwork(stack);
+        return newRequestQueue(context, network);
+    }
 
+    /**
+     * Creates a default instance of the worker pool and calls {@link RequestQueue#start()} on it.
+     *
+     * @param context A {@link Context} to use for creating the cache dir.
+     * @param stack An {@link HttpStack} to use for the network, or null for default.
+     * @return A started {@link RequestQueue} instance.
+     * @deprecated Use {@link #newRequestQueue(Context, BaseHttpStack)} instead to avoid depending
+     *             on Apache HTTP. This method may be removed in a future release of Volley.
+     */
+    @Deprecated
+    @SuppressWarnings("deprecation")
+    public static RequestQueue newRequestQueue(Context context, HttpStack stack) {
+        if (stack == null) {
+            return newRequestQueue(context, (BaseHttpStack) null);
+        }
+        return newRequestQueue(context, new BasicNetwork(stack));
+    }
+
+    private static RequestQueue newRequestQueue(Context context, Network network) {
+        File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);
         RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
         queue.start();
-
         return queue;
     }
 
@@ -75,6 +99,6 @@
      * @return A started {@link RequestQueue} instance.
      */
     public static RequestQueue newRequestQueue(Context context) {
-        return newRequestQueue(context, null);
+        return newRequestQueue(context, (BaseHttpStack) null);
     }
 }
diff --git a/src/test/java/com/android/volley/CacheDispatcherTest.java b/src/test/java/com/android/volley/CacheDispatcherTest.java
index 42bdda0..54886f8 100644
--- a/src/test/java/com/android/volley/CacheDispatcherTest.java
+++ b/src/test/java/com/android/volley/CacheDispatcherTest.java
@@ -112,4 +112,60 @@
         Request request = mNetworkQueue.take();
         assertSame(entry, request.getCacheEntry());
     }
+
+    @Test public void duplicateCacheMiss() throws Exception {
+        MockRequest secondRequest = new MockRequest();
+        mRequest.setSequence(1);
+        secondRequest.setSequence(2);
+        mCacheQueue.add(mRequest);
+        mCacheQueue.add(secondRequest);
+        mCacheQueue.waitUntilEmpty(TIMEOUT_MILLIS);
+        assertTrue(mNetworkQueue.size() == 1);
+        assertFalse(mDelivery.postResponse_called);
+    }
+
+    @Test public void duplicateSoftExpiredCacheHit_failedRequest() throws Exception {
+        Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, false, true);
+        mCache.setEntryToReturn(entry);
+
+        MockRequest secondRequest = new MockRequest();
+        mRequest.setSequence(1);
+        secondRequest.setSequence(2);
+
+        mCacheQueue.add(mRequest);
+        mCacheQueue.add(secondRequest);
+        mCacheQueue.waitUntilEmpty(TIMEOUT_MILLIS);
+
+        assertTrue(mNetworkQueue.size() == 1);
+        assertTrue(mDelivery.postResponse_calledNtimes == 2);
+
+        Request request = mNetworkQueue.take();
+        request.notifyListenerResponseNotUsable();
+        // Second request should now be in network queue.
+        assertTrue(mNetworkQueue.size() == 1);
+        request = mNetworkQueue.take();
+        assertTrue(request.equals(secondRequest));
+    }
+
+    @Test public void duplicateSoftExpiredCacheHit_successfulRequest() throws Exception {
+        Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, false, true);
+        mCache.setEntryToReturn(entry);
+
+        MockRequest secondRequest = new MockRequest();
+        mRequest.setSequence(1);
+        secondRequest.setSequence(2);
+
+        mCacheQueue.add(mRequest);
+        mCacheQueue.add(secondRequest);
+        mCacheQueue.waitUntilEmpty(TIMEOUT_MILLIS);
+
+        assertTrue(mNetworkQueue.size() == 1);
+        assertTrue(mDelivery.postResponse_calledNtimes == 2);
+
+        Request request = mNetworkQueue.take();
+        request.notifyListenerResponseReceived(Response.success(null, entry));
+        // Second request should have delivered response.
+        assertTrue(mNetworkQueue.size() == 0);
+        assertTrue(mDelivery.postResponse_calledNtimes == 3);
+    }
 }
diff --git a/src/test/java/com/android/volley/NetworkResponseTest.java b/src/test/java/com/android/volley/NetworkResponseTest.java
new file mode 100644
index 0000000..be34143
--- /dev/null
+++ b/src/test/java/com/android/volley/NetworkResponseTest.java
@@ -0,0 +1,63 @@
+package com.android.volley;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+@RunWith(RobolectricTestRunner.class)
+public class NetworkResponseTest {
+
+    @SuppressWarnings("deprecation")
+    @Test
+    public void mapToList() {
+        Map<String, String> headers = new HashMap<>();
+        headers.put("key1", "value1");
+        headers.put("key2", "value2");
+
+        NetworkResponse resp = new NetworkResponse(200, null, headers, false);
+
+        List<Header> expectedHeaders = new ArrayList<>();
+        expectedHeaders.add(new Header("key1", "value1"));
+        expectedHeaders.add(new Header("key2", "value2"));
+
+        assertThat(expectedHeaders,
+                containsInAnyOrder(resp.allHeaders.toArray(new Header[resp.allHeaders.size()])));
+    }
+
+    @Test
+    public void listToMap() {
+        List<Header> headers = new ArrayList<>();
+        headers.add(new Header("key1", "value1"));
+        // Later values should be preferred.
+        headers.add(new Header("key2", "ignoredvalue"));
+        headers.add(new Header("key2", "value2"));
+
+        NetworkResponse resp = new NetworkResponse(200, null, false, 0L, headers);
+
+        Map<String, String> expectedHeaders = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+        expectedHeaders.put("key1", "value1");
+        expectedHeaders.put("key2", "value2");
+
+        assertEquals(expectedHeaders, resp.headers);
+    }
+
+    @SuppressWarnings("deprecation")
+    @Test
+    public void nullValuesDontCrash() {
+        new NetworkResponse(null);
+        new NetworkResponse(null, null);
+        new NetworkResponse(200, null, null, false);
+        new NetworkResponse(200, null, null, false, 0L);
+        new NetworkResponse(200, null, false, 0L, null);
+    }
+}
diff --git a/src/test/java/com/android/volley/RequestQueueIntegrationTest.java b/src/test/java/com/android/volley/RequestQueueIntegrationTest.java
index a73435c..304a1ab 100644
--- a/src/test/java/com/android/volley/RequestQueueIntegrationTest.java
+++ b/src/test/java/com/android/volley/RequestQueueIntegrationTest.java
@@ -22,29 +22,30 @@
 import com.android.volley.mock.ShadowSystemClock;
 import com.android.volley.toolbox.NoCache;
 import com.android.volley.utils.ImmediateResponseDelivery;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.InOrder;
 import org.mockito.Mock;
 import org.mockito.invocation.InvocationOnMock;
 import org.mockito.stubbing.Answer;
 import org.robolectric.RobolectricTestRunner;
 import org.robolectric.annotation.Config;
 
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Random;
-import java.util.concurrent.Semaphore;
-import java.util.concurrent.TimeUnit;
-
-import static org.junit.Assert.*;
-import static org.mockito.Mockito.*;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 import static org.mockito.MockitoAnnotations.initMocks;
 
 
 /**
- * Integration tests for {@link RequestQueue}, that verify its behavior in conjunction with real dispatcher, queues and
- * Requests. Network is mocked out
+ * Integration tests for {@link RequestQueue} that verify its behavior in conjunction with real
+ * dispatcher, queues and Requests.
+ *
+ * <p>The Network is mocked out.
  */
 @RunWith(RobolectricTestRunner.class)
 @Config(shadows = {ShadowSystemClock.class})
@@ -52,6 +53,8 @@
 
     private ResponseDelivery mDelivery;
     @Mock private Network mMockNetwork;
+    @Mock private RequestFinishedListener<byte[]> mMockListener;
+    @Mock private RequestFinishedListener<byte[]> mMockListener2;
 
     @Before public void setUp() throws Exception {
         mDelivery = new ImmediateResponseDelivery();
@@ -59,9 +62,10 @@
     }
 
     @Test public void add_requestProcessedInCorrectOrder() throws Exception {
-        // Enqueue 2 requests with different cache keys, and different priorities. The second, higher priority request
-        // takes 20ms.
-        // Assert that first request is only handled after the first one has been parsed and delivered.
+        // Enqueue 2 requests with different cache keys, and different priorities. The second,
+        // higher priority request takes 20ms.
+        // Assert that the first request is only handled after the first one has been parsed and
+        // delivered.
         MockRequest lowerPriorityReq = new MockRequest();
         MockRequest higherPriorityReq = new MockRequest();
         lowerPriorityReq.setCacheKey("1");
@@ -69,7 +73,6 @@
         lowerPriorityReq.setPriority(Priority.LOW);
         higherPriorityReq.setPriority(Priority.HIGH);
 
-        RequestFinishedListener listener = mock(RequestFinishedListener.class);
         Answer<NetworkResponse> delayAnswer = new Answer<NetworkResponse>() {
             @Override
             public NetworkResponse answer(InvocationOnMock invocationOnMock) throws Throwable {
@@ -77,37 +80,31 @@
                 return mock(NetworkResponse.class);
             }
         };
-        //delay only for higher request
+        // delay only for higher request
         when(mMockNetwork.performRequest(higherPriorityReq)).thenAnswer(delayAnswer);
         when(mMockNetwork.performRequest(lowerPriorityReq)).thenReturn(mock(NetworkResponse.class));
 
         RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 1, mDelivery);
-        queue.addRequestFinishedListener(listener);
+        queue.addRequestFinishedListener(mMockListener);
         queue.add(lowerPriorityReq);
         queue.add(higherPriorityReq);
         queue.start();
 
-        // you cannot do strict order verification in combination with timeouts with mockito 1.9.5 :(
-        // as an alternative, first verify no requests have finished, while higherPriorityReq should be processing
-        verifyNoMoreInteractions(listener);
+        InOrder inOrder = inOrder(mMockListener);
         // verify higherPriorityReq goes through first
-        verify(listener, timeout(100)).onRequestFinished(higherPriorityReq);
+        inOrder.verify(mMockListener, timeout(10000)).onRequestFinished(higherPriorityReq);
         // verify lowerPriorityReq goes last
-        verify(listener, timeout(10)).onRequestFinished(lowerPriorityReq);
+        inOrder.verify(mMockListener, timeout(10000)).onRequestFinished(lowerPriorityReq);
+
         queue.stop();
     }
 
-    /**
-     * Asserts that requests with same cache key are processed in order.
-     *
-     * Needs to be an integration test because relies on complex interations between various queues
-     */
+    /** Asserts that requests with same cache key are processed in order. */
     @Test public void add_dedupeByCacheKey() throws Exception {
         // Enqueue 2 requests with the same cache key. The first request takes 20ms. Assert that the
         // second request is only handled after the first one has been parsed and delivered.
-        Request req1 = new MockRequest();
-        Request req2 = new MockRequest();
-        RequestFinishedListener listener = mock(RequestFinishedListener.class);
+        MockRequest req1 = new MockRequest();
+        MockRequest req2 = new MockRequest();
         Answer<NetworkResponse> delayAnswer = new Answer<NetworkResponse>() {
             @Override
             public NetworkResponse answer(InvocationOnMock invocationOnMock) throws Throwable {
@@ -120,27 +117,23 @@
         when(mMockNetwork.performRequest(req2)).thenReturn(mock(NetworkResponse.class));
 
         RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 3, mDelivery);
-        queue.addRequestFinishedListener(listener);
+        queue.addRequestFinishedListener(mMockListener);
         queue.add(req1);
         queue.add(req2);
         queue.start();
 
-        // you cannot do strict order verification with mockito 1.9.5 :(
-        // as an alternative, first verify no requests have finished, then verify req1 goes through
-        verifyNoMoreInteractions(listener);
-        verify(listener, timeout(100)).onRequestFinished(req1);
-        verify(listener, timeout(10)).onRequestFinished(req2);
+        InOrder inOrder = inOrder(mMockListener);
+        // verify req1 goes through first
+        inOrder.verify(mMockListener, timeout(10000)).onRequestFinished(req1);
+        // verify req2 goes last
+        inOrder.verify(mMockListener, timeout(10000)).onRequestFinished(req2);
+
         queue.stop();
     }
 
-    /**
-     * Verify RequestFinishedListeners are informed when requests are canceled
-     *
-     * Needs to be an integration test because relies on Request -> dispatcher -> RequestQueue interaction
-     */
+    /** Verify RequestFinishedListeners are informed when requests are canceled.  */
     @Test public void add_requestFinishedListenerCanceled() throws Exception {
-        RequestFinishedListener listener = mock(RequestFinishedListener.class);
-        Request request = new MockRequest();
+        MockRequest request = new MockRequest();
         Answer<NetworkResponse> delayAnswer = new Answer<NetworkResponse>() {
             @Override
             public NetworkResponse answer(InvocationOnMock invocationOnMock) throws Throwable {
@@ -152,56 +145,43 @@
 
         when(mMockNetwork.performRequest(request)).thenAnswer(delayAnswer);
 
-        queue.addRequestFinishedListener(listener);
+        queue.addRequestFinishedListener(mMockListener);
         queue.start();
         queue.add(request);
 
         request.cancel();
-        verify(listener, timeout(100)).onRequestFinished(request);
+        verify(mMockListener, timeout(10000)).onRequestFinished(request);
         queue.stop();
     }
 
-    /**
-     * Verify RequestFinishedListeners are informed when requests are successfully delivered
-     *
-     * Needs to be an integration test because relies on Request -> dispatcher -> RequestQueue interaction
-     */
+    /** Verify RequestFinishedListeners are informed when requests are successfully delivered. */
     @Test public void add_requestFinishedListenerSuccess() throws Exception {
-        NetworkResponse response = mock(NetworkResponse.class);
-        Request request = new MockRequest();
-        RequestFinishedListener listener = mock(RequestFinishedListener.class);
-        RequestFinishedListener listener2 = mock(RequestFinishedListener.class);
+        MockRequest request = new MockRequest();
         RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 1, mDelivery);
 
-        queue.addRequestFinishedListener(listener);
-        queue.addRequestFinishedListener(listener2);
+        queue.addRequestFinishedListener(mMockListener);
+        queue.addRequestFinishedListener(mMockListener2);
         queue.start();
         queue.add(request);
 
-        verify(listener, timeout(100)).onRequestFinished(request);
-        verify(listener2, timeout(100)).onRequestFinished(request);
+        verify(mMockListener, timeout(10000)).onRequestFinished(request);
+        verify(mMockListener2, timeout(10000)).onRequestFinished(request);
 
         queue.stop();
     }
 
-    /**
-     * Verify RequestFinishedListeners are informed when request errors
-     *
-     * Needs to be an integration test because relies on Request -> dispatcher -> RequestQueue interaction
-     */
+    /** Verify RequestFinishedListeners are informed when request errors. */
     @Test public void add_requestFinishedListenerError() throws Exception {
-        RequestFinishedListener listener = mock(RequestFinishedListener.class);
-        Request request = new MockRequest();
+        MockRequest request = new MockRequest();
         RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 1, mDelivery);
 
         when(mMockNetwork.performRequest(request)).thenThrow(new VolleyError());
 
-        queue.addRequestFinishedListener(listener);
+        queue.addRequestFinishedListener(mMockListener);
         queue.start();
         queue.add(request);
 
-        verify(listener, timeout(100)).onRequestFinished(request);
+        verify(mMockListener, timeout(10000)).onRequestFinished(request);
         queue.stop();
     }
-
 }
diff --git a/src/test/java/com/android/volley/mock/MockHttpClient.java b/src/test/java/com/android/volley/mock/MockHttpClient.java
deleted file mode 100644
index c2a36bc..0000000
--- a/src/test/java/com/android/volley/mock/MockHttpClient.java
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.volley.mock;
-
-import org.apache.http.HttpEntity;
-import org.apache.http.HttpHost;
-import org.apache.http.HttpRequest;
-import org.apache.http.HttpResponse;
-import org.apache.http.HttpStatus;
-import org.apache.http.ProtocolVersion;
-import org.apache.http.StatusLine;
-import org.apache.http.client.HttpClient;
-import org.apache.http.client.ResponseHandler;
-import org.apache.http.client.methods.HttpUriRequest;
-import org.apache.http.conn.ClientConnectionManager;
-import org.apache.http.message.BasicHttpResponse;
-import org.apache.http.message.BasicStatusLine;
-import org.apache.http.params.HttpParams;
-import org.apache.http.protocol.HttpContext;
-
-
-public class MockHttpClient implements HttpClient {
-    private int mStatusCode = HttpStatus.SC_OK;
-    private HttpEntity mResponseEntity = null;
-
-    public void setResponseData(HttpEntity entity) {
-        mStatusCode = HttpStatus.SC_OK;
-        mResponseEntity = entity;
-    }
-
-    public void setErrorCode(int statusCode) {
-        if (statusCode == HttpStatus.SC_OK) {
-            throw new IllegalArgumentException("statusCode cannot be 200 for an error");
-        }
-        mStatusCode = statusCode;
-    }
-
-    public HttpUriRequest requestExecuted = null;
-
-    // This is the only one we actually use.
-    @Override
-    public HttpResponse execute(HttpUriRequest request, HttpContext context) {
-        requestExecuted = request;
-        StatusLine statusLine = new BasicStatusLine(
-                new ProtocolVersion("HTTP", 1, 1), mStatusCode, "");
-        HttpResponse response = new BasicHttpResponse(statusLine);
-        response.setEntity(mResponseEntity);
-
-        return response;
-    }
-
-
-    // Unimplemented methods ahoy
-
-    @Override
-    public HttpResponse execute(HttpUriRequest request) {
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public HttpResponse execute(HttpHost target, HttpRequest request) {
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public <T> T execute(HttpUriRequest arg0, ResponseHandler<? extends T> arg1) {
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public HttpResponse execute(HttpHost target, HttpRequest request, HttpContext context) {
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public <T> T execute(HttpUriRequest arg0, ResponseHandler<? extends T> arg1, HttpContext arg2) {
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public <T> T execute(HttpHost arg0, HttpRequest arg1, ResponseHandler<? extends T> arg2) {
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public <T> T execute(HttpHost arg0, HttpRequest arg1, ResponseHandler<? extends T> arg2,
-            HttpContext arg3) {
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public ClientConnectionManager getConnectionManager() {
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public HttpParams getParams() {
-        throw new UnsupportedOperationException();
-    }
-}
diff --git a/src/test/java/com/android/volley/mock/MockHttpStack.java b/src/test/java/com/android/volley/mock/MockHttpStack.java
index 91872d3..56b29f1 100644
--- a/src/test/java/com/android/volley/mock/MockHttpStack.java
+++ b/src/test/java/com/android/volley/mock/MockHttpStack.java
@@ -18,15 +18,14 @@
 
 import com.android.volley.AuthFailureError;
 import com.android.volley.Request;
-import com.android.volley.toolbox.HttpStack;
-
-import org.apache.http.HttpResponse;
+import com.android.volley.toolbox.BaseHttpStack;
+import com.android.volley.toolbox.HttpResponse;
 
 import java.io.IOException;
 import java.util.HashMap;
 import java.util.Map;
 
-public class MockHttpStack implements HttpStack {
+public class MockHttpStack extends BaseHttpStack {
 
     private HttpResponse mResponseToReturn;
 
@@ -59,13 +58,13 @@
     }
 
     @Override
-    public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
+    public HttpResponse executeRequest(Request<?> request, Map<String, String> additionalHeaders)
             throws IOException, AuthFailureError {
         if (mExceptionToThrow != null) {
             throw mExceptionToThrow;
         }
         mLastUrl = request.getUrl();
-        mLastHeaders = new HashMap<String, String>();
+        mLastHeaders = new HashMap<>();
         if (request.getHeaders() != null) {
             mLastHeaders.putAll(request.getHeaders());
         }
diff --git a/src/test/java/com/android/volley/mock/MockResponseDelivery.java b/src/test/java/com/android/volley/mock/MockResponseDelivery.java
index 4dbfd5c..e923c1a 100644
--- a/src/test/java/com/android/volley/mock/MockResponseDelivery.java
+++ b/src/test/java/com/android/volley/mock/MockResponseDelivery.java
@@ -25,6 +25,7 @@
 
     public boolean postResponse_called = false;
     public boolean postError_called = false;
+    public long postResponse_calledNtimes = 0;
 
     public boolean wasEitherResponseCalled() {
         return postResponse_called || postError_called;
@@ -34,12 +35,14 @@
     @Override
     public void postResponse(Request<?> request, Response<?> response) {
         postResponse_called = true;
+        postResponse_calledNtimes++;
         responsePosted = response;
     }
 
     @Override
     public void postResponse(Request<?> request, Response<?> response, Runnable runnable) {
         postResponse_called = true;
+        postResponse_calledNtimes++;
         responsePosted = response;
         runnable.run();
     }
diff --git a/src/test/java/com/android/volley/toolbox/AdaptedHttpStackTest.java b/src/test/java/com/android/volley/toolbox/AdaptedHttpStackTest.java
new file mode 100644
index 0000000..615687d
--- /dev/null
+++ b/src/test/java/com/android/volley/toolbox/AdaptedHttpStackTest.java
@@ -0,0 +1,135 @@
+package com.android.volley.toolbox;
+
+import android.util.Pair;
+
+import com.android.volley.Header;
+import com.android.volley.Request;
+import com.android.volley.mock.TestRequest;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.StatusLine;
+import org.apache.http.conn.ConnectTimeoutException;
+import org.apache.http.message.BasicHeader;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.SocketTimeoutException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.mockito.Mockito.when;
+
+@RunWith(RobolectricTestRunner.class)
+public class AdaptedHttpStackTest {
+    private static final Request<?> REQUEST = new TestRequest.Get();
+    private static final Map<String, String> ADDITIONAL_HEADERS = Collections.emptyMap();
+
+    @Mock
+    private HttpStack mHttpStack;
+    @Mock
+    private HttpResponse mHttpResponse;
+    @Mock
+    private StatusLine mStatusLine;
+    @Mock
+    private HttpEntity mHttpEntity;
+    @Mock
+    private InputStream mContent;
+
+    private AdaptedHttpStack mAdaptedHttpStack;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mAdaptedHttpStack = new AdaptedHttpStack(mHttpStack);
+        when(mHttpResponse.getStatusLine()).thenReturn(mStatusLine);
+    }
+
+    @Test(expected = SocketTimeoutException.class)
+    public void requestTimeout() throws Exception {
+        when(mHttpStack.performRequest(REQUEST, ADDITIONAL_HEADERS))
+                .thenThrow(new ConnectTimeoutException());
+
+        mAdaptedHttpStack.executeRequest(REQUEST, ADDITIONAL_HEADERS);
+    }
+
+    @Test
+    public void emptyResponse() throws Exception {
+        when(mHttpStack.performRequest(REQUEST, ADDITIONAL_HEADERS)).thenReturn(mHttpResponse);
+        when(mStatusLine.getStatusCode()).thenReturn(12345);
+        when(mHttpResponse.getAllHeaders()).thenReturn(new org.apache.http.Header[0]);
+
+        com.android.volley.toolbox.HttpResponse response =
+                mAdaptedHttpStack.executeRequest(REQUEST, ADDITIONAL_HEADERS);
+
+        assertEquals(12345, response.getStatusCode());
+        assertEquals(Collections.emptyList(), response.getHeaders());
+        assertNull(response.getContent());
+    }
+
+    @Test
+    public void nonEmptyResponse() throws Exception {
+        when(mHttpStack.performRequest(REQUEST, ADDITIONAL_HEADERS)).thenReturn(mHttpResponse);
+        when(mStatusLine.getStatusCode()).thenReturn(12345);
+        when(mHttpResponse.getAllHeaders()).thenReturn(new org.apache.http.Header[0]);
+        when(mHttpResponse.getEntity()).thenReturn(mHttpEntity);
+        when(mHttpEntity.getContentLength()).thenReturn((long) Integer.MAX_VALUE);
+        when(mHttpEntity.getContent()).thenReturn(mContent);
+
+        com.android.volley.toolbox.HttpResponse response =
+                mAdaptedHttpStack.executeRequest(REQUEST, ADDITIONAL_HEADERS);
+
+        assertEquals(12345, response.getStatusCode());
+        assertEquals(Collections.emptyList(), response.getHeaders());
+        assertEquals(Integer.MAX_VALUE, response.getContentLength());
+        assertSame(mContent, response.getContent());
+    }
+
+    @Test(expected = IOException.class)
+    public void responseTooBig() throws Exception {
+        when(mHttpStack.performRequest(REQUEST, ADDITIONAL_HEADERS)).thenReturn(mHttpResponse);
+        when(mStatusLine.getStatusCode()).thenReturn(12345);
+        when(mHttpResponse.getAllHeaders()).thenReturn(new org.apache.http.Header[0]);
+        when(mHttpResponse.getEntity()).thenReturn(mHttpEntity);
+        when(mHttpEntity.getContentLength()).thenReturn(Integer.MAX_VALUE + 1L);
+        when(mHttpEntity.getContent()).thenReturn(mContent);
+
+        mAdaptedHttpStack.executeRequest(REQUEST, ADDITIONAL_HEADERS);
+    }
+
+    @Test
+    public void responseWithHeaders() throws Exception {
+        when(mHttpStack.performRequest(REQUEST, ADDITIONAL_HEADERS)).thenReturn(mHttpResponse);
+        when(mStatusLine.getStatusCode()).thenReturn(12345);
+        when(mHttpResponse.getAllHeaders()).thenReturn(new org.apache.http.Header[] {
+                new BasicHeader("header1", "value1_B"),
+                new BasicHeader("header3", "value3"),
+                new BasicHeader("HEADER2", "value2"),
+                new BasicHeader("header1", "value1_A")
+        });
+
+        com.android.volley.toolbox.HttpResponse response =
+                mAdaptedHttpStack.executeRequest(REQUEST, ADDITIONAL_HEADERS);
+
+        assertEquals(12345, response.getStatusCode());
+        assertNull(response.getContent());
+
+        List<Header> expectedHeaders = new ArrayList<>();
+        expectedHeaders.add(new Header("header1", "value1_B"));
+        expectedHeaders.add(new Header("header3", "value3"));
+        expectedHeaders.add(new Header("HEADER2", "value2"));
+        expectedHeaders.add(new Header("header1", "value1_A"));
+        assertEquals(expectedHeaders, response.getHeaders());
+    }
+}
diff --git a/src/test/java/com/android/volley/toolbox/BaseHttpStackTest.java b/src/test/java/com/android/volley/toolbox/BaseHttpStackTest.java
new file mode 100644
index 0000000..3ae145c
--- /dev/null
+++ b/src/test/java/com/android/volley/toolbox/BaseHttpStackTest.java
@@ -0,0 +1,108 @@
+package com.android.volley.toolbox;
+
+import com.android.volley.AuthFailureError;
+import com.android.volley.Header;
+import com.android.volley.Request;
+import com.android.volley.mock.TestRequest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+
+@RunWith(RobolectricTestRunner.class)
+public class BaseHttpStackTest {
+    private static final Request<?> REQUEST = new TestRequest.Get();
+    private static final Map<String, String> ADDITIONAL_HEADERS = Collections.emptyMap();
+
+    @Mock
+    private InputStream mContent;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    @Test
+    public void legacyRequestWithoutBody() throws Exception {
+        BaseHttpStack stack = new BaseHttpStack() {
+            @Override
+            public HttpResponse executeRequest(
+                    Request<?> request, Map<String, String> additionalHeaders)
+                    throws IOException, AuthFailureError {
+                assertSame(REQUEST, request);
+                assertSame(ADDITIONAL_HEADERS, additionalHeaders);
+                return new HttpResponse(12345, Collections.<Header>emptyList());
+            }
+        };
+        org.apache.http.HttpResponse resp = stack.performRequest(REQUEST, ADDITIONAL_HEADERS);
+        assertEquals(12345, resp.getStatusLine().getStatusCode());
+        assertEquals(0, resp.getAllHeaders().length);
+        assertNull(resp.getEntity());
+    }
+
+    @Test
+    public void legacyResponseWithBody() throws Exception {
+        BaseHttpStack stack = new BaseHttpStack() {
+            @Override
+            public HttpResponse executeRequest(
+                    Request<?> request, Map<String, String> additionalHeaders)
+                    throws IOException, AuthFailureError {
+                assertSame(REQUEST, request);
+                assertSame(ADDITIONAL_HEADERS, additionalHeaders);
+                return new HttpResponse(
+                        12345,
+                        Collections.<Header>emptyList(),
+                        555,
+                        mContent);
+            }
+        };
+        org.apache.http.HttpResponse resp = stack.performRequest(REQUEST, ADDITIONAL_HEADERS);
+        assertEquals(12345, resp.getStatusLine().getStatusCode());
+        assertEquals(0, resp.getAllHeaders().length);
+        assertEquals(555L, resp.getEntity().getContentLength());
+        assertSame(mContent, resp.getEntity().getContent());
+    }
+
+    @Test
+    public void legacyResponseHeaders() throws Exception {
+        BaseHttpStack stack = new BaseHttpStack() {
+            @Override
+            public HttpResponse executeRequest(
+                    Request<?> request, Map<String, String> additionalHeaders)
+                    throws IOException, AuthFailureError {
+                assertSame(REQUEST, request);
+                assertSame(ADDITIONAL_HEADERS, additionalHeaders);
+                List<Header> headers = new ArrayList<>();
+                headers.add(new Header("HeaderA", "ValueA"));
+                headers.add(new Header("HeaderB", "ValueB_1"));
+                headers.add(new Header("HeaderB", "ValueB_2"));
+                return new HttpResponse(12345, headers);
+            }
+        };
+        org.apache.http.HttpResponse resp = stack.performRequest(REQUEST, ADDITIONAL_HEADERS);
+        assertEquals(12345, resp.getStatusLine().getStatusCode());
+        assertEquals(3, resp.getAllHeaders().length);
+        assertEquals("HeaderA", resp.getAllHeaders()[0].getName());
+        assertEquals("ValueA", resp.getAllHeaders()[0].getValue());
+        assertEquals("HeaderB", resp.getAllHeaders()[1].getName());
+        assertEquals("ValueB_1", resp.getAllHeaders()[1].getValue());
+        assertEquals("HeaderB", resp.getAllHeaders()[2].getName());
+        assertEquals("ValueB_2", resp.getAllHeaders()[2].getValue());
+        assertNull(resp.getEntity());
+    }
+}
diff --git a/src/test/java/com/android/volley/toolbox/BasicNetworkTest.java b/src/test/java/com/android/volley/toolbox/BasicNetworkTest.java
index c01d9b0..7f0d5e2 100644
--- a/src/test/java/com/android/volley/toolbox/BasicNetworkTest.java
+++ b/src/test/java/com/android/volley/toolbox/BasicNetworkTest.java
@@ -17,6 +17,8 @@
 package com.android.volley.toolbox;
 
 import com.android.volley.AuthFailureError;
+import com.android.volley.Cache.Entry;
+import com.android.volley.Header;
 import com.android.volley.NetworkResponse;
 import com.android.volley.Request;
 import com.android.volley.Response;
@@ -26,23 +28,31 @@
 import com.android.volley.VolleyError;
 import com.android.volley.mock.MockHttpStack;
 
-import org.apache.http.ProtocolVersion;
-import org.apache.http.conn.ConnectTimeoutException;
-import org.apache.http.entity.StringEntity;
-import org.apache.http.message.BasicHttpResponse;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
 import org.robolectric.RobolectricTestRunner;
 
+import java.io.ByteArrayInputStream;
 import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
 import java.net.SocketTimeoutException;
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
-import static org.junit.Assert.*;
-import static org.mockito.Mockito.*;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
 import static org.mockito.MockitoAnnotations.initMocks;
 
 @RunWith(RobolectricTestRunner.class)
@@ -50,7 +60,6 @@
 
     @Mock private Request<String> mMockRequest;
     @Mock private RetryPolicy mMockRetryPolicy;
-    private BasicNetwork mNetwork;
 
     @Before public void setUp() throws Exception {
         initMocks(this);
@@ -58,17 +67,95 @@
 
     @Test public void headersAndPostParams() throws Exception {
         MockHttpStack mockHttpStack = new MockHttpStack();
-        BasicHttpResponse fakeResponse = new BasicHttpResponse(new ProtocolVersion("HTTP", 1, 1),
-                200, "OK");
-        fakeResponse.setEntity(new StringEntity("foobar"));
+        InputStream responseStream =
+                new ByteArrayInputStream("foobar".getBytes());
+        HttpResponse fakeResponse =
+                new HttpResponse(200, Collections.<Header>emptyList(), 6, responseStream);
         mockHttpStack.setResponseToReturn(fakeResponse);
         BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack);
         Request<String> request = buildRequest();
+        Entry entry = new Entry();
+        entry.etag = "foobar";
+        entry.lastModified = 1503102002000L;
+        request.setCacheEntry(entry);
         httpNetwork.performRequest(request);
         assertEquals("foo", mockHttpStack.getLastHeaders().get("requestheader"));
+        assertEquals("foobar", mockHttpStack.getLastHeaders().get("If-None-Match"));
+        assertEquals("Sat, 19 Aug 2017 00:20:02 GMT",
+                mockHttpStack.getLastHeaders().get("If-Modified-Since"));
         assertEquals("requestpost=foo&", new String(mockHttpStack.getLastPostBody()));
     }
 
+    @Test public void notModified() throws Exception {
+        MockHttpStack mockHttpStack = new MockHttpStack();
+        List<Header> headers = new ArrayList<>();
+        headers.add(new Header("ServerKeyA", "ServerValueA"));
+        headers.add(new Header("ServerKeyB", "ServerValueB"));
+        headers.add(new Header("SharedKey", "ServerValueShared"));
+        headers.add(new Header("sharedcaseinsensitivekey", "ServerValueShared1"));
+        headers.add(new Header("SharedCaseInsensitiveKey", "ServerValueShared2"));
+        HttpResponse fakeResponse =
+                new HttpResponse(HttpURLConnection.HTTP_NOT_MODIFIED, headers);
+        mockHttpStack.setResponseToReturn(fakeResponse);
+        BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack);
+        Request<String> request = buildRequest();
+        Entry entry = new Entry();
+        entry.allResponseHeaders = new ArrayList<>();
+        entry.allResponseHeaders.add(new Header("CachedKeyA", "CachedValueA"));
+        entry.allResponseHeaders.add(new Header("CachedKeyB", "CachedValueB"));
+        entry.allResponseHeaders.add(new Header("SharedKey", "CachedValueShared"));
+        entry.allResponseHeaders.add(new Header("SHAREDCASEINSENSITIVEKEY", "CachedValueShared1"));
+        entry.allResponseHeaders.add(new Header("shAREDcaSEinSENSITIVEkeY", "CachedValueShared2"));
+        request.setCacheEntry(entry);
+        NetworkResponse response = httpNetwork.performRequest(request);
+        List<Header> expectedHeaders = new ArrayList<>();
+        // Should have all server headers + cache headers that didn't show up in server response.
+        expectedHeaders.add(new Header("ServerKeyA", "ServerValueA"));
+        expectedHeaders.add(new Header("ServerKeyB", "ServerValueB"));
+        expectedHeaders.add(new Header("SharedKey", "ServerValueShared"));
+        expectedHeaders.add(new Header("sharedcaseinsensitivekey", "ServerValueShared1"));
+        expectedHeaders.add(new Header("SharedCaseInsensitiveKey", "ServerValueShared2"));
+        expectedHeaders.add(new Header("CachedKeyA", "CachedValueA"));
+        expectedHeaders.add(new Header("CachedKeyB", "CachedValueB"));
+        assertThat(expectedHeaders, containsInAnyOrder(
+                response.allHeaders.toArray(new Header[response.allHeaders.size()])));
+    }
+
+    @Test public void notModified_legacyCache() throws Exception {
+        MockHttpStack mockHttpStack = new MockHttpStack();
+        List<Header> headers = new ArrayList<>();
+        headers.add(new Header("ServerKeyA", "ServerValueA"));
+        headers.add(new Header("ServerKeyB", "ServerValueB"));
+        headers.add(new Header("SharedKey", "ServerValueShared"));
+        headers.add(new Header("sharedcaseinsensitivekey", "ServerValueShared1"));
+        headers.add(new Header("SharedCaseInsensitiveKey", "ServerValueShared2"));
+        HttpResponse fakeResponse =
+                new HttpResponse(HttpURLConnection.HTTP_NOT_MODIFIED, headers);
+        mockHttpStack.setResponseToReturn(fakeResponse);
+        BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack);
+        Request<String> request = buildRequest();
+        Entry entry = new Entry();
+        entry.responseHeaders = new HashMap<>();
+        entry.responseHeaders.put("CachedKeyA", "CachedValueA");
+        entry.responseHeaders.put("CachedKeyB", "CachedValueB");
+        entry.responseHeaders.put("SharedKey", "CachedValueShared");
+        entry.responseHeaders.put("SHAREDCASEINSENSITIVEKEY", "CachedValueShared1");
+        entry.responseHeaders.put("shAREDcaSEinSENSITIVEkeY", "CachedValueShared2");
+        request.setCacheEntry(entry);
+        NetworkResponse response = httpNetwork.performRequest(request);
+        List<Header> expectedHeaders = new ArrayList<>();
+        // Should have all server headers + cache headers that didn't show up in server response.
+        expectedHeaders.add(new Header("ServerKeyA", "ServerValueA"));
+        expectedHeaders.add(new Header("ServerKeyB", "ServerValueB"));
+        expectedHeaders.add(new Header("SharedKey", "ServerValueShared"));
+        expectedHeaders.add(new Header("sharedcaseinsensitivekey", "ServerValueShared1"));
+        expectedHeaders.add(new Header("SharedCaseInsensitiveKey", "ServerValueShared2"));
+        expectedHeaders.add(new Header("CachedKeyA", "CachedValueA"));
+        expectedHeaders.add(new Header("CachedKeyB", "CachedValueB"));
+        assertThat(expectedHeaders, containsInAnyOrder(
+                response.allHeaders.toArray(new Header[response.allHeaders.size()])));
+    }
+
     @Test public void socketTimeout() throws Exception {
         MockHttpStack mockHttpStack = new MockHttpStack();
         mockHttpStack.setExceptionToThrow(new SocketTimeoutException());
@@ -85,22 +172,6 @@
         verify(mMockRetryPolicy).retry(any(TimeoutError.class));
     }
 
-    @Test public void connectTimeout() throws Exception {
-        MockHttpStack mockHttpStack = new MockHttpStack();
-        mockHttpStack.setExceptionToThrow(new ConnectTimeoutException());
-        BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack);
-        Request<String> request = buildRequest();
-        request.setRetryPolicy(mMockRetryPolicy);
-        doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class));
-        try {
-            httpNetwork.performRequest(request);
-        } catch (VolleyError e) {
-            // expected
-        }
-        // should retry connection timeouts
-        verify(mMockRetryPolicy).retry(any(TimeoutError.class));
-    }
-
     @Test public void noConnection() throws Exception {
         MockHttpStack mockHttpStack = new MockHttpStack();
         mockHttpStack.setExceptionToThrow(new IOException());
@@ -119,8 +190,7 @@
 
     @Test public void unauthorized() throws Exception {
         MockHttpStack mockHttpStack = new MockHttpStack();
-        BasicHttpResponse fakeResponse = new BasicHttpResponse(new ProtocolVersion("HTTP", 1, 1),
-                401, "Unauthorized");
+        HttpResponse fakeResponse = new HttpResponse(401, Collections.<Header>emptyList());
         mockHttpStack.setResponseToReturn(fakeResponse);
         BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack);
         Request<String> request = buildRequest();
@@ -137,8 +207,7 @@
 
     @Test public void forbidden() throws Exception {
         MockHttpStack mockHttpStack = new MockHttpStack();
-        BasicHttpResponse fakeResponse = new BasicHttpResponse(new ProtocolVersion("HTTP", 1, 1),
-                403, "Forbidden");
+        HttpResponse fakeResponse = new HttpResponse(403, Collections.<Header>emptyList());
         mockHttpStack.setResponseToReturn(fakeResponse);
         BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack);
         Request<String> request = buildRequest();
@@ -156,8 +225,7 @@
     @Test public void redirect() throws Exception {
         for (int i = 300; i <= 399; i++) {
             MockHttpStack mockHttpStack = new MockHttpStack();
-            BasicHttpResponse fakeResponse =
-                    new BasicHttpResponse(new ProtocolVersion("HTTP", 1, 1), i, "");
+            HttpResponse fakeResponse = new HttpResponse(i, Collections.<Header>emptyList());
             mockHttpStack.setResponseToReturn(fakeResponse);
             BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack);
             Request<String> request = buildRequest();
@@ -181,8 +249,7 @@
                 continue;
             }
             MockHttpStack mockHttpStack = new MockHttpStack();
-            BasicHttpResponse fakeResponse =
-                    new BasicHttpResponse(new ProtocolVersion("HTTP", 1, 1), i, "");
+            HttpResponse fakeResponse = new HttpResponse(i, Collections.<Header>emptyList());
             mockHttpStack.setResponseToReturn(fakeResponse);
             BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack);
             Request<String> request = buildRequest();
@@ -202,8 +269,7 @@
     @Test public void serverError_enableRetries() throws Exception {
         for (int i = 500; i <= 599; i++) {
             MockHttpStack mockHttpStack = new MockHttpStack();
-            BasicHttpResponse fakeResponse =
-                    new BasicHttpResponse(new ProtocolVersion("HTTP", 1, 1), i, "");
+            HttpResponse fakeResponse = new HttpResponse(i, Collections.<Header>emptyList());
             mockHttpStack.setResponseToReturn(fakeResponse);
             BasicNetwork httpNetwork =
                     new BasicNetwork(mockHttpStack, new ByteArrayPool(4096));
@@ -225,8 +291,7 @@
     @Test public void serverError_disableRetries() throws Exception {
         for (int i = 500; i <= 599; i++) {
             MockHttpStack mockHttpStack = new MockHttpStack();
-            BasicHttpResponse fakeResponse =
-                    new BasicHttpResponse(new ProtocolVersion("HTTP", 1, 1), i, "");
+            HttpResponse fakeResponse = new HttpResponse(i, Collections.<Header>emptyList());
             mockHttpStack.setResponseToReturn(fakeResponse);
             BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack);
             Request<String> request = buildRequest();
diff --git a/src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java b/src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java
index 3d8d1f1..04c071e 100644
--- a/src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java
+++ b/src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java
@@ -17,6 +17,7 @@
 package com.android.volley.toolbox;
 
 import com.android.volley.Cache;
+import com.android.volley.Header;
 import com.android.volley.toolbox.DiskBasedCache.CacheHeader;
 import com.android.volley.toolbox.DiskBasedCache.CountingInputStream;
 
@@ -38,7 +39,9 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Random;
 
@@ -428,28 +431,33 @@
         assertEquals(DiskBasedCache.readString(cis), "ファイカス");
     }
 
-    @Test public void serializeMap() throws Exception {
+    @Test public void serializeHeaders() throws Exception {
         ByteArrayOutputStream baos = new ByteArrayOutputStream();
-        Map<String, String> empty = new HashMap<>();
-        DiskBasedCache.writeStringStringMap(empty, baos);
-        DiskBasedCache.writeStringStringMap(null, baos);
-        Map<String, String> twoThings = new HashMap<>();
-        twoThings.put("first", "thing");
-        twoThings.put("second", "item");
-        DiskBasedCache.writeStringStringMap(twoThings, baos);
-        Map<String, String> emptyKey = new HashMap<>();
-        emptyKey.put("", "value");
-        DiskBasedCache.writeStringStringMap(emptyKey, baos);
-        Map<String, String> emptyValue = new HashMap<>();
-        emptyValue.put("key", "");
-        DiskBasedCache.writeStringStringMap(emptyValue, baos);
+        List<Header> empty = new ArrayList<>();
+        DiskBasedCache.writeHeaderList(empty, baos);
+        DiskBasedCache.writeHeaderList(null, baos);
+        List<Header> twoThings = new ArrayList<>();
+        twoThings.add(new Header("first", "thing"));
+        twoThings.add(new Header("second", "item"));
+        DiskBasedCache.writeHeaderList(twoThings, baos);
+        List<Header> emptyKey = new ArrayList<>();
+        emptyKey.add(new Header("", "value"));
+        DiskBasedCache.writeHeaderList(emptyKey, baos);
+        List<Header> emptyValue = new ArrayList<>();
+        emptyValue.add(new Header("key", ""));
+        DiskBasedCache.writeHeaderList(emptyValue, baos);
+        List<Header> sameKeys = new ArrayList<>();
+        sameKeys.add(new Header("key", "value"));
+        sameKeys.add(new Header("key", "value2"));
+        DiskBasedCache.writeHeaderList(sameKeys, baos);
         CountingInputStream cis =
                 new CountingInputStream(new ByteArrayInputStream(baos.toByteArray()), baos.size());
-        assertEquals(DiskBasedCache.readStringStringMap(cis), empty);
-        assertEquals(DiskBasedCache.readStringStringMap(cis), empty); // null reads back empty
-        assertEquals(DiskBasedCache.readStringStringMap(cis), twoThings);
-        assertEquals(DiskBasedCache.readStringStringMap(cis), emptyKey);
-        assertEquals(DiskBasedCache.readStringStringMap(cis), emptyValue);
+        assertEquals(DiskBasedCache.readHeaderList(cis), empty);
+        assertEquals(DiskBasedCache.readHeaderList(cis), empty); // null reads back empty
+        assertEquals(DiskBasedCache.readHeaderList(cis), twoThings);
+        assertEquals(DiskBasedCache.readHeaderList(cis), emptyKey);
+        assertEquals(DiskBasedCache.readHeaderList(cis), emptyValue);
+        assertEquals(DiskBasedCache.readHeaderList(cis), sameKeys);
     }
 
     @Test
diff --git a/src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java b/src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java
index fd8cf51..9ccac05 100644
--- a/src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java
+++ b/src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java
@@ -17,10 +17,9 @@
 package com.android.volley.toolbox;
 
 import com.android.volley.Cache;
+import com.android.volley.Header;
 import com.android.volley.NetworkResponse;
 
-import org.apache.http.Header;
-import org.apache.http.message.BasicHeader;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -28,8 +27,10 @@
 
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
+import java.util.ArrayList;
 import java.util.Date;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 
@@ -269,24 +270,23 @@
     }
 
     @Test public void parseCaseInsensitive() {
-
         long now = System.currentTimeMillis();
 
-        Header[] headersArray = new Header[5];
-        headersArray[0] = new BasicHeader("eTAG", "Yow!");
-        headersArray[1] = new BasicHeader("DATE", rfc1123Date(now));
-        headersArray[2] = new BasicHeader("expires", rfc1123Date(now + ONE_HOUR_MILLIS));
-        headersArray[3] = new BasicHeader("cache-control", "public, max-age=86400");
-        headersArray[4] = new BasicHeader("content-type", "text/plain");
+        List<Header> headers = new ArrayList<>();
+        headers.add(new Header("eTAG", "Yow!"));
+        headers.add(new Header("DATE", rfc1123Date(now)));
+        headers.add(new Header("expires", rfc1123Date(now + ONE_HOUR_MILLIS)));
+        headers.add(new Header("cache-control", "public, max-age=86400"));
+        headers.add(new Header("content-type", "text/plain"));
 
-        Map<String, String> headers = BasicNetwork.convertHeaders(headersArray);
-        NetworkResponse response = new NetworkResponse(0, null, headers, false);
+        NetworkResponse response = new NetworkResponse(0, null, false, 0, headers);
         Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response);
 
         assertNotNull(entry);
         assertEquals("Yow!", entry.etag);
         assertEqualsWithin(now + ONE_DAY_MILLIS, entry.ttl, ONE_MINUTE_MILLIS);
         assertEquals(entry.softTtl, entry.ttl);
-        assertEquals("ISO-8859-1", HttpHeaderParser.parseCharset(headers));
+        assertEquals("ISO-8859-1",
+                HttpHeaderParser.parseCharset(HttpHeaderParser.toHeaderMap(headers)));
     }
 }
diff --git a/src/test/java/com/android/volley/toolbox/HurlStackTest.java b/src/test/java/com/android/volley/toolbox/HurlStackTest.java
index 42aeea8..c8dd6f1 100644
--- a/src/test/java/com/android/volley/toolbox/HurlStackTest.java
+++ b/src/test/java/com/android/volley/toolbox/HurlStackTest.java
@@ -16,6 +16,7 @@
 
 package com.android.volley.toolbox;
 
+import com.android.volley.Header;
 import com.android.volley.Request.Method;
 import com.android.volley.mock.MockHttpURLConnection;
 import com.android.volley.mock.TestRequest;
@@ -25,6 +26,12 @@
 import org.junit.runner.RunWith;
 import org.robolectric.RobolectricTestRunner;
 
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
 import static org.junit.Assert.*;
 
 @RunWith(RobolectricTestRunner.class)
@@ -152,4 +159,20 @@
         assertEquals("PATCH", mMockConnection.getRequestMethod());
         assertTrue(mMockConnection.getDoOutput());
     }
+
+    @Test public void convertHeaders() {
+        Map<String, List<String>> headers = new HashMap<>();
+        headers.put(null, Collections.singletonList("Ignored"));
+        headers.put("HeaderA", Collections.singletonList("ValueA"));
+        List<String> values = new ArrayList<>();
+        values.add("ValueB_1");
+        values.add("ValueB_2");
+        headers.put("HeaderB", values);
+        List<Header> result = HurlStack.convertHeaders(headers);
+        List<Header> expected = new ArrayList<>();
+        expected.add(new Header("HeaderA", "ValueA"));
+        expected.add(new Header("HeaderB", "ValueB_1"));
+        expected.add(new Header("HeaderB", "ValueB_2"));
+        assertEquals(expected, result);
+    }
 }