http-client-branch: Make HttpHeaders final http-client-branch
authorchegar
Fri, 25 May 2018 16:13:11 +0100
branchhttp-client-branch
changeset 56619 57f17e890a40
parent 56618 e4022357f852
child 56621 a85c163fc41c
http-client-branch: Make HttpHeaders final
src/java.net.http/share/classes/java/net/http/HttpHeaders.java
src/java.net.http/share/classes/java/net/http/HttpRequest.java
src/java.net.http/share/classes/jdk/internal/net/http/AuthenticationFilter.java
src/java.net.http/share/classes/jdk/internal/net/http/CookieFilter.java
src/java.net.http/share/classes/jdk/internal/net/http/Http1HeaderParser.java
src/java.net.http/share/classes/jdk/internal/net/http/Http1Request.java
src/java.net.http/share/classes/jdk/internal/net/http/Http2Connection.java
src/java.net.http/share/classes/jdk/internal/net/http/HttpConnection.java
src/java.net.http/share/classes/jdk/internal/net/http/HttpRequestBuilderImpl.java
src/java.net.http/share/classes/jdk/internal/net/http/HttpRequestImpl.java
src/java.net.http/share/classes/jdk/internal/net/http/ImmutableHeaders.java
src/java.net.http/share/classes/jdk/internal/net/http/Response.java
src/java.net.http/share/classes/jdk/internal/net/http/Stream.java
src/java.net.http/share/classes/jdk/internal/net/http/common/HttpHeadersBuilder.java
src/java.net.http/share/classes/jdk/internal/net/http/common/HttpHeadersImpl.java
src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java
test/jdk/java/net/httpclient/BodyProcessorInputStreamTest.java
test/jdk/java/net/httpclient/DependentPromiseActionsTest.java
test/jdk/java/net/httpclient/DigestEchoServer.java
test/jdk/java/net/httpclient/HeadersTest.java
test/jdk/java/net/httpclient/HttpClientBuilderTest.java
test/jdk/java/net/httpclient/HttpHeadersOf.java
test/jdk/java/net/httpclient/HttpInputStreamTest.java
test/jdk/java/net/httpclient/HttpServerAdapters.java
test/jdk/java/net/httpclient/MethodsTest.java
test/jdk/java/net/httpclient/RedirectMethodChange.java
test/jdk/java/net/httpclient/ThrowingPushPromises.java
test/jdk/java/net/httpclient/http2/BadHeadersTest.java
test/jdk/java/net/httpclient/http2/ContinuationFrameTest.java
test/jdk/java/net/httpclient/http2/ImplicitPushCancel.java
test/jdk/java/net/httpclient/http2/ServerPush.java
test/jdk/java/net/httpclient/http2/ServerPushWithDiffTypes.java
test/jdk/java/net/httpclient/http2/server/Http2EchoHandler.java
test/jdk/java/net/httpclient/http2/server/Http2RedirectHandler.java
test/jdk/java/net/httpclient/http2/server/Http2TestExchange.java
test/jdk/java/net/httpclient/http2/server/Http2TestExchangeImpl.java
test/jdk/java/net/httpclient/http2/server/Http2TestExchangeSupplier.java
test/jdk/java/net/httpclient/http2/server/Http2TestServerConnection.java
test/jdk/java/net/httpclient/http2/server/OutgoingPushPromise.java
test/jdk/java/net/httpclient/http2/server/PushHandler.java
test/jdk/java/net/httpclient/offline/FixedHttpHeaders.java
test/jdk/java/net/httpclient/offline/OfflineTesting.java
test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/AuthenticationFilterTest.java
--- a/src/java.net.http/share/classes/java/net/http/HttpHeaders.java	Mon May 28 17:22:37 2018 +0100
+++ b/src/java.net.http/share/classes/java/net/http/HttpHeaders.java	Fri May 25 16:13:11 2018 +0100
@@ -25,62 +25,68 @@
 
 package java.net.http;
 
+import java.util.ArrayList;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Optional;
 import java.util.OptionalLong;
-import static java.util.Collections.emptyList;
-import static java.util.Collections.unmodifiableList;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.function.BiPredicate;
+import static java.lang.String.CASE_INSENSITIVE_ORDER;
+import static java.util.Collections.unmodifiableMap;
 import static java.util.Objects.requireNonNull;
 
 /**
  * A read-only view of a set of HTTP headers.
  *
- * <p> An {@code HttpHeaders} is not created directly, but rather returned from
- * an {@link HttpResponse HttpResponse}. Specific HTTP headers can be set for
- * {@linkplain HttpRequest requests} through the one of the request builder's
- * {@link HttpRequest.Builder#header(String, String) headers} methods.
+ * <p> An {@code HttpHeaders} is not typically created directly, but rather
+ * returned from an {@link HttpRequest#headers() HttpRequest} or an
+ * {@link HttpResponse#headers() HttpResponse}. Specific HTTP headers can be
+ * set for a {@linkplain HttpRequest request} through one of the request
+ * builder's {@link HttpRequest.Builder#header(String, String) headers} methods.
  *
  * <p> The methods of this class ( that accept a String header name ), and the
- * Map returned by the {@link #map() map} method, operate without regard to
- * case when retrieving the header value.
+ * {@code Map} returned by the {@link #map() map} method, operate without regard
+ * to case when retrieving the header value(s).
+ *
+ * <p> An HTTP header name may appear more than once in the HTTP protocol. As
+ * such, headers are represented as a name and a list of values. Each occurrence
+ * of a header value is added verbatim, to the appropriate header name list,
+ * without interpreting its value. In particular, {@code HttpHeaders} does not
+ * perform any splitting or joining of comma separated header value strings. The
+ * order of elements in a header value list is preserved when {@link
+ * HttpRequest.Builder#header(String, String) building} a request. For
+ * responses, the order of elements in a header value list is the order in which
+ * they were received. The {@code Map} returned by the {@code map} method,
+ * however, does not provide any guarantee with regard to the ordering of its
+ * entries.
  *
  * <p> {@code HttpHeaders} instances are immutable.
  *
  * @since 11
  */
-public abstract class HttpHeaders {
-
-    /**
-     * Creates an HttpHeaders.
-     */
-    protected HttpHeaders() {}
+public final class HttpHeaders {
 
     /**
-     * Returns an {@link Optional} containing the first value of the given named
-     * (and possibly multi-valued) header. If the header is not present, then
-     * the returned {@code Optional} is empty.
-     *
-     * @implSpec
-     * The default implementation invokes
-     * {@code allValues(name).stream().findFirst()}
+     * Returns an {@link Optional} containing the first header string value of
+     * the given named (and possibly multi-valued) header. If the header is not
+     * present, then the returned {@code Optional} is empty.
      *
      * @param name the header name
-     * @return an {@code Optional<String>} for the first named value
+     * @return an {@code Optional<String>} containing the first named header
+     *         string value, if present
      */
     public Optional<String> firstValue(String name) {
         return allValues(name).stream().findFirst();
     }
 
     /**
-     * Returns an {@link OptionalLong} containing the first value of the
-     * named header field. If the header is not present, then the Optional is
-     * empty. If the header is present but contains a value that does not parse
-     * as a {@code Long} value, then an exception is thrown.
-     *
-     * @implSpec
-     * The default implementation invokes
-     * {@code allValues(name).stream().mapToLong(Long::valueOf).findFirst()}
+     * Returns an {@link OptionalLong} containing the first header string value
+     * of the named header field. If the header is not present, then the
+     * Optional is empty. If the header is present but contains a value that
+     * does not parse as a {@code Long} value, then an exception is thrown.
      *
      * @param name the header name
      * @return  an {@code OptionalLong}
@@ -92,23 +98,19 @@
     }
 
     /**
-     * Returns an unmodifiable List of all of the values of the given named
-     * header. Always returns a List, which may be empty if the header is not
-     * present.
-     *
-     * @implSpec
-     * The default implementation invokes, among other things, the
-     * {@code map().get(name)} to retrieve the list of header values.
+     * Returns an unmodifiable List of all of the header string values of the
+     * given named header. Always returns a List, which may be empty if the
+     * header is not present.
      *
      * @param name the header name
-     * @return a List of String values
+     * @return a List of headers string values
      */
     public List<String> allValues(String name) {
         requireNonNull(name);
         List<String> values = map().get(name);
         // Making unmodifiable list out of empty in order to make a list which
         // throws UOE unconditionally
-        return values != null ? values : unmodifiableList(emptyList());
+        return values != null ? values : List.of();
     }
 
     /**
@@ -116,7 +118,9 @@
      *
      * @return the Map
      */
-    public abstract Map<String, List<String>> map();
+    public Map<String,List<String>> map() {
+        return headers;
+    }
 
     /**
      * Tests this HTTP headers instance for equality with the given object.
@@ -165,4 +169,84 @@
         sb.append(" }");
         return sb.toString();
     }
+
+    /**
+     * Returns an HTTP headers from the given map. The given map's key
+     * represents the header name, and its value the list of string header
+     * values for that header name.
+     *
+     * <p> An HTTP header name may appear more than once in the HTTP protocol.
+     * Such, <i>multi-valued</i>, headers must be represented by a single entry
+     * in the given map, whose entry value is a list that represents the
+     * multiple header string values. Leading and trailing whitespaces are
+     * removed from all string values retrieved from the given map and its lists
+     * before processing. Only headers that, after filtering, contain at least
+     * one, possibly empty string, value will be added to the HTTP headers.
+     *
+     * @apiNote The primary purpose of this method is for testing frameworks.
+     * Per-request headers can be set through one of the {@code HttpRequest}
+     * {@link HttpRequest.Builder#header(String, String) headers} methods.
+     *
+     * @param headerMap the map containing the header names and values
+     * @param filter a filter that can be used to inspect each
+     *               header-name-and-value pair in the given map to determine if
+     *               it should, or should not, be added to the to the HTTP
+     *               headers
+     * @return an HTTP headers instance containing the given headers
+     * @throws NullPointerException if any of: {@code headerMap}, a key or value
+     *        in the given map, or an entry in the map's value list, or
+     *        {@code filter}, is {@code null}
+     * @throws IllegalArgumentException if the given {@code headerMap} contains
+     *         any two keys that are equal ( without regard to case ); or if the
+     *         given map contains any key whose length, after trimming
+     *         whitespaces, is {@code 0}
+     */
+    public static HttpHeaders of(Map<String,List<String>> headerMap,
+                                 BiPredicate<String,String> filter) {
+        requireNonNull(headerMap);
+        requireNonNull(filter);
+        return headersOf(headerMap, filter);
+    }
+
+    // --
+
+    private static final HttpHeaders NO_HEADERS = new HttpHeaders(Map.of());
+
+    private final Map<String,List<String>> headers;
+
+    private HttpHeaders(Map<String,List<String>> headers) {
+        this.headers = headers;
+    }
+
+    // Returns a new HTTP headers after performing a structural copy and filtering.
+    private static HttpHeaders headersOf(Map<String,List<String>> map,
+                                         BiPredicate<String,String> filter) {
+        TreeMap<String,List<String>> other = new TreeMap<>(CASE_INSENSITIVE_ORDER);
+        TreeSet<String> notAdded = new TreeSet<>(CASE_INSENSITIVE_ORDER);
+        ArrayList<String> tempList = new ArrayList<>();
+        map.forEach((key, value) -> {
+            String headerName = requireNonNull(key).trim();
+            if (headerName.isEmpty()) {
+                throw new IllegalArgumentException("empty key");
+            }
+            List<String> headerValues = requireNonNull(value);
+            headerValues.forEach(headerValue -> {
+                headerValue = requireNonNull(headerValue).trim();
+                if (filter.test(headerName, headerValue)) {
+                    tempList.add(headerValue);
+                }
+            });
+
+            if (tempList.isEmpty()) {
+                if (other.containsKey(headerName)
+                        || notAdded.contains(headerName.toLowerCase(Locale.ROOT)))
+                    throw new IllegalArgumentException("duplicate key: " + headerName);
+                notAdded.add(headerName.toLowerCase(Locale.ROOT));
+            } else if (other.put(headerName, List.copyOf(tempList)) != null) {
+                throw new IllegalArgumentException("duplicate key: " + headerName);
+            }
+            tempList.clear();
+        });
+        return other.isEmpty() ? NO_HEADERS : new HttpHeaders(unmodifiableMap(other));
+    }
 }
--- a/src/java.net.http/share/classes/java/net/http/HttpRequest.java	Mon May 28 17:22:37 2018 +0100
+++ b/src/java.net.http/share/classes/java/net/http/HttpRequest.java	Fri May 25 16:13:11 2018 +0100
@@ -89,10 +89,12 @@
      * <p> Instances of {@code HttpRequest.Builder} are created by calling {@link
      * HttpRequest#newBuilder(URI)} or {@link HttpRequest#newBuilder()}.
      *
-     * <p> Each of the setter methods modifies the state of the builder
-     * and returns the same instance. The methods are not synchronized and
-     * should not be called from multiple threads without external
-     * synchronization. The {@link #build() build} method returns a new
+     * <p> The builder can be used to configure per-request state, like: the
+     * request URI, the request method ( default is GET unless explicitly set ),
+     * specific request headers, etc. Each of the setter methods modifies the
+     * state of the builder and returns the same instance. The methods are not
+     * synchronized and should not be called from multiple threads without
+     * external synchronization. The {@link #build() build} method returns a new
      * {@code HttpRequest} each time it is invoked. Once built an {@code
      * HttpRequest} is immutable, and can be sent multiple times.
      *
--- a/src/java.net.http/share/classes/jdk/internal/net/http/AuthenticationFilter.java	Mon May 28 17:22:37 2018 +0100
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/AuthenticationFilter.java	Fri May 25 16:13:11 2018 +0100
@@ -59,9 +59,9 @@
     static final int UNAUTHORIZED = 401;
     static final int PROXY_UNAUTHORIZED = 407;
 
-    private static final List<String> BASIC_DUMMY =
-            List.of("Basic " + Base64.getEncoder()
-                    .encodeToString("o:o".getBytes(ISO_8859_1)));
+    private static final String BASIC_DUMMY =
+            "Basic " + Base64.getEncoder()
+                    .encodeToString("o:o".getBytes(ISO_8859_1));
 
     // A public no-arg constructor is required by FilterFactory
     public AuthenticationFilter() {}
@@ -182,14 +182,12 @@
         String value = "Basic " + s;
         if (proxy) {
             if (r.isConnect()) {
-                if (!Utils.PROXY_TUNNEL_FILTER
-                        .test(hdrname, List.of(value))) {
+                if (!Utils.PROXY_TUNNEL_FILTER.test(hdrname, value)) {
                     Log.logError("{0} disabled", hdrname);
                     return;
                 }
             } else if (r.proxy() != null) {
-                if (!Utils.PROXY_FILTER
-                        .test(hdrname, List.of(value))) {
+                if (!Utils.PROXY_FILTER.test(hdrname, value)) {
                     Log.logError("{0} disabled", hdrname);
                     return;
                 }
--- a/src/java.net.http/share/classes/jdk/internal/net/http/CookieFilter.java	Mon May 28 17:22:37 2018 +0100
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/CookieFilter.java	Fri May 25 16:13:11 2018 +0100
@@ -31,7 +31,7 @@
 import java.util.Map;
 import java.util.Optional;
 import java.net.http.HttpHeaders;
-import jdk.internal.net.http.common.HttpHeadersImpl;
+import jdk.internal.net.http.common.HttpHeadersBuilder;
 import jdk.internal.net.http.common.Log;
 import jdk.internal.net.http.common.Utils;
 
@@ -50,7 +50,7 @@
             Map<String,List<String>> cookies = cookieHandler.get(r.uri(), userheaders);
 
             // add the returned cookies
-            HttpHeadersImpl systemHeaders = r.getSystemHeaders();
+            HttpHeadersBuilder systemHeadersBuilder = r.getSystemHeadersBuilder();
             if (cookies.isEmpty()) {
                 Log.logTrace("Request: no cookie to add for {0}", r.uri());
             } else {
@@ -65,7 +65,7 @@
                 if (values == null || values.isEmpty()) continue;
                 for (String val : values) {
                     if (Utils.isValidValue(val)) {
-                        systemHeaders.addHeader(hdrname, val);
+                        systemHeadersBuilder.addHeader(hdrname, val);
                     }
                 }
             }
--- a/src/java.net.http/share/classes/jdk/internal/net/http/Http1HeaderParser.java	Mon May 28 17:22:37 2018 +0100
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http1HeaderParser.java	Fri May 25 16:13:11 2018 +0100
@@ -35,6 +35,7 @@
 import java.net.http.HttpHeaders;
 import static java.lang.String.format;
 import static java.util.Objects.requireNonNull;
+import static jdk.internal.net.http.common.Utils.ACCEPT_ALL;
 
 class Http1HeaderParser {
 
@@ -194,7 +195,7 @@
         assert sb.length() == 0;
         char c = state == State.STATUS_LINE_END_LF ? LF : (char)input.get();
         if (c == LF) {
-            headers = ImmutableHeaders.of(privateMap);
+            headers = HttpHeaders.of(privateMap, ACCEPT_ALL);
             privateMap = null;
             state = State.FINISHED;  // no headers
         } else {
@@ -268,7 +269,7 @@
                 state = State.HEADER_FOUND_CR_LF_CR;
             } else {
                 state = State.FINISHED;
-                headers = ImmutableHeaders.of(privateMap);
+                headers = HttpHeaders.of(privateMap, ACCEPT_ALL);
                 privateMap = null;
             }
         } else if (c == SP || c == HT) {
@@ -293,7 +294,7 @@
         char c = (char)input.get();
         if (c == LF) {
             state = State.FINISHED;
-            headers = ImmutableHeaders.of(privateMap);
+            headers = HttpHeaders.of(privateMap, ACCEPT_ALL);
             privateMap = null;
         } else {
             throw protocolException("Unexpected \"%s\", after CR LF CR", c);
--- a/src/java.net.http/share/classes/jdk/internal/net/http/Http1Request.java	Mon May 28 17:22:37 2018 +0100
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http1Request.java	Fri May 25 16:13:11 2018 +0100
@@ -26,7 +26,6 @@
 package jdk.internal.net.http;
 
 import java.io.IOException;
-import java.lang.System.Logger.Level;
 import java.net.URI;
 import java.nio.ByteBuffer;
 import java.util.ArrayList;
@@ -39,7 +38,7 @@
 import java.net.http.HttpHeaders;
 import java.net.http.HttpRequest;
 import jdk.internal.net.http.Http1Exchange.Http1BodySubscriber;
-import jdk.internal.net.http.common.HttpHeadersImpl;
+import jdk.internal.net.http.common.HttpHeadersBuilder;
 import jdk.internal.net.http.common.Log;
 import jdk.internal.net.http.common.Logger;
 import jdk.internal.net.http.common.Utils;
@@ -52,7 +51,7 @@
 class Http1Request {
 
     private static final String COOKIE_HEADER = "Cookie";
-    private static final BiPredicate<String,List<String>> NOCOOKIES =
+    private static final BiPredicate<String,String> NOCOOKIES =
             (k,v) -> !COOKIE_HEADER.equalsIgnoreCase(k);
 
     private final HttpRequestImpl request;
@@ -60,7 +59,7 @@
     private final HttpConnection connection;
     private final HttpRequest.BodyPublisher requestPublisher;
     private final HttpHeaders userHeaders;
-    private final HttpHeadersImpl systemHeaders;
+    private final HttpHeadersBuilder systemHeadersBuilder;
     private volatile boolean streaming;
     private volatile long contentLength;
 
@@ -73,7 +72,7 @@
         this.connection = http1Exchange.connection();
         this.requestPublisher = request.requestPublisher;  // may be null
         this.userHeaders = request.getUserHeaders();
-        this.systemHeaders = request.getSystemHeaders();
+        this.systemHeadersBuilder = request.getSystemHeadersBuilder();
     }
 
     private void logHeaders(String completeHeaders) {
@@ -92,12 +91,13 @@
 
 
     private void collectHeaders0(StringBuilder sb) {
-        BiPredicate<String,List<String>> filter =
+        BiPredicate<String,String> filter =
                 connection.headerFilter(request);
 
         // Filter out 'Cookie:' headers, we will collect them at the end.
-        BiPredicate<String,List<String>> nocookies =
-                NOCOOKIES.and(filter);
+        BiPredicate<String,String> nocookies = NOCOOKIES.and(filter);
+
+        HttpHeaders systemHeaders = systemHeadersBuilder.build();
 
         // If we're sending this request through a tunnel,
         // then don't send any preemptive proxy-* headers that
@@ -112,8 +112,7 @@
 
         // Gather all 'Cookie:' headers and concatenate their
         // values in a single line.
-        collectCookies(sb, COOKIE_HEADER,
-                systemHeaders, userHeaders, filter);
+        collectCookies(sb, systemHeaders, userHeaders);
 
         // terminate headers
         sb.append('\r').append('\n');
@@ -142,20 +141,16 @@
     // any illegal character for header field values.
     //
     private void collectCookies(StringBuilder sb,
-                                String key,
                                 HttpHeaders system,
-                                HttpHeaders user,
-                                BiPredicate<String, List<String>> filter) {
-        List<String> systemList = system.allValues(key);
-        if (systemList != null && !filter.test(key, systemList)) systemList = null;
-        List<String> userList = user.allValues(key);
-        if (userList != null && !filter.test(key, userList)) userList = null;
+                                HttpHeaders user) {
+        List<String> systemList = system.allValues(COOKIE_HEADER);
+        List<String> userList = user.allValues(COOKIE_HEADER);
         boolean found = false;
         if (systemList != null) {
             for (String cookie : systemList) {
                 if (!found) {
                     found = true;
-                    sb.append(key).append(':').append(' ');
+                    sb.append(COOKIE_HEADER).append(':').append(' ');
                 } else {
                     sb.append(';').append(' ');
                 }
@@ -166,7 +161,7 @@
             for (String cookie : userList) {
                 if (!found) {
                     found = true;
-                    sb.append(key).append(':').append(' ');
+                    sb.append(COOKIE_HEADER).append(':').append(' ');
                 } else {
                     sb.append(';').append(' ');
                 }
@@ -176,13 +171,15 @@
         if (found) sb.append('\r').append('\n');
     }
 
-    private void collectHeaders1(StringBuilder sb, HttpHeaders headers,
-                                 BiPredicate<String, List<String>> filter) {
+    private void collectHeaders1(StringBuilder sb,
+                                 HttpHeaders headers,
+                                 BiPredicate<String,String> filter) {
         for (Map.Entry<String,List<String>> entry : headers.map().entrySet()) {
             String key = entry.getKey();
             List<String> values = entry.getValue();
-            if (!filter.test(key, values)) continue;
             for (String value : values) {
+                if (!filter.test(key, value))
+                    continue;
                 sb.append(key).append(':').append(' ')
                         .append(value)
                         .append('\r').append('\n');
@@ -279,7 +276,7 @@
 
         URI uri = request.uri();
         if (uri != null) {
-            systemHeaders.setHeader("Host", hostString());
+            systemHeadersBuilder.setHeader("Host", hostString());
         }
         if (requestPublisher == null) {
             // Not a user request, or maybe a method, e.g. GET, with no body.
@@ -289,13 +286,13 @@
         }
 
         if (contentLength == 0) {
-            systemHeaders.setHeader("Content-Length", "0");
+            systemHeadersBuilder.setHeader("Content-Length", "0");
         } else if (contentLength > 0) {
-            systemHeaders.setHeader("Content-Length", Long.toString(contentLength));
+            systemHeadersBuilder.setHeader("Content-Length", Long.toString(contentLength));
             streaming = false;
         } else {
             streaming = true;
-            systemHeaders.setHeader("Transfer-encoding", "chunked");
+            systemHeadersBuilder.setHeader("Transfer-encoding", "chunked");
         }
         collectHeaders0(sb);
         String hs = sb.toString();
--- a/src/java.net.http/share/classes/jdk/internal/net/http/Http2Connection.java	Mon May 28 17:22:37 2018 +0100
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http2Connection.java	Fri May 25 16:13:11 2018 +0100
@@ -28,7 +28,6 @@
 import java.io.EOFException;
 import java.io.IOException;
 import java.io.UncheckedIOException;
-import java.lang.System.Logger.Level;
 import java.net.InetSocketAddress;
 import java.net.URI;
 import java.nio.ByteBuffer;
@@ -54,7 +53,7 @@
 import jdk.internal.net.http.HttpConnection.HttpPublisher;
 import jdk.internal.net.http.common.FlowTube;
 import jdk.internal.net.http.common.FlowTube.TubeSubscriber;
-import jdk.internal.net.http.common.HttpHeadersImpl;
+import jdk.internal.net.http.common.HttpHeadersBuilder;
 import jdk.internal.net.http.common.Log;
 import jdk.internal.net.http.common.Logger;
 import jdk.internal.net.http.common.MinimalFuture;
@@ -82,7 +81,6 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static jdk.internal.net.http.frame.SettingsFrame.*;
 
-
 /**
  * An Http2Connection. Encapsulates the socket(channel) and any SSLEngine used
  * over it. Contains an HttpConnection which hides the SocketChannel SSL stuff.
@@ -792,7 +790,7 @@
             nextPushStream += 2;
         }
 
-        HttpHeadersImpl headers = decoder.headers();
+        HttpHeaders headers = decoder.headers();
         HttpRequestImpl pushReq = HttpRequestImpl.createPushRequest(parentReq, headers);
         Exchange<T> pushExch = new Exchange<>(pushReq, parent.exchange.multi);
         Stream.PushedStream<T> pushStream = createPushStream(parent, pushExch);
@@ -1303,10 +1301,10 @@
 
     static class HeaderDecoder extends ValidatingHeadersConsumer {
 
-        HttpHeadersImpl headers;
+        HttpHeadersBuilder headersBuilder;
 
         HeaderDecoder() {
-            this.headers = new HttpHeadersImpl();
+            this.headersBuilder = new HttpHeadersBuilder();
         }
 
         @Override
@@ -1314,11 +1312,11 @@
             String n = name.toString();
             String v = value.toString();
             super.onDecoded(n, v);
-            headers.addHeader(n, v);
+            headersBuilder.addHeader(n, v);
         }
 
-        HttpHeadersImpl headers() {
-            return headers;
+        HttpHeaders headers() {
+            return headersBuilder.build();
         }
     }
 
--- a/src/java.net.http/share/classes/jdk/internal/net/http/HttpConnection.java	Mon May 28 17:22:37 2018 +0100
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/HttpConnection.java	Fri May 25 16:13:11 2018 +0100
@@ -27,7 +27,6 @@
 
 import java.io.Closeable;
 import java.io.IOException;
-import java.lang.System.Logger.Level;
 import java.net.InetSocketAddress;
 import java.nio.ByteBuffer;
 import java.nio.channels.SocketChannel;
@@ -250,7 +249,7 @@
      * @param request
      * @return
      */
-    BiPredicate<String,List<String>> headerFilter(HttpRequestImpl request) {
+    BiPredicate<String,String> headerFilter(HttpRequestImpl request) {
         if (isTunnel()) {
             // talking to a server through a proxy tunnel
             // don't send proxy-* headers to a plain server
@@ -280,12 +279,12 @@
     // start with "proxy-"
     private static HttpHeaders proxyTunnelHeaders(HttpRequestImpl request) {
         Map<String, List<String>> combined = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
-        combined.putAll(request.getSystemHeaders().map());
+        combined.putAll(request.getSystemHeadersBuilder().map());
         combined.putAll(request.headers().map()); // let user override system
 
         // keep only proxy-* - and also strip authorization headers
         // for disabled schemes
-        return ImmutableHeaders.of(combined, Utils.PROXY_TUNNEL_FILTER);
+        return HttpHeaders.of(combined, Utils.PROXY_TUNNEL_FILTER);
     }
 
     /* Returns either a plain HTTP connection or a plain tunnelling connection
--- a/src/java.net.http/share/classes/jdk/internal/net/http/HttpRequestBuilderImpl.java	Mon May 28 17:22:37 2018 +0100
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/HttpRequestBuilderImpl.java	Fri May 25 16:13:11 2018 +0100
@@ -32,9 +32,9 @@
 import java.net.http.HttpClient;
 import java.net.http.HttpRequest;
 import java.net.http.HttpRequest.BodyPublisher;
-import jdk.internal.net.http.common.HttpHeadersImpl;
+
+import jdk.internal.net.http.common.HttpHeadersBuilder;
 import jdk.internal.net.http.common.Utils;
-import static java.lang.String.format;
 import static java.util.Objects.requireNonNull;
 import static jdk.internal.net.http.common.Utils.isValidName;
 import static jdk.internal.net.http.common.Utils.isValidValue;
@@ -42,7 +42,7 @@
 
 public class HttpRequestBuilderImpl implements HttpRequest.Builder {
 
-    private HttpHeadersImpl userHeaders;
+    private HttpHeadersBuilder headersBuilder;
     private URI uri;
     private String method;
     private boolean expectContinue;
@@ -54,13 +54,13 @@
         requireNonNull(uri, "uri must be non-null");
         checkURI(uri);
         this.uri = uri;
-        this.userHeaders = new HttpHeadersImpl();
+        this.headersBuilder = new HttpHeadersBuilder();
         this.method = "GET"; // default, as per spec
         this.version = Optional.empty();
     }
 
     public HttpRequestBuilderImpl() {
-        this.userHeaders = new HttpHeadersImpl();
+        this.headersBuilder = new HttpHeadersBuilder();
         this.method = "GET"; // default, as per spec
         this.version = Optional.empty();
     }
@@ -90,7 +90,7 @@
     public HttpRequestBuilderImpl copy() {
         HttpRequestBuilderImpl b = new HttpRequestBuilderImpl();
         b.uri = this.uri;
-        b.userHeaders = this.userHeaders.deepCopy();
+        b.headersBuilder = this.headersBuilder.structuralCopy();
         b.method = this.method;
         b.expectContinue = this.expectContinue;
         b.bodyPublisher = bodyPublisher;
@@ -106,7 +106,7 @@
         if (!isValidName(name)) {
             throw newIAE("invalid header name: \"%s\"", name);
         }
-        if (!Utils.ALLOWED_HEADERS.test(name)) {
+        if (!Utils.ALLOWED_HEADERS.test(name, null)) {
             throw newIAE("restricted header name: \"%s\"", name);
         }
         if (!isValidValue(value)) {
@@ -117,14 +117,14 @@
     @Override
     public HttpRequestBuilderImpl setHeader(String name, String value) {
         checkNameAndValue(name, value);
-        userHeaders.setHeader(name, value);
+        headersBuilder.setHeader(name, value);
         return this;
     }
 
     @Override
     public HttpRequestBuilderImpl header(String name, String value) {
         checkNameAndValue(name, value);
-        userHeaders.addHeader(name, value);
+        headersBuilder.addHeader(name, value);
         return this;
     }
 
@@ -155,7 +155,7 @@
         return this;
     }
 
-    HttpHeadersImpl headers() {  return userHeaders; }
+    HttpHeadersBuilder headersBuilder() {  return headersBuilder; }
 
     URI uri() { return uri; }
 
--- a/src/java.net.http/share/classes/jdk/internal/net/http/HttpRequestImpl.java	Mon May 28 17:22:37 2018 +0100
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/HttpRequestImpl.java	Fri May 25 16:13:11 2018 +0100
@@ -41,7 +41,7 @@
 import java.net.http.HttpClient;
 import java.net.http.HttpHeaders;
 import java.net.http.HttpRequest;
-import jdk.internal.net.http.common.HttpHeadersImpl;
+import jdk.internal.net.http.common.HttpHeadersBuilder;
 import jdk.internal.net.http.common.Utils;
 import jdk.internal.net.http.websocket.WebSocketRequest;
 
@@ -50,7 +50,7 @@
 class HttpRequestImpl extends HttpRequest implements WebSocketRequest {
 
     private final HttpHeaders userHeaders;
-    private final HttpHeadersImpl systemHeaders;
+    private final HttpHeadersBuilder systemHeadersBuilder;
     private final URI uri;
     private volatile Proxy proxy; // ensure safe publishing
     private final InetSocketAddress authority; // only used when URI not specified
@@ -78,8 +78,8 @@
     public HttpRequestImpl(HttpRequestBuilderImpl builder) {
         String method = builder.method();
         this.method = method == null ? "GET" : method;
-        this.userHeaders = ImmutableHeaders.of(builder.headers().map(), ALLOWED_HEADERS);
-        this.systemHeaders = new HttpHeadersImpl();
+        this.userHeaders = HttpHeaders.of(builder.headersBuilder().map(), ALLOWED_HEADERS);
+        this.systemHeadersBuilder = new HttpHeadersBuilder();
         this.uri = builder.uri();
         assert uri != null;
         this.proxy = null;
@@ -106,21 +106,21 @@
                 "uri must be non null");
         Duration timeout = request.timeout().orElse(null);
         this.method = method == null ? "GET" : method;
-        this.userHeaders = ImmutableHeaders.validate(request.headers());
+        this.userHeaders = HttpHeaders.of(request.headers().map(), Utils.VALIDATE_USER_HEADER);
         if (request instanceof HttpRequestImpl) {
             // all cases exception WebSocket should have a new system headers
             this.isWebSocket = ((HttpRequestImpl) request).isWebSocket;
             if (isWebSocket) {
-                this.systemHeaders = ((HttpRequestImpl) request).systemHeaders;
+                this.systemHeadersBuilder = ((HttpRequestImpl)request).systemHeadersBuilder;
             } else {
-                this.systemHeaders = new HttpHeadersImpl();
+                this.systemHeadersBuilder = new HttpHeadersBuilder();
             }
         } else {
             HttpRequestBuilderImpl.checkURI(requestURI);
             checkTimeout(timeout);
-            this.systemHeaders = new HttpHeadersImpl();
+            this.systemHeadersBuilder = new HttpHeadersBuilder();
         }
-        this.systemHeaders.setHeader("User-Agent", USER_AGENT);
+        this.systemHeadersBuilder.setHeader("User-Agent", USER_AGENT);
         this.uri = requestURI;
         if (isWebSocket) {
             // WebSocket determines and sets the proxy itself
@@ -169,8 +169,8 @@
         this.method = method == null? "GET" : method;
         this.userHeaders = other.userHeaders;
         this.isWebSocket = other.isWebSocket;
-        this.systemHeaders = new HttpHeadersImpl();
-        this.systemHeaders.setHeader("User-Agent", USER_AGENT);
+        this.systemHeadersBuilder = new HttpHeadersBuilder();
+        this.systemHeadersBuilder.setHeader("User-Agent", USER_AGENT);
         this.uri = uri;
         this.proxy = other.proxy;
         this.expectContinue = other.expectContinue;
@@ -189,8 +189,8 @@
         // to the connection pool (we might need to revisit this constructor later)
         assert "CONNECT".equalsIgnoreCase(method);
         this.method = method;
-        this.systemHeaders = new HttpHeadersImpl();
-        this.userHeaders = ImmutableHeaders.of(headers);
+        this.systemHeadersBuilder = new HttpHeadersBuilder();
+        this.userHeaders = headers;
         this.uri = URI.create("socket://" + authority.getHostString() + ":"
                               + Integer.toString(authority.getPort()) + "/");
         this.proxy = null;
@@ -218,14 +218,14 @@
      * parent.
      */
     static HttpRequestImpl createPushRequest(HttpRequestImpl parent,
-                                             HttpHeadersImpl headers)
+                                             HttpHeaders headers)
         throws IOException
     {
         return new HttpRequestImpl(parent, headers);
     }
 
     // only used for push requests
-    private HttpRequestImpl(HttpRequestImpl parent, HttpHeadersImpl headers)
+    private HttpRequestImpl(HttpRequestImpl parent, HttpHeaders headers)
         throws IOException
     {
         this.method = headers.firstValue(":method")
@@ -240,8 +240,8 @@
         sb.append(scheme).append("://").append(authority).append(path);
         this.uri = URI.create(sb.toString());
         this.proxy = null;
-        this.userHeaders = ImmutableHeaders.of(headers.map(), ALLOWED_HEADERS);
-        this.systemHeaders = parent.systemHeaders;
+        this.userHeaders = HttpHeaders.of(headers.map(), ALLOWED_HEADERS);
+        this.systemHeadersBuilder = parent.systemHeadersBuilder;
         this.expectContinue = parent.expectContinue;
         this.secure = parent.secure;
         this.requestPublisher = parent.requestPublisher;
@@ -264,9 +264,9 @@
     InetSocketAddress authority() { return authority; }
 
     void setH2Upgrade(Http2ClientImpl h2client) {
-        systemHeaders.setHeader("Connection", "Upgrade, HTTP2-Settings");
-        systemHeaders.setHeader("Upgrade", "h2c");
-        systemHeaders.setHeader("HTTP2-Settings", h2client.getSettingsString());
+        systemHeadersBuilder.setHeader("Connection", "Upgrade, HTTP2-Settings");
+        systemHeadersBuilder.setHeader("Upgrade", "h2c");
+        systemHeadersBuilder.setHeader("HTTP2-Settings", h2client.getSettingsString());
     }
 
     @Override
@@ -332,18 +332,18 @@
 
     HttpHeaders getUserHeaders() { return userHeaders; }
 
-    HttpHeadersImpl getSystemHeaders() { return systemHeaders; }
+    HttpHeadersBuilder getSystemHeadersBuilder() { return systemHeadersBuilder; }
 
     @Override
     public Optional<HttpClient.Version> version() { return version; }
 
     void addSystemHeader(String name, String value) {
-        systemHeaders.addHeader(name, value);
+        systemHeadersBuilder.addHeader(name, value);
     }
 
     @Override
     public void setSystemHeader(String name, String value) {
-        systemHeaders.setHeader(name, value);
+        systemHeadersBuilder.setHeader(name, value);
     }
 
     InetSocketAddress getAddress() {
--- a/src/java.net.http/share/classes/jdk/internal/net/http/ImmutableHeaders.java	Mon May 28 17:22:37 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,116 +0,0 @@
-/*
- * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation.  Oracle designates this
- * particular file as subject to the "Classpath" exception as provided
- * by Oracle in the LICENSE file that accompanied this code.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-
-package jdk.internal.net.http;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.TreeMap;
-import java.util.function.BiPredicate;
-import java.util.function.Predicate;
-import java.net.http.HttpHeaders;
-import jdk.internal.net.http.common.HttpHeadersImpl;
-import jdk.internal.net.http.common.Utils;
-import static java.util.Collections.emptyMap;
-import static java.util.Collections.unmodifiableList;
-import static java.util.Collections.unmodifiableMap;
-import static java.util.Objects.requireNonNull;
-
-final class ImmutableHeaders extends HttpHeaders {
-
-    private final Map<String, List<String>> map;
-
-    public static ImmutableHeaders empty() {
-        return of(emptyMap());
-    }
-
-    public static ImmutableHeaders of(Map<String, List<String>> src) {
-        return of(src, x -> true);
-    }
-
-    public static ImmutableHeaders of(HttpHeaders headers) {
-        return (headers instanceof ImmutableHeaders)
-                ? (ImmutableHeaders)headers
-                : of(headers.map());
-    }
-
-    static ImmutableHeaders validate(HttpHeaders headers) {
-        if (headers instanceof ImmutableHeaders) {
-            return of(headers);
-        }
-        if (headers instanceof HttpHeadersImpl) {
-            return of(headers);
-        }
-        Map<String, List<String>> map = headers.map();
-        return new ImmutableHeaders(map, Utils.VALIDATE_USER_HEADER);
-    }
-
-    public static ImmutableHeaders of(Map<String, List<String>> src,
-                                      Predicate<? super String> keyAllowed) {
-        requireNonNull(src, "src");
-        requireNonNull(keyAllowed, "keyAllowed");
-        return new ImmutableHeaders(src, headerAllowed(keyAllowed));
-    }
-
-    public static ImmutableHeaders of(Map<String, List<String>> src,
-                                      BiPredicate<? super String, ? super List<String>> headerAllowed) {
-        requireNonNull(src, "src");
-        requireNonNull(headerAllowed, "headerAllowed");
-        return new ImmutableHeaders(src, headerAllowed);
-    }
-
-    private ImmutableHeaders(Map<String, List<String>> src,
-                             BiPredicate<? super String, ? super List<String>> headerAllowed) {
-        Map<String, List<String>> m = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
-        src.entrySet().stream()
-                .forEach(e -> addIfAllowed(e, headerAllowed, m));
-        this.map = unmodifiableMap(m);
-    }
-
-    private static void addIfAllowed(Map.Entry<String, List<String>> e,
-                                     BiPredicate<? super String, ? super List<String>> headerAllowed,
-                                     Map<String, List<String>> map) {
-        String key = e.getKey();
-        List<String> values = unmodifiableValues(e.getValue());
-        if (headerAllowed.test(key, values)) {
-            map.put(key, values);
-        }
-    }
-
-    private static List<String> unmodifiableValues(List<String> values) {
-        return unmodifiableList(new ArrayList<>(Objects.requireNonNull(values)));
-    }
-
-    private static BiPredicate<String, List<String>> headerAllowed(Predicate<? super String> keyAllowed) {
-        return (n,v) -> keyAllowed.test(n);
-    }
-
-    @Override
-    public Map<String, List<String>> map() {
-        return map;
-    }
-}
--- a/src/java.net.http/share/classes/jdk/internal/net/http/Response.java	Mon May 28 17:22:37 2018 +0100
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/Response.java	Fri May 25 16:13:11 2018 +0100
@@ -64,7 +64,7 @@
              int statusCode,
              HttpClient.Version version,
              boolean isConnectResponse) {
-        this.headers = ImmutableHeaders.of(headers);
+        this.headers = headers;
         this.request = req;
         this.version = version;
         this.exchange = exchange;
--- a/src/java.net.http/share/classes/jdk/internal/net/http/Stream.java	Mon May 28 17:22:37 2018 +0100
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/Stream.java	Fri May 25 16:13:11 2018 +0100
@@ -115,8 +115,8 @@
     final Http2Connection connection;
     final HttpRequestImpl request;
     final HeadersConsumer rspHeadersConsumer;
-    final HttpHeadersImpl responseHeaders;
-    final HttpHeadersImpl requestPseudoHeaders;
+    final HttpHeadersBuilder responseHeadersBuilder;
+    final HttpHeaders requestPseudoHeaders;
     volatile HttpResponse.BodySubscriber<T> responseSubscriber;
     final HttpRequest.BodyPublisher requestPublisher;
     volatile RequestSubscriber requestSubscriber;
@@ -346,10 +346,9 @@
         this.windowController = windowController;
         this.request = e.request();
         this.requestPublisher = request.requestPublisher;  // may be null
-        responseHeaders = new HttpHeadersImpl();
-        rspHeadersConsumer = new HeadersConsumer();
-        this.requestPseudoHeaders = new HttpHeadersImpl();
-        // NEW
+        this.responseHeadersBuilder = new HttpHeadersBuilder();
+        this.rspHeadersConsumer = new HeadersConsumer();
+        this.requestPseudoHeaders = createPseudoHeaders(request);
         this.windowUpdater = new StreamWindowUpdateSender(connection);
     }
 
@@ -400,6 +399,7 @@
     }
 
     protected void handleResponse() throws IOException {
+        HttpHeaders responseHeaders = responseHeadersBuilder.build();
         responseCode = (int)responseHeaders
                 .firstValueAsLong(":status")
                 .orElseThrow(() -> new IOException("no statuscode in response"));
@@ -576,13 +576,12 @@
     }
 
     private OutgoingHeaders<Stream<T>> headerFrame(long contentLength) {
-        HttpHeadersImpl h = request.getSystemHeaders();
+        HttpHeadersBuilder h = request.getSystemHeadersBuilder();
         if (contentLength > 0) {
             h.setHeader("content-length", Long.toString(contentLength));
         }
-        setPseudoHeaderFields();
-        HttpHeaders sysh = filter(h);
-        HttpHeaders userh = filter(request.getUserHeaders());
+        HttpHeaders sysh = filterHeaders(h.build());
+        HttpHeaders userh = filterHeaders(request.getUserHeaders());
         OutgoingHeaders<Stream<T>> f = new OutgoingHeaders<>(sysh, userh, this);
         if (contentLength == 0) {
             f.setFlag(HeadersFrame.END_STREAM);
@@ -606,7 +605,7 @@
     // If nothing needs filtering then we can just use the
     // original headers.
     private boolean needsFiltering(HttpHeaders headers,
-                                   BiPredicate<String, List<String>> filter) {
+                                   BiPredicate<String, String> filter) {
         if (filter == Utils.PROXY_TUNNEL_FILTER || filter == Utils.PROXY_FILTER) {
             // we're either connecting or proxying
             // slight optimization: we only need to filter out
@@ -624,18 +623,17 @@
         }
     }
 
-    private HttpHeaders filter(HttpHeaders headers) {
+    private HttpHeaders filterHeaders(HttpHeaders headers) {
         HttpConnection conn = connection();
-        BiPredicate<String, List<String>> filter =
-                conn.headerFilter(request);
+        BiPredicate<String, String> filter = conn.headerFilter(request);
         if (needsFiltering(headers, filter)) {
-            return ImmutableHeaders.of(headers.map(), filter);
+            return HttpHeaders.of(headers.map(), filter);
         }
         return headers;
     }
 
-    private void setPseudoHeaderFields() {
-        HttpHeadersImpl hdrs = requestPseudoHeaders;
+    private static HttpHeaders createPseudoHeaders(HttpRequest request) {
+        HttpHeadersBuilder hdrs = new HttpHeadersBuilder();
         String method = request.method();
         hdrs.setHeader(":method", method);
         URI uri = request.uri();
@@ -656,9 +654,10 @@
             path += "?" + query;
         }
         hdrs.setHeader(":path", Utils.encode(path));
+        return hdrs.build();
     }
 
-    HttpHeadersImpl getRequestPseudoHeaders() {
+    HttpHeaders getRequestPseudoHeaders() {
         return requestPseudoHeaders;
     }
 
@@ -1242,6 +1241,7 @@
         // create and return the PushResponseImpl
         @Override
         protected void handleResponse() {
+            HttpHeaders responseHeaders = responseHeadersBuilder.build();
             responseCode = (int)responseHeaders
                 .firstValueAsLong(":status")
                 .orElse(-1);
@@ -1310,17 +1310,17 @@
 
         void reset() {
             super.reset();
-            responseHeaders.clear();
+            responseHeadersBuilder.clear();
         }
 
         @Override
         public void onDecoded(CharSequence name, CharSequence value)
-                throws UncheckedIOException
+            throws UncheckedIOException
         {
             String n = name.toString();
             String v = value.toString();
             super.onDecoded(n, v);
-            responseHeaders.addHeader(n, v);
+            responseHeadersBuilder.addHeader(n, v);
             if (Log.headers() && Log.trace()) {
                 Log.logTrace("RECEIVED HEADER (streamid={0}): {1}: {2}",
                              streamid, n, v);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/HttpHeadersBuilder.java	Fri May 25 16:13:11 2018 +0100
@@ -0,0 +1,85 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.internal.net.http.common;
+
+import java.net.http.HttpHeaders;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import static jdk.internal.net.http.common.Utils.ACCEPT_ALL;
+
+/** A mutable builder for collecting and building HTTP headers. */
+public class HttpHeadersBuilder {
+
+    private final TreeMap<String, List<String>> headersMap;
+
+    public HttpHeadersBuilder() {
+        headersMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+    }
+
+    public HttpHeadersBuilder structuralCopy() {
+        HttpHeadersBuilder builder = new HttpHeadersBuilder();
+        for (Map.Entry<String, List<String>> entry : headersMap.entrySet()) {
+            List<String> valuesCopy = new ArrayList<>(entry.getValue());
+            builder.headersMap.put(entry.getKey(), valuesCopy);
+        }
+        return builder;
+    }
+
+    public void addHeader(String name, String value) {
+        headersMap.computeIfAbsent(name, k -> new ArrayList<>(1))
+                  .add(value);
+    }
+
+    public void setHeader(String name, String value) {
+        // headers typically have one value
+        List<String> values = new ArrayList<>(1);
+        values.add(value);
+        headersMap.put(name, values);
+    }
+
+    public void clear() {
+        headersMap.clear();
+    }
+
+    public Map<String, List<String>> map() {
+        return headersMap;
+    }
+
+    public HttpHeaders build() {
+        return HttpHeaders.of(headersMap, ACCEPT_ALL);
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append(super.toString()).append(" { ");
+        sb.append(map());
+        sb.append(" }");
+        return sb.toString();
+    }
+}
--- a/src/java.net.http/share/classes/jdk/internal/net/http/common/HttpHeadersImpl.java	Mon May 28 17:22:37 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,89 +0,0 @@
-/*
- * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation.  Oracle designates this
- * particular file as subject to the "Classpath" exception as provided
- * by Oracle in the LICENSE file that accompanied this code.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-
-package jdk.internal.net.http.common;
-
-import java.net.http.HttpHeaders;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.TreeMap;
-
-/**
- * Implementation of HttpHeaders.
- *
- * The public HttpHeaders API provides a read-only view, while the
- * non-HttpHeaders members allow for implementation specific mutation, e.g.
- * during creation, etc.
- */
-public class HttpHeadersImpl extends HttpHeaders {
-
-    private final TreeMap<String, List<String>> headers;
-
-    public HttpHeadersImpl() {
-        headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
-    }
-
-    @Override
-    public Map<String, List<String>> map() {
-        return Collections.unmodifiableMap(headersMap());
-    }
-
-    // non-HttpHeaders private mutators
-
-    public HttpHeadersImpl deepCopy() {
-        HttpHeadersImpl h1 = newDeepCopy();
-        for (Map.Entry<String, List<String>> entry : headersMap().entrySet()) {
-            List<String> valuesCopy = new ArrayList<>(entry.getValue());
-            h1.headersMap().put(entry.getKey(), valuesCopy);
-        }
-        return h1;
-    }
-
-    public void addHeader(String name, String value) {
-        headersMap().computeIfAbsent(name, k -> new ArrayList<>(1))
-                    .add(value);
-    }
-
-    public void setHeader(String name, String value) {
-        // headers typically have one value
-        List<String> values = new ArrayList<>(1);
-        values.add(value);
-        headersMap().put(name, values);
-    }
-
-    public void clear() {
-        headersMap().clear();
-    }
-
-    protected HttpHeadersImpl newDeepCopy() {
-        return new HttpHeadersImpl();
-    }
-
-    protected Map<String, List<String>> headersMap() {
-        return headers;
-    }
-}
--- a/src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java	Mon May 28 17:22:37 2018 +0100
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java	Fri May 25 16:13:11 2018 +0100
@@ -119,6 +119,8 @@
             "jdk.httpclient.bufsize", DEFAULT_BUFSIZE
     );
 
+    public static final BiPredicate<String,String> ACCEPT_ALL = (x,y) -> true;
+
     private static final Set<String> DISALLOWED_HEADERS_SET;
 
     static {
@@ -131,24 +133,21 @@
         DISALLOWED_HEADERS_SET = Collections.unmodifiableSet(treeSet);
     }
 
-    public static final Predicate<String>
-            ALLOWED_HEADERS = header -> !DISALLOWED_HEADERS_SET.contains(header);
+    public static final BiPredicate<String, String>
+            ALLOWED_HEADERS = (header, unused) -> !DISALLOWED_HEADERS_SET.contains(header);
 
-    public static final BiPredicate<String, List<String>> VALIDATE_USER_HEADER =
-            (name, lv) -> {
-                requireNonNull(name, "header name");
-                requireNonNull(lv, "header values");
+    public static final BiPredicate<String, String> VALIDATE_USER_HEADER =
+            (name, value) -> {
+                assert name != null : "null header name";
+                assert value != null : "null header value";
                 if (!isValidName(name)) {
                     throw newIAE("invalid header name: \"%s\"", name);
                 }
-                if (!Utils.ALLOWED_HEADERS.test(name)) {
+                if (!Utils.ALLOWED_HEADERS.test(name, null)) {
                     throw newIAE("restricted header name: \"%s\"", name);
                 }
-                for (String value : lv) {
-                    requireNonNull(value, "header value");
-                    if (!isValidValue(value)) {
-                        throw newIAE("invalid header value for %s: \"%s\"", name, value);
-                    }
+                if (!isValidValue(value)) {
+                    throw newIAE("invalid header value for %s: \"%s\"", name, value);
                 }
                 return true;
             };
@@ -182,7 +181,7 @@
 
     private static final String WSPACES = " \t\r\n";
     private static final boolean isAllowedForProxy(String name,
-                                                   List<String> value,
+                                                   String value,
                                                    Set<String> disabledSchemes,
                                                    Predicate<String> allowedKeys) {
         if (!allowedKeys.test(name)) return false;
@@ -191,21 +190,19 @@
             if (value.isEmpty()) return false;
             for (String scheme : disabledSchemes) {
                 int slen = scheme.length();
-                for (String v : value) {
-                    int vlen = v.length();
-                    if (vlen == slen) {
-                        if (v.equalsIgnoreCase(scheme)) {
+                int vlen = value.length();
+                if (vlen == slen) {
+                    if (value.equalsIgnoreCase(scheme)) {
+                        return false;
+                    }
+                } else if (vlen > slen) {
+                    if (value.substring(0,slen).equalsIgnoreCase(scheme)) {
+                        int c = value.codePointAt(slen);
+                        if (WSPACES.indexOf(c) > -1
+                                || Character.isSpaceChar(c)
+                                || Character.isWhitespace(c)) {
                             return false;
                         }
-                    } else if (vlen > slen) {
-                        if (v.substring(0,slen).equalsIgnoreCase(scheme)) {
-                            int c = v.codePointAt(slen);
-                            if (WSPACES.indexOf(c) > -1
-                                    || Character.isSpaceChar(c)
-                                    || Character.isWhitespace(c)) {
-                                return false;
-                            }
-                        }
                     }
                 }
             }
@@ -213,13 +210,13 @@
         return true;
     }
 
-    public static final BiPredicate<String, List<String>> PROXY_TUNNEL_FILTER =
+    public static final BiPredicate<String, String> PROXY_TUNNEL_FILTER =
             (s,v) -> isAllowedForProxy(s, v, PROXY_AUTH_TUNNEL_DISABLED_SCHEMES,
                     IS_PROXY_HEADER);
-    public static final BiPredicate<String, List<String>> PROXY_FILTER =
+    public static final BiPredicate<String, String> PROXY_FILTER =
             (s,v) -> isAllowedForProxy(s, v, PROXY_AUTH_DISABLED_SCHEMES,
                     ALL_HEADERS);
-    public static final BiPredicate<String, List<String>> NO_PROXY_HEADERS_FILTER =
+    public static final BiPredicate<String, String> NO_PROXY_HEADERS_FILTER =
             (n,v) -> Utils.NO_PROXY_HEADER.test(n);
 
 
--- a/test/jdk/java/net/httpclient/BodyProcessorInputStreamTest.java	Mon May 28 17:22:37 2018 +0100
+++ b/test/jdk/java/net/httpclient/BodyProcessorInputStreamTest.java	Fri May 25 16:13:11 2018 +0100
@@ -41,7 +41,7 @@
  * @test
  * @bug 8187503
  * @summary An example on how to read a response body with InputStream.
- * @run main/manual -Dtest.debug=true BodyProcessorInputStreamTest
+ * @run main/othervm/manual -Dtest.debug=true BodyProcessorInputStreamTest
  * @author daniel fuchs
  */
 public class BodyProcessorInputStreamTest {
--- a/test/jdk/java/net/httpclient/DependentPromiseActionsTest.java	Mon May 28 17:22:37 2018 +0100
+++ b/test/jdk/java/net/httpclient/DependentPromiseActionsTest.java	Fri May 25 16:13:11 2018 +0100
@@ -40,7 +40,6 @@
 import java.io.BufferedReader;
 import java.io.InputStreamReader;
 import java.lang.StackWalker.StackFrame;
-import jdk.internal.net.http.common.HttpHeadersImpl;
 import jdk.testlibrary.SimpleSSLContext;
 import org.testng.annotations.AfterTest;
 import org.testng.annotations.AfterClass;
@@ -79,6 +78,7 @@
 import java.util.concurrent.Semaphore;
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.BiPredicate;
 import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.function.Supplier;
@@ -690,8 +690,13 @@
         https2TestServer.stop();
     }
 
-    private static void pushPromiseFor(HttpTestExchange t, URI requestURI, String pushPath, boolean fixed)
-            throws IOException
+    static final BiPredicate<String,String> ACCEPT_ALL = (x, y) -> true;
+
+    private static void pushPromiseFor(HttpTestExchange t,
+                                       URI requestURI,
+                                       String pushPath,
+                                       boolean fixed)
+        throws IOException
     {
         try {
             URI promise = new URI(requestURI.getScheme(),
@@ -700,9 +705,13 @@
             byte[] promiseBytes = promise.toASCIIString().getBytes(UTF_8);
             out.printf("TestServer: %s Pushing promise: %s%n", now(), promise);
             err.printf("TestServer: %s Pushing promise: %s%n", now(), promise);
-            HttpTestHeaders headers =  HttpTestHeaders.of(new HttpHeadersImpl());
+            HttpHeaders headers;
             if (fixed) {
-                headers.addHeader("Content-length", String.valueOf(promiseBytes.length));
+                String length = String.valueOf(promiseBytes.length);
+                headers = HttpHeaders.of(Map.of("Content-Length", List.of(length)),
+                                         ACCEPT_ALL);
+            } else {
+                headers = HttpHeaders.of(Map.of(), ACCEPT_ALL); // empty
             }
             t.serverPush(promise, headers, promiseBytes);
         } catch (URISyntaxException x) {
--- a/test/jdk/java/net/httpclient/DigestEchoServer.java	Mon May 28 17:22:37 2018 +0100
+++ b/test/jdk/java/net/httpclient/DigestEchoServer.java	Fri May 25 16:13:11 2018 +0100
@@ -170,7 +170,7 @@
         }
     }
 
-    private static String toString(HttpTestHeaders headers) {
+    private static String toString(HttpTestRequestHeaders headers) {
         return headers.entrySet().stream()
                 .map((e) -> e.getKey() + ": " + e.getValue())
                 .collect(Collectors.joining("\n"));
@@ -847,9 +847,9 @@
         }
 
         public static String computeDigest(boolean isRequest,
-                                            String reqMethod,
-                                            char[] password,
-                                            DigestResponse params)
+                                           String reqMethod,
+                                           char[] password,
+                                           DigestResponse params)
             throws NoSuchAlgorithmException
         {
 
@@ -970,11 +970,13 @@
 
         @Override
         protected void requestAuthentication(HttpTestExchange he)
-            throws IOException {
-            he.getResponseHeaders().addHeader(getAuthenticate(),
-                 "Basic realm=\"" + auth.getRealm() + "\"");
-            System.out.println(type + ": Requesting Basic Authentication "
-                 + he.getResponseHeaders().firstValue(getAuthenticate()));
+            throws IOException
+        {
+            String headerName = getAuthenticate();
+            String headerValue = "Basic realm=\"" + auth.getRealm() + "\"";
+            he.getResponseHeaders().addHeader(headerName, headerValue);
+            System.out.println(type + ": Requesting Basic Authentication, "
+                               + headerName + " : "+ headerValue);
         }
 
         @Override
@@ -1061,14 +1063,13 @@
             } else {
                 throw new InternalError(String.valueOf(v));
             }
-            he.getResponseHeaders().addHeader(getAuthenticate(),
-                 "Digest realm=\"" + auth.getRealm() + "\","
-                 + separator + "qop=\"auth\","
-                 + separator + "nonce=\"" + ns +"\"");
-            System.out.println(type + ": Requesting Digest Authentication "
-                 + he.getResponseHeaders()
-                    .firstValue(getAuthenticate())
-                    .orElse("null"));
+            String headerName = getAuthenticate();
+            String headerValue = "Digest realm=\"" + auth.getRealm() + "\","
+                    + separator + "qop=\"auth\","
+                    + separator + "nonce=\"" + ns +"\"";
+            he.getResponseHeaders().addHeader(headerName, headerValue);
+            System.out.println(type + ": Requesting Digest Authentication, "
+                               + headerName + " : " + headerValue);
         }
 
         @Override
--- a/test/jdk/java/net/httpclient/HeadersTest.java	Mon May 28 17:22:37 2018 +0100
+++ b/test/jdk/java/net/httpclient/HeadersTest.java	Fri May 25 16:13:11 2018 +0100
@@ -32,6 +32,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
+import java.util.function.BiPredicate;
 import static java.net.http.HttpClient.Builder.NO_PROXY;
 
 /**
@@ -41,20 +42,11 @@
  */
 public class HeadersTest {
 
+    static final BiPredicate<String,String> ACCEPT_ALL = (x, y) -> true;
+
     static final URI TEST_URI = URI.create("http://www.foo.com/");
     static final HttpClient client = HttpClient.newBuilder().proxy(NO_PROXY).build();
 
-    static final class HttpHeadersStub extends HttpHeaders {
-        Map<String, List<String>> map;
-        HttpHeadersStub(Map<String, List<String>> map) {
-            this.map = map;
-        }
-        @Override
-        public Map<String, List<String>> map() {
-            return map;
-        }
-    }
-
     static void bad(String name) throws Exception {
         HttpRequest.Builder builder = HttpRequest.newBuilder(TEST_URI);
         try {
@@ -85,7 +77,7 @@
                 }
                 @Override public HttpHeaders headers() {
                     Map<String, List<String>> map = Map.of(name, List.of("foo"));
-                    return new HttpHeadersStub(map);
+                    return HttpHeaders.of(map, ACCEPT_ALL);
                 }
             };
             client.send(req, HttpResponse.BodyHandlers.ofString());
@@ -127,7 +119,7 @@
                 }
                 @Override public HttpHeaders headers() {
                     Map<String, List<String>> map = Map.of("x-bad", List.of(value));
-                    return new HttpHeadersStub(map);
+                    return HttpHeaders.of(map, ACCEPT_ALL);
                 }
             };
             client.send(req, HttpResponse.BodyHandlers.ofString());
@@ -170,7 +162,7 @@
                 @Override public HttpHeaders headers() {
                     Map<String, List<String>> map = new HashMap<>();
                     map.put(null, List.of("foo"));
-                    return new HttpHeadersStub(map);
+                    return HttpHeaders.of(map, ACCEPT_ALL);
                 }
             };
             client.send(req, HttpResponse.BodyHandlers.ofString());
@@ -211,7 +203,7 @@
                 @Override public HttpHeaders headers() {
                     Map<String, List<String>> map = new HashMap<>();
                     map.put("x-bar", null);
-                    return new HttpHeadersStub(map);
+                    return HttpHeaders.of(map, ACCEPT_ALL);
                 }
             };
             client.send(req, HttpResponse.BodyHandlers.ofString());
@@ -243,11 +235,10 @@
                     List<String> values = new ArrayList<>();
                     values.add("foo");
                     values.add(null);
-                    return new HttpHeadersStub(Map.of("x-bar", values));
+                    return HttpHeaders.of(Map.of("x-bar", values), ACCEPT_ALL);
                 }
             };
-            client
-                    .send(req, HttpResponse.BodyHandlers.ofString());
+            client.send(req, HttpResponse.BodyHandlers.ofString());
             throw new RuntimeException("Expected NPE for null header value");
         } catch (NullPointerException expected) {
             System.out.println("Got expected NPE: " + expected);
@@ -276,7 +267,7 @@
                     return Optional.empty();
                 }
                 @Override public HttpHeaders headers() {
-                    return new HttpHeadersStub(null);
+                    return HttpHeaders.of(null, ACCEPT_ALL);
                 }
             };
             client.send(req, HttpResponse.BodyHandlers.ofString());
@@ -370,7 +361,7 @@
                 }
                 @Override public HttpHeaders headers() {
                     Map<String, List<String>> map = Map.of("x-good", List.of("foo"));
-                    return new HttpHeadersStub(map);
+                    return HttpHeaders.of(map, ACCEPT_ALL);
                 }
             };
             client.send(req, HttpResponse.BodyHandlers.ofString());
@@ -416,7 +407,7 @@
                 }
                 @Override public HttpHeaders headers() {
                     Map<String, List<String>> map = Map.of("x-good", List.of("foo"));
-                    return new HttpHeadersStub(map);
+                    return HttpHeaders.of(map, ACCEPT_ALL);
                 }
             };
             client.send(req, HttpResponse.BodyHandlers.ofString());
@@ -472,7 +463,7 @@
                     @Override
                     public HttpHeaders headers() {
                         Map<String, List<String>> map = Map.of("x-good", List.of("foo"));
-                        return new HttpHeadersStub(map);
+                        return HttpHeaders.of(map, ACCEPT_ALL);
                     }
                 };
                 client.send(req, HttpResponse.BodyHandlers.ofString());
@@ -526,7 +517,7 @@
                 @Override
                 public HttpHeaders headers() {
                     Map<String, List<String>> map = Map.of("x-good", List.of("foo"));
-                    return new HttpHeadersStub(map);
+                    return HttpHeaders.of(map, ACCEPT_ALL);
                 }
             };
             client.send(req, HttpResponse.BodyHandlers.ofString());
@@ -538,12 +529,10 @@
 
     public static void main(String[] args) throws Exception {
         bad("bad:header");
-        bad("Foo\n");
         good("X-Foo!");
         good("Bar~");
         good("x");
         bad(" ");
-        bad("Bar\r\n");
         good("Hello#world");
         good("Qwer#ert");
         badValue("blah\r\n blah");
--- a/test/jdk/java/net/httpclient/HttpClientBuilderTest.java	Mon May 28 17:22:37 2018 +0100
+++ b/test/jdk/java/net/httpclient/HttpClientBuilderTest.java	Fri May 25 16:13:11 2018 +0100
@@ -275,16 +275,7 @@
         @Override public boolean expectContinue() { return false; }
         @Override public URI uri() { return URI.create("http://foo.com/"); }
         @Override public Optional<Version> version() { return Optional.empty(); }
-        private final FixedHttpHeaders headers = new FixedHttpHeaders();
-        @Override public HttpHeaders headers() { return headers; }
-        public class FixedHttpHeaders extends HttpHeaders {
-            private final Map<String, List<String>> map =
-                    new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
-            @Override
-            public Map<String, List<String>> map() {
-                return map;
-            }
-        }
+        @Override public HttpHeaders headers() { return HttpHeaders.of(Map.of(), (x, y) -> true); }
     }
 
     // ---
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/HttpHeadersOf.java	Fri May 25 16:13:11 2018 +0100
@@ -0,0 +1,396 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @summary Tests for HttpHeaders.of factory method
+ * @run testng HttpHeadersOf
+ */
+
+import java.net.http.HttpHeaders;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiPredicate;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertThrows;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+public class HttpHeadersOf {
+
+    static final Class<NullPointerException> NPE = NullPointerException.class;
+    static final Class<NumberFormatException> NFE = NumberFormatException.class;
+    static final Class<UnsupportedOperationException> UOE = UnsupportedOperationException.class;
+
+    static final BiPredicate<String,String> ACCEPT_ALL =
+        new BiPredicate<>() {
+            @Override public boolean test(String name, String value) { return true; }
+            @Override public String toString() { return "ACCEPT_ALL"; }
+        };
+
+    static final BiPredicate<String,String> REJECT_ALL =
+        new BiPredicate<>() {
+            @Override public boolean test(String name, String value) { return false; }
+            @Override public String toString() { return "REJECT_ALL"; }
+        };
+
+    @DataProvider(name = "predicates")
+    public Object[][] predicates() {
+        return new Object[][] { { ACCEPT_ALL }, { REJECT_ALL } };
+    }
+
+    @Test(dataProvider = "predicates")
+    public void testNull(BiPredicate<String,String> filter) {
+        assertThrows(NPE, () -> HttpHeaders.of(null, null));
+        assertThrows(NPE, () -> HttpHeaders.of(null, filter));
+        assertThrows(NPE, () -> HttpHeaders.of(Map.of(), null));
+        assertThrows(NPE, () -> HttpHeaders.of(Map.of("name", List.of("value")), null));
+
+        // nulls in the Map
+        assertThrows(NPE, () -> HttpHeaders.of(Map.of(null, List.of("value)")), filter));
+        assertThrows(NPE, () -> HttpHeaders.of(Map.of("name", null), filter));
+        assertThrows(NPE, () -> HttpHeaders.of(Map.of("name", List.of(null)), filter));
+        assertThrows(NPE, () -> HttpHeaders.of(Map.of("name", List.of("aValue", null)), filter));
+        assertThrows(NPE, () -> HttpHeaders.of(Map.of("name", List.of(null, "aValue")), filter));
+    }
+
+
+    @DataProvider(name = "filterMaps")
+    public Object[][] filterMaps() {
+        List<Map<String, List<String>>> maps = List.of(
+                Map.of("A", List.of("B"),           "X", List.of("Y", "Z")),
+                Map.of("A", List.of("B", "C"),      "X", List.of("Y", "Z")),
+                Map.of("A", List.of("B", "C", "D"), "X", List.of("Y", "Z"))
+        );
+        return maps.stream().map(p -> new Object[] { p }).toArray(Object[][]::new);
+    }
+
+    @Test(dataProvider = "filterMaps")
+    public void testFilter(Map<String,List<String>> map) {
+        HttpHeaders headers = HttpHeaders.of(map, REJECT_ALL);
+        assertEquals(headers.map().size(), 0);
+        assertFalse(headers.firstValue("A").isPresent());
+        assertEquals(headers.allValues("A").size(), 0);
+
+        headers = HttpHeaders.of(map, (name, value) -> {
+            if (name.equals("A")) return true; else return false; });
+        assertEquals(headers.map().size(), 1);
+        assertTrue(headers.firstValue("A").isPresent());
+        assertEquals(headers.allValues("A"), map.get("A"));
+        assertEquals(headers.allValues("A").size(), map.get("A").size());
+        assertFalse(headers.firstValue("X").isPresent());
+
+        headers = HttpHeaders.of(map, (name, value) -> {
+            if (name.equals("X")) return true; else return false; });
+        assertEquals(headers.map().size(), 1);
+        assertTrue(headers.firstValue("X").isPresent());
+        assertEquals(headers.allValues("X"), map.get("X"));
+        assertEquals(headers.allValues("X").size(), map.get("X").size());
+        assertFalse(headers.firstValue("A").isPresent());
+    }
+
+
+    @DataProvider(name = "mapValues")
+    public Object[][] mapValues() {
+        List<Map<String, List<String>>> maps = List.of(
+            Map.of("A", List.of("B")),
+            Map.of("A", List.of("B", "C")),
+            Map.of("A", List.of("B", "C", "D")),
+
+            Map.of("A", List.of("B"),           "X", List.of("Y", "Z")),
+            Map.of("A", List.of("B", "C"),      "X", List.of("Y", "Z")),
+            Map.of("A", List.of("B", "C", "D"), "X", List.of("Y", "Z")),
+
+            Map.of("A", List.of("B"),           "X", List.of("Y", "Z")),
+            Map.of("A", List.of("B", "C"),      "X", List.of("Y", "Z")),
+            Map.of("A", List.of("B", "C", "D"), "X", List.of("Y", "Z")),
+
+            Map.of("X", List.of("Y", "Z"), "A", List.of("B")),
+            Map.of("X", List.of("Y", "Z"), "A", List.of("B", "C")),
+            Map.of("X", List.of("Y", "Z"), "A", List.of("B", "C", "D"))
+        );
+        return maps.stream().map(p -> new Object[] { p }).toArray(Object[][]::new);
+    }
+
+    @Test(dataProvider = "mapValues")
+    public void testMapValues(Map<String,List<String>> map) {
+        HttpHeaders headers = HttpHeaders.of(map, ACCEPT_ALL);
+
+        assertEquals(headers.map().size(), map.size());
+        assertTrue(headers.firstValue("A").isPresent());
+        assertTrue(headers.firstValue("a").isPresent());
+        assertEquals(headers.firstValue("A").get(), "B");
+        assertEquals(headers.firstValue("a").get(), "B");
+        assertEquals(headers.allValues("A"), map.get("A"));
+        assertEquals(headers.allValues("a"), map.get("A"));
+        assertEquals(headers.allValues("F").size(), 0);
+        assertTrue(headers.map().get("A").contains("B"));
+        assertFalse(headers.map().get("A").contains("F"));
+        assertThrows(NFE, () -> headers.firstValueAsLong("A"));
+
+        // a non-exhaustive list of mutators
+        assertThrows(UOE, () -> headers.map().put("Z", List.of("Z")));
+        assertThrows(UOE, () -> headers.map().remove("A"));
+        assertThrows(UOE, () -> headers.map().remove("A", "B"));
+        assertThrows(UOE, () -> headers.map().clear());
+        assertThrows(UOE, () -> headers.allValues("A").remove("B"));
+        assertThrows(UOE, () -> headers.allValues("A").remove(1));
+        assertThrows(UOE, () -> headers.allValues("A").clear());
+        assertThrows(UOE, () -> headers.allValues("A").add("Z"));
+        assertThrows(UOE, () -> headers.allValues("A").addAll(List.of("Z")));
+        assertThrows(UOE, () -> headers.allValues("A").add(1, "Z"));
+    }
+
+
+    @DataProvider(name = "caseInsensitivity")
+    public Object[][] caseInsensitivity() {
+        List<Map<String, List<String>>> maps = List.of(
+             Map.of("Accept-Encoding", List.of("gzip, deflate")),
+             Map.of("accept-encoding", List.of("gzip, deflate")),
+             Map.of("AccePT-ENCoding", List.of("gzip, deflate")),
+             Map.of("ACCept-EncodING", List.of("gzip, deflate")),
+             Map.of("ACCEPT-ENCODING", List.of("gzip, deflate"))
+        );
+        return maps.stream().map(p -> new Object[] { p }).toArray(Object[][]::new);
+    }
+
+    @Test(dataProvider = "caseInsensitivity")
+    public void testCaseInsensitivity(Map<String,List<String>> map) {
+        HttpHeaders headers = HttpHeaders.of(map, ACCEPT_ALL);
+
+        for (String name : List.of("Accept-Encoding", "accept-encoding",
+                                   "aCCept-EnCODing", "accepT-encodinG")) {
+            assertTrue(headers.firstValue(name).isPresent());
+            assertTrue(headers.allValues(name).contains("gzip, deflate"));
+            assertEquals(headers.firstValue(name).get(), "gzip, deflate");
+            assertEquals(headers.allValues(name).size(), 1);
+            assertEquals(headers.map().size(), 1);
+            assertEquals(headers.map().get(name).size(), 1);
+            assertEquals(headers.map().get(name).get(0), "gzip, deflate");
+        }
+    }
+
+    @DataProvider(name = "valueAsLong")
+    public Object[][] valueAsLong() {
+        return new Object[][] {
+            new Object[] { Map.of("Content-Length", List.of("10")), 10l },
+            new Object[] { Map.of("Content-Length", List.of("101")), 101l },
+            new Object[] { Map.of("Content-Length", List.of("56789")), 56789l },
+            new Object[] { Map.of("Content-Length", List.of(Long.toString(Long.MAX_VALUE))), Long.MAX_VALUE },
+            new Object[] { Map.of("Content-Length", List.of(Long.toString(Long.MIN_VALUE))), Long.MIN_VALUE }
+        };
+    }
+
+    @Test(dataProvider = "valueAsLong")
+    public void testValueAsLong(Map<String,List<String>> map, long expected) {
+        HttpHeaders headers = HttpHeaders.of(map, ACCEPT_ALL);
+        assertEquals(headers.firstValueAsLong("Content-Length").getAsLong(), expected);
+    }
+
+
+    @DataProvider(name = "duplicateNames")
+    public Object[][] duplicateNames() {
+        List<Map<String, List<String>>> maps = List.of(
+                Map.of("X-name", List.of(),
+                       "x-name", List.of()),
+                Map.of("X-name", List.of(""),
+                       "x-name", List.of("")),
+                Map.of("X-name", List.of("C"),
+                       "x-name", List.of("D")),
+                Map.of("X-name", List.of("E"),
+                       "Y-name", List.of("F"),
+                       "X-Name", List.of("G")),
+                Map.of("X-chegar", List.of("H"),
+                       "y-dfuchs", List.of("I"),
+                       "Y-dfuchs", List.of("J")),
+                Map.of("X-name ", List.of("K"),
+                       "X-Name", List.of("L")),
+                Map.of("X-name", List.of("M"),
+                       "\rX-Name", List.of("N"))
+        );
+        return maps.stream().map(p -> new Object[] { p }).toArray(Object[][]::new);
+    }
+
+    @Test(dataProvider = "duplicateNames")
+    public void testDuplicates(Map<String,List<String>> map) {
+        HttpHeaders headers;
+        try {
+            headers = HttpHeaders.of(map, ACCEPT_ALL);
+            fail("UNEXPECTED: " + headers);
+        } catch (IllegalArgumentException iae) {
+            System.out.println("caught EXPECTED IAE:" + iae);
+            assertTrue(iae.getMessage().contains("duplicate"));
+        }
+    }
+
+
+    @DataProvider(name = "noSplittingJoining")
+    public Object[][] noSplittingJoining() {
+        List<Map<String, List<String>>> maps = List.of(
+                Map.of("A", List.of("B")),
+                Map.of("A", List.of("B", "C")),
+                Map.of("A", List.of("B", "C", "D")),
+                Map.of("A", List.of("B", "C", "D", "E")),
+                Map.of("A", List.of("B", "C", "D", "E", "F")),
+                Map.of("A", List.of("B, C")),
+                Map.of("A", List.of("B, C, D")),
+                Map.of("A", List.of("B, C, D, E")),
+                Map.of("A", List.of("B, C, D, E, F")),
+                Map.of("A", List.of("B, C", "D", "E", "F")),
+                Map.of("A", List.of("B", "C, D", "E", "F")),
+                Map.of("A", List.of("B", "C, D", "E, F")),
+                Map.of("A", List.of("B", "C, D, E", "F")),
+                Map.of("A", List.of("B", "C, D, E, F"))
+        );
+        return maps.stream().map(p -> new Object[] { p }).toArray(Object[][]::new);
+    }
+
+    @Test(dataProvider = "noSplittingJoining")
+    public void testNoSplittingJoining(Map<String,List<String>> map) {
+        HttpHeaders headers = HttpHeaders.of(map, ACCEPT_ALL);
+        Map<String,List<String>> headersMap = headers.map();
+
+        assertEquals(headers.map().size(), map.size());
+        for (Map.Entry<String,List<String>> entry : map.entrySet()) {
+            String headerName = entry.getKey();
+            List<String> headerValues = entry.getValue();
+            assertEquals(headerValues, headersMap.get(headerName));
+            assertEquals(headerValues, headers.allValues(headerName));
+            assertEquals(headerValues.get(0), headers.firstValue(headerName).get());
+        }
+    }
+
+
+    @DataProvider(name = "trimming")
+    public Object[][] trimming() {
+        List<Map<String, List<String>>> maps = List.of(
+                Map.of("A", List.of("B")),
+                Map.of(" A", List.of("B")),
+                Map.of("A ", List.of("B")),
+                Map.of("A", List.of(" B")),
+                Map.of("A", List.of("B ")),
+                Map.of("\tA", List.of("B")),
+                Map.of("A\t", List.of("B")),
+                Map.of("A", List.of("\tB")),
+                Map.of("A", List.of("B\t")),
+                Map.of("A\r", List.of("B")),
+                Map.of("A\n", List.of("B")),
+                Map.of("A\r\n", List.of("B"))
+        );
+        return maps.stream().map(p -> new Object[] { p }).toArray(Object[][]::new);
+    }
+
+    @Test(dataProvider = "trimming")
+    public void testTrimming(Map<String,List<String>> map) {
+        HttpHeaders headers = HttpHeaders.of(map, (name, value) -> {
+            assertEquals(name, "A");
+            assertEquals(value, "B");
+            return true;
+        });
+
+        assertEquals(headers.map().size(), 1);
+        assertEquals(headers.firstValue("A").get(), "B");
+        assertEquals(headers.allValues("A"), List.of("B"));
+        assertTrue(headers.map().get("A").equals(List.of("B")));
+    }
+
+
+    @DataProvider(name = "emptyKey")
+    public Object[][] emptyKey() {
+        List<Map<String, List<String>>> maps = List.of(
+                Map.of("", List.of("B")),
+                Map.of(" ", List.of("B")),
+                Map.of("  ", List.of("B")),
+                Map.of("\t", List.of("B")),
+                Map.of("\t\t", List.of("B"))
+        );
+        return maps.stream().map(p -> new Object[] { p }).toArray(Object[][]::new);
+    }
+
+    @Test(dataProvider = "emptyKey")
+    public void testEmptyKey(Map<String,List<String>> map) {
+        HttpHeaders headers;
+        try {
+            headers = HttpHeaders.of(map, ACCEPT_ALL);
+            fail("UNEXPECTED: " + headers);
+        } catch (IllegalArgumentException iae) {
+            System.out.println("caught EXPECTED IAE:" + iae);
+            assertTrue(iae.getMessage().contains("empty"));
+        }
+    }
+
+
+    @DataProvider(name = "emptyValue")
+    public Object[][] emptyValue() {
+        List<Map<String, List<String>>> maps = List.of(
+                Map.of("A", List.of("")),
+                Map.of("A", List.of("", "")),
+                Map.of("A", List.of("", "", " ")),
+                Map.of("A", List.of("\t")),
+                Map.of("A", List.of("\t\t"))
+        );
+        return maps.stream().map(p -> new Object[] { p }).toArray(Object[][]::new);
+    }
+
+    @Test(dataProvider = "emptyValue")
+    public void testEmptyValue(Map<String,List<String>> map) {
+        HttpHeaders headers = HttpHeaders.of(map, (name, value) -> {
+            assertEquals(value, "");
+            return true;
+        });
+        assertEquals(headers.map().size(), map.size());
+        assertEquals(headers.map().get("A").get(0), "");
+        headers.allValues("A").forEach(v -> assertEquals(v, ""));
+        assertEquals(headers.firstValue("A").get(), "");
+    }
+
+
+    @DataProvider(name = "noValues")
+    public Object[][] noValues() {
+        List<Map<String, List<String>>> maps = List.of(
+                Map.of("A", List.of()),
+                Map.of("A", List.of(), "B", List.of()),
+                Map.of("A", List.of(), "B", List.of(), "C", List.of()),
+                Map.of("A", new ArrayList()),
+                Map.of("A", new LinkedList())
+        );
+        return maps.stream().map(p -> new Object[] { p }).toArray(Object[][]::new);
+    }
+
+    @Test(dataProvider = "noValues")
+    public void testNoValues(Map<String,List<String>> map) {
+        HttpHeaders headers = HttpHeaders.of(map, (name, value) -> {
+            fail("UNEXPECTED call to filter");
+            return true;
+        });
+        assertEquals(headers.map().size(), 0);
+        assertEquals(headers.map().get("A"), null);
+        assertEquals(headers.allValues("A").size(), 0);
+        assertFalse(headers.firstValue("A").isPresent());
+    }
+}
--- a/test/jdk/java/net/httpclient/HttpInputStreamTest.java	Mon May 28 17:22:37 2018 +0100
+++ b/test/jdk/java/net/httpclient/HttpInputStreamTest.java	Fri May 25 16:13:11 2018 +0100
@@ -47,7 +47,7 @@
 /*
  * @test
  * @summary An example on how to read a response body with InputStream.
- * @run main/manual -Dtest.debug=true HttpInputStreamTest
+ * @run main/othervm/manual -Dtest.debug=true HttpInputStreamTest
  * @author daniel fuchs
  */
 public class HttpInputStreamTest {
--- a/test/jdk/java/net/httpclient/HttpServerAdapters.java	Mon May 28 17:22:37 2018 +0100
+++ b/test/jdk/java/net/httpclient/HttpServerAdapters.java	Fri May 25 16:13:11 2018 +0100
@@ -27,11 +27,11 @@
 import com.sun.net.httpserver.HttpExchange;
 import com.sun.net.httpserver.HttpHandler;
 import com.sun.net.httpserver.HttpServer;
+import jdk.internal.net.http.common.HttpHeadersBuilder;
 
 import java.net.InetAddress;
 import java.io.ByteArrayInputStream;
 import java.net.http.HttpClient.Version;
-import jdk.internal.net.http.common.HttpHeadersImpl;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
@@ -41,6 +41,7 @@
 import java.math.BigInteger;
 import java.net.InetSocketAddress;
 import java.net.URI;
+import java.net.http.HttpHeaders;
 import java.util.List;
 import java.util.ListIterator;
 import java.util.Map;
@@ -95,26 +96,26 @@
     }
 
     /**
-     * A version agnostic adapter class for HTTP Headers.
+     * A version agnostic adapter class for HTTP request Headers.
      */
-    public static abstract class HttpTestHeaders {
+    public static abstract class HttpTestRequestHeaders {
         public abstract Optional<String> firstValue(String name);
-        public abstract void addHeader(String name, String value);
         public abstract Set<String> keySet();
         public abstract Set<Map.Entry<String, List<String>>> entrySet();
         public abstract List<String> get(String name);
         public abstract boolean containsKey(String name);
 
-        public static HttpTestHeaders of(Headers headers) {
-            return new Http1TestHeaders(headers);
-        }
-        public static HttpTestHeaders of(HttpHeadersImpl headers) {
-            return new Http2TestHeaders(headers);
+        public static HttpTestRequestHeaders of(Headers headers) {
+            return new Http1TestRequestHeaders(headers);
         }
 
-        private final static class Http1TestHeaders extends HttpTestHeaders {
+        public static HttpTestRequestHeaders of(HttpHeaders headers) {
+            return new Http2TestRequestHeaders(headers);
+        }
+
+        private static final class Http1TestRequestHeaders extends HttpTestRequestHeaders {
             private final Headers headers;
-            Http1TestHeaders(Headers h) { this.headers = h; }
+            Http1TestRequestHeaders(Headers h) { this.headers = h; }
             @Override
             public Optional<String> firstValue(String name) {
                 if (headers.containsKey(name)) {
@@ -123,11 +124,6 @@
                 return Optional.empty();
             }
             @Override
-            public void addHeader(String name, String value) {
-                headers.add(name, value);
-            }
-
-            @Override
             public Set<String> keySet() { return headers.keySet(); }
             @Override
             public Set<Map.Entry<String, List<String>>> entrySet() {
@@ -142,17 +138,14 @@
                 return headers.containsKey(name);
             }
         }
-        private final static class Http2TestHeaders extends HttpTestHeaders {
-            private final HttpHeadersImpl headers;
-            Http2TestHeaders(HttpHeadersImpl h) { this.headers = h; }
+        private static final class Http2TestRequestHeaders extends HttpTestRequestHeaders {
+            private final HttpHeaders headers;
+            Http2TestRequestHeaders(HttpHeaders h) { this.headers = h; }
             @Override
             public Optional<String> firstValue(String name) {
                 return headers.firstValue(name);
             }
             @Override
-            public void addHeader(String name, String value) {
-                headers.addHeader(name, value);
-            }
             public Set<String> keySet() { return headers.map().keySet(); }
             @Override
             public Set<Map.Entry<String, List<String>>> entrySet() {
@@ -170,6 +163,37 @@
     }
 
     /**
+     * A version agnostic adapter class for HTTP response Headers.
+     */
+    public static abstract class HttpTestResponseHeaders {
+        public abstract void addHeader(String name, String value);
+
+        public static HttpTestResponseHeaders of(Headers headers) {
+            return new Http1TestResponseHeaders(headers);
+        }
+        public static HttpTestResponseHeaders of(HttpHeadersBuilder headersBuilder) {
+            return new Http2TestResponseHeaders(headersBuilder);
+        }
+
+        private final static class Http1TestResponseHeaders extends HttpTestResponseHeaders {
+            private final Headers headers;
+            Http1TestResponseHeaders(Headers h) { this.headers = h; }
+            @Override
+            public void addHeader(String name, String value) {
+                headers.add(name, value);
+            }
+        }
+        private final static class Http2TestResponseHeaders extends HttpTestResponseHeaders {
+            private final HttpHeadersBuilder headersBuilder;
+            Http2TestResponseHeaders(HttpHeadersBuilder hb) { this.headersBuilder = hb; }
+            @Override
+            public void addHeader(String name, String value) {
+                headersBuilder.addHeader(name, value);
+            }
+        }
+    }
+
+    /**
      * A version agnostic adapter class for HTTP Server Exchange.
      */
     public static abstract class HttpTestExchange {
@@ -177,17 +201,17 @@
         public abstract Version getExchangeVersion();
         public abstract InputStream   getRequestBody();
         public abstract OutputStream  getResponseBody();
-        public abstract HttpTestHeaders getRequestHeaders();
-        public abstract HttpTestHeaders getResponseHeaders();
+        public abstract HttpTestRequestHeaders getRequestHeaders();
+        public abstract HttpTestResponseHeaders getResponseHeaders();
         public abstract void sendResponseHeaders(int code, int contentLength) throws IOException;
         public abstract URI getRequestURI();
         public abstract String getRequestMethod();
         public abstract void close();
-        public void serverPush(URI uri, HttpTestHeaders headers, byte[] body) {
+        public void serverPush(URI uri, HttpHeaders headers, byte[] body) {
             ByteArrayInputStream bais = new ByteArrayInputStream(body);
             serverPush(uri, headers, bais);
         }
-        public void serverPush(URI uri, HttpTestHeaders headers, InputStream body) {
+        public void serverPush(URI uri, HttpHeaders headers, InputStream body) {
             throw new UnsupportedOperationException("serverPush with " + getExchangeVersion());
         }
         public boolean serverPushAllowed() {
@@ -221,12 +245,12 @@
                 return exchange.getResponseBody();
             }
             @Override
-            public HttpTestHeaders getRequestHeaders() {
-                return HttpTestHeaders.of(exchange.getRequestHeaders());
+            public HttpTestRequestHeaders getRequestHeaders() {
+                return HttpTestRequestHeaders.of(exchange.getRequestHeaders());
             }
             @Override
-            public HttpTestHeaders getResponseHeaders() {
-                return HttpTestHeaders.of(exchange.getResponseHeaders());
+            public HttpTestResponseHeaders getResponseHeaders() {
+                return HttpTestResponseHeaders.of(exchange.getResponseHeaders());
             }
             @Override
             public void sendResponseHeaders(int code, int contentLength) throws IOException {
@@ -268,12 +292,13 @@
                 return exchange.getResponseBody();
             }
             @Override
-            public HttpTestHeaders getRequestHeaders() {
-                return HttpTestHeaders.of(exchange.getRequestHeaders());
+            public HttpTestRequestHeaders getRequestHeaders() {
+                return HttpTestRequestHeaders.of(exchange.getRequestHeaders());
             }
+
             @Override
-            public HttpTestHeaders getResponseHeaders() {
-                return HttpTestHeaders.of(exchange.getResponseHeaders());
+            public HttpTestResponseHeaders getResponseHeaders() {
+                return HttpTestResponseHeaders.of(exchange.getResponseHeaders());
             }
             @Override
             public void sendResponseHeaders(int code, int contentLength) throws IOException {
@@ -286,20 +311,8 @@
                 return exchange.serverPushAllowed();
             }
             @Override
-            public void serverPush(URI uri, HttpTestHeaders headers, InputStream body) {
-                HttpHeadersImpl headersImpl;
-                if (headers instanceof HttpTestHeaders.Http2TestHeaders) {
-                    headersImpl = ((HttpTestHeaders.Http2TestHeaders)headers).headers.deepCopy();
-                } else {
-                    headersImpl = new HttpHeadersImpl();
-                    for (Map.Entry<String, List<String>> e : headers.entrySet()) {
-                        String name = e.getKey();
-                        for (String v : e.getValue()) {
-                            headersImpl.addHeader(name, v);
-                        }
-                    }
-                }
-                exchange.serverPush(uri, headersImpl, body);
+            public void serverPush(URI uri, HttpHeaders headers, InputStream body) {
+                exchange.serverPush(uri, headers, body);
             }
             void doFilter(Filter.Chain filter) throws IOException {
                 throw new IOException("cannot use HTTP/1.1 filter with HTTP/2 server");
@@ -363,7 +376,7 @@
     }
 
     public static boolean expectException(HttpTestExchange e) {
-        HttpTestHeaders h = e.getRequestHeaders();
+        HttpTestRequestHeaders h = e.getRequestHeaders();
         Optional<String> expectException = h.firstValue("X-expect-exception");
         if (expectException.isPresent()) {
             return expectException.get().equalsIgnoreCase("true");
--- a/test/jdk/java/net/httpclient/MethodsTest.java	Mon May 28 17:22:37 2018 +0100
+++ b/test/jdk/java/net/httpclient/MethodsTest.java	Fri May 25 16:13:11 2018 +0100
@@ -21,8 +21,6 @@
  * questions.
  */
 
-import jdk.internal.net.http.common.HttpHeadersImpl;
-
 import java.io.IOException;
 import java.net.http.HttpClient;
 import java.net.http.HttpHeaders;
@@ -30,10 +28,10 @@
 import java.net.URI;
 import java.net.http.HttpResponse;
 import java.time.Duration;
+import java.util.Map;
 import java.util.Optional;
 import static java.net.http.HttpClient.Builder.NO_PROXY;
 
-
 /**
  * @test
  * @bug 8199135
@@ -75,7 +73,7 @@
                     return Optional.empty();
                 }
                 @Override public HttpHeaders headers() {
-                    return new HttpHeadersImpl();
+                    return HttpHeaders.of(Map.of(), (x, y) -> true);
                 }
             };
             client.send(req, HttpResponse.BodyHandlers.ofString());
--- a/test/jdk/java/net/httpclient/RedirectMethodChange.java	Mon May 28 17:22:37 2018 +0100
+++ b/test/jdk/java/net/httpclient/RedirectMethodChange.java	Fri May 25 16:13:11 2018 +0100
@@ -283,14 +283,14 @@
             }
 
             if (newtest) {
-                HttpTestHeaders hdrs = he.getRequestHeaders();
+                HttpTestRequestHeaders hdrs = he.getRequestHeaders();
                 String value = hdrs.firstValue("X-Redirect-Code").get();
                 int redirectCode = Integer.parseInt(value);
                 expectedMethod = hdrs.firstValue("X-Expect-Method").get();
                 if (!readAndCheckBody(he))
                     return;
-                hdrs = he.getResponseHeaders();
-                hdrs.addHeader("Location", targetURL);
+                HttpTestResponseHeaders headersbuilder = he.getResponseHeaders();
+                headersbuilder.addHeader("Location", targetURL);
                 he.sendResponseHeaders(redirectCode, 0);
                 inTest = true;
             } else {
--- a/test/jdk/java/net/httpclient/ThrowingPushPromises.java	Mon May 28 17:22:37 2018 +0100
+++ b/test/jdk/java/net/httpclient/ThrowingPushPromises.java	Fri May 25 16:13:11 2018 +0100
@@ -35,7 +35,6 @@
  * @run testng/othervm -Djdk.internal.httpclient.debug=true ThrowingPushPromises
  */
 
-import jdk.internal.net.http.common.HttpHeadersImpl;
 import jdk.testlibrary.SimpleSSLContext;
 import org.testng.annotations.AfterTest;
 import org.testng.annotations.AfterClass;
@@ -53,6 +52,7 @@
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.http.HttpClient;
+import java.net.http.HttpHeaders;
 import java.net.http.HttpRequest;
 import java.net.http.HttpResponse;
 import java.net.http.HttpResponse.BodyHandler;
@@ -72,6 +72,7 @@
 import java.util.concurrent.Executors;
 import java.util.concurrent.Flow;
 import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.BiPredicate;
 import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.function.Predicate;
@@ -679,8 +680,13 @@
         }
     }
 
-    private static void pushPromiseFor(HttpTestExchange t, URI requestURI, String pushPath, boolean fixed)
-            throws IOException
+    static final BiPredicate<String,String> ACCEPT_ALL = (x, y) -> true;
+
+    private static void pushPromiseFor(HttpTestExchange t,
+                                       URI requestURI,
+                                       String pushPath,
+                                       boolean fixed)
+        throws IOException
     {
         try {
             URI promise = new URI(requestURI.getScheme(),
@@ -689,9 +695,13 @@
             byte[] promiseBytes = promise.toASCIIString().getBytes(UTF_8);
             out.printf("TestServer: %s Pushing promise: %s%n", now(), promise);
             err.printf("TestServer: %s Pushing promise: %s%n", now(), promise);
-            HttpTestHeaders headers =  HttpTestHeaders.of(new HttpHeadersImpl());
+            HttpHeaders headers;
             if (fixed) {
-                headers.addHeader("Content-length", String.valueOf(promiseBytes.length));
+                String length = String.valueOf(promiseBytes.length);
+                headers = HttpHeaders.of(Map.of("Content-Length", List.of(length)),
+                                         ACCEPT_ALL);
+            } else {
+                headers = HttpHeaders.of(Map.of(), ACCEPT_ALL); // empty
             }
             t.serverPush(promise, headers, promiseBytes);
         } catch (URISyntaxException x) {
--- a/test/jdk/java/net/httpclient/http2/BadHeadersTest.java	Mon May 28 17:22:37 2018 +0100
+++ b/test/jdk/java/net/httpclient/http2/BadHeadersTest.java	Fri May 25 16:13:11 2018 +0100
@@ -33,8 +33,7 @@
  * @run testng/othervm -Djdk.internal.httpclient.debug=true BadHeadersTest
  */
 
-import jdk.internal.net.http.common.HttpHeadersImpl;
-import jdk.internal.net.http.common.Pair;
+import jdk.internal.net.http.common.HttpHeadersBuilder;
 import jdk.internal.net.http.frame.ContinuationFrame;
 import jdk.internal.net.http.frame.HeaderFrame;
 import jdk.internal.net.http.frame.HeadersFrame;
@@ -44,41 +43,38 @@
 import org.testng.annotations.BeforeTest;
 import org.testng.annotations.DataProvider;
 import org.testng.annotations.Test;
-
 import javax.net.ssl.SSLContext;
 import javax.net.ssl.SSLSession;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.net.Socket;
 import java.net.URI;
 import java.net.http.HttpClient;
+import java.net.http.HttpHeaders;
 import java.net.http.HttpRequest;
 import java.net.http.HttpRequest.BodyPublishers;
+import java.net.http.HttpResponse;
 import java.net.http.HttpResponse.BodyHandlers;
 import java.nio.ByteBuffer;
 import java.util.ArrayList;
-import java.util.LinkedHashMap;
 import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.concurrent.CompletionException;
-import java.util.concurrent.atomic.AtomicInteger;
+import java.util.Map.Entry;
+import java.util.concurrent.ExecutionException;
 import java.util.function.BiFunction;
-
 import static java.util.List.of;
-import static jdk.internal.net.http.common.Pair.pair;
-import static org.testng.Assert.assertThrows;
+import static java.util.Map.entry;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
 
 // Code copied from ContinuationFrameTest
 public class BadHeadersTest {
 
-    private static final List<List<Pair<String, String>>> BAD_HEADERS = of(
-            of(pair(":status", "200"),  pair(":hello", "GET")),                      // Unknown pseudo-header
-            of(pair(":status", "200"),  pair("hell o", "value")),                    // Space in the name
-            of(pair(":status", "200"),  pair("hello", "line1\r\n  line2\r\n")),      // Multiline value
-            of(pair(":status", "200"),  pair("hello", "DE" + ((char) 0x7F) + "L")),  // Bad byte in value
-            of(pair("hello", "world!"), pair(":status", "200"))                      // Pseudo header is not the first one
+    private static final List<List<Entry<String, String>>> BAD_HEADERS = of(
+        of(entry(":status", "200"),  entry(":hello", "GET")),                      // Unknown pseudo-header
+        of(entry(":status", "200"),  entry("hell o", "value")),                    // Space in the name
+        of(entry(":status", "200"),  entry("hello", "line1\r\n  line2\r\n")),      // Multiline value
+        of(entry(":status", "200"),  entry("hello", "DE" + ((char) 0x7F) + "L")),  // Bad byte in value
+        of(entry("hello", "world!"), entry(":status", "200"))                      // Pseudo header is not the first one
     );
 
     SSLContext sslContext;
@@ -143,7 +139,35 @@
     void test(String uri,
               boolean sameClient,
               BiFunction<Integer,List<ByteBuffer>,List<Http2Frame>> headerFramesSupplier)
-            throws Exception
+        throws Exception
+    {
+        CFTHttp2TestExchange.setHeaderFrameSupplier(headerFramesSupplier);
+
+        HttpClient client = null;
+        for (int i=0; i< BAD_HEADERS.size(); i++) {
+            if (!sameClient || client == null)
+                client = HttpClient.newBuilder().sslContext(sslContext).build();
+
+            URI uriWithQuery = URI.create(uri +  "?BAD_HEADERS=" + i);
+            HttpRequest request = HttpRequest.newBuilder(uriWithQuery)
+                    .POST(BodyPublishers.ofString("Hello there!"))
+                    .build();
+            System.out.println("\nSending request:" + uriWithQuery);
+            final HttpClient cc = client;
+            try {
+                HttpResponse<String> response = cc.send(request, BodyHandlers.ofString());
+                fail("Expected exception, got :" + response + ", " + response.body());
+            } catch (IOException ioe) {
+                System.out.println("Got EXPECTED: " + ioe);
+                assertDetailMessage(ioe, i);
+            }
+        }
+    }
+
+    @Test(dataProvider = "variants")
+    void testAsync(String uri,
+                   boolean sameClient,
+                   BiFunction<Integer,List<ByteBuffer>,List<Http2Frame>> headerFramesSupplier)
     {
         CFTHttp2TestExchange.setHeaderFrameSupplier(headerFramesSupplier);
 
@@ -152,31 +176,45 @@
             if (!sameClient || client == null)
                 client = HttpClient.newBuilder().sslContext(sslContext).build();
 
-            HttpRequest request = HttpRequest.newBuilder(URI.create(uri))
+            URI uriWithQuery = URI.create(uri +  "?BAD_HEADERS=" + i);
+            HttpRequest request = HttpRequest.newBuilder(uriWithQuery)
                     .POST(BodyPublishers.ofString("Hello there!"))
                     .build();
+            System.out.println("\nSending request:" + uriWithQuery);
             final HttpClient cc = client;
-            if (i % 2 == 0) {
-                assertThrows(IOException.class, () -> cc.send(request, BodyHandlers.ofString()));
-            } else {
-                Throwable t = null;
-                try {
-                    cc.sendAsync(request, BodyHandlers.ofString()).join();
-                } catch (Throwable t0) {
-                    t = t0;
+
+            Throwable t = null;
+            try {
+                HttpResponse<String> response = cc.sendAsync(request, BodyHandlers.ofString()).get();
+                fail("Expected exception, got :" + response + ", " + response.body());
+            } catch (Throwable t0) {
+                System.out.println("Got EXPECTED: " + t0);
+                if (t0 instanceof ExecutionException) {
+                    t0 = t0.getCause();
                 }
-                if (t == null) {
-                    throw new AssertionError("An exception was expected");
-                }
-                if (t instanceof CompletionException) {
-                    Throwable c = t.getCause();
-                    if (!(c instanceof IOException)) {
-                        throw new AssertionError("Unexpected exception", c);
-                    }
-                } else if (!(t instanceof IOException)) {
-                    throw new AssertionError("Unexpected exception", t);
-                }
+                t = t0;
             }
+            assertDetailMessage(t, i);
+        }
+    }
+
+    // Assertions based on implementation specific detail messages. Keep in
+    // sync with implementation.
+    static void assertDetailMessage(Throwable throwable, int iterationIndex) {
+        assertTrue(throwable instanceof IOException,
+                   "Expected IOException, got, " + throwable);
+        assertTrue(throwable.getMessage().contains("protocol error"),
+                "Expected \"protocol error\" in: " + throwable.getMessage());
+
+        if (iterationIndex == 0) { // unknown
+            assertTrue(throwable.getMessage().contains("Unknown pseudo-header"),
+                    "Expected \"Unknown pseudo-header\" in: " + throwable.getMessage());
+        } else if (iterationIndex == 4) { // unexpected
+            assertTrue(throwable.getMessage().contains(" Unexpected pseudo-header"),
+                    "Expected \" Unexpected pseudo-header\" in: " + throwable.getMessage());
+        } else {
+            assertTrue(throwable.getMessage().contains("Bad header"),
+                    "Expected \"Bad header\" in: " + throwable.getMessage());
         }
     }
 
@@ -186,38 +224,12 @@
         if (sslContext == null)
             throw new AssertionError("Unexpected null sslContext");
 
-        http2TestServer = new Http2TestServer("localhost", false, 0) {
-            @Override
-            protected Http2TestServerConnection createConnection(Http2TestServer http2TestServer,
-                                                                 Socket socket,
-                                                                 Http2TestExchangeSupplier exchangeSupplier)
-                    throws IOException {
-                return new Http2TestServerConnection(http2TestServer, socket, exchangeSupplier, null) {
-                    @Override
-                    protected HttpHeadersImpl createNewResponseHeaders() {
-                        return new OrderedHttpHeaders();
-                    }
-                };
-            }
-        };
+        http2TestServer = new Http2TestServer("localhost", false, 0);
         http2TestServer.addHandler(new Http2EchoHandler(), "/http2/echo");
         int port = http2TestServer.getAddress().getPort();
         http2URI = "http://localhost:" + port + "/http2/echo";
 
-        https2TestServer = new Http2TestServer("localhost", true, 0){
-            @Override
-            protected Http2TestServerConnection createConnection(Http2TestServer http2TestServer,
-                                                                 Socket socket,
-                                                                 Http2TestExchangeSupplier exchangeSupplier)
-                    throws IOException {
-                return new Http2TestServerConnection(http2TestServer, socket, exchangeSupplier, null) {
-                    @Override
-                    protected HttpHeadersImpl createNewResponseHeaders() {
-                        return new OrderedHttpHeaders();
-                    }
-                };
-            }
-        };
+        https2TestServer = new Http2TestServer("localhost", true, 0);
         https2TestServer.addHandler(new Http2EchoHandler(), "/https2/echo");
         port = https2TestServer.getAddress().getPort();
         https2URI = "https://localhost:" + port + "/https2/echo";
@@ -239,16 +251,14 @@
 
     static class Http2EchoHandler implements Http2Handler {
 
-        private final AtomicInteger requestNo = new AtomicInteger();
-
         @Override
         public void handle(Http2TestExchange t) throws IOException {
             try (InputStream is = t.getRequestBody();
                  OutputStream os = t.getResponseBody()) {
                 byte[] bytes = is.readAllBytes();
-                int i = requestNo.incrementAndGet();
-                List<Pair<String, String>> p = BAD_HEADERS.get(i % BAD_HEADERS.size());
-                p.forEach(h -> t.getResponseHeaders().addHeader(h.first, h.second));
+                // Note: strictly ordered response headers will be added within
+                // the custom sendResponseHeaders implementation, based upon the
+                // query parameter
                 t.sendResponseHeaders(200, bytes.length);
                 os.write(bytes);
             }
@@ -259,23 +269,34 @@
     // allow headers to be sent with a number of CONTINUATION frames.
     static class CFTHttp2TestExchange extends Http2TestExchangeImpl {
         static volatile BiFunction<Integer,List<ByteBuffer>,List<Http2Frame>> headerFrameSupplier;
+        volatile int badHeadersIndex = -1;
 
         static void setHeaderFrameSupplier(BiFunction<Integer,List<ByteBuffer>,List<Http2Frame>> hfs) {
             headerFrameSupplier = hfs;
         }
 
-        CFTHttp2TestExchange(int streamid, String method, HttpHeadersImpl reqheaders,
-                             HttpHeadersImpl rspheaders, URI uri, InputStream is,
+        CFTHttp2TestExchange(int streamid, String method, HttpHeaders reqheaders,
+                             HttpHeadersBuilder rspheadersBuilder, URI uri, InputStream is,
                              SSLSession sslSession, BodyOutputStream os,
                              Http2TestServerConnection conn, boolean pushAllowed) {
-            super(streamid, method, reqheaders, rspheaders, uri, is, sslSession,
+            super(streamid, method, reqheaders, rspheadersBuilder, uri, is, sslSession,
                   os, conn, pushAllowed);
-
+            String query = uri.getQuery();
+            badHeadersIndex = Integer.parseInt(query.substring(query.indexOf("=") + 1));
+            assert badHeadersIndex >= 0 && badHeadersIndex < BAD_HEADERS.size() :
+                    "Unexpected badHeadersIndex value: " + badHeadersIndex;
         }
 
         @Override
         public void sendResponseHeaders(int rCode, long responseLength) throws IOException {
-            List<ByteBuffer> encodeHeaders = conn.encodeHeaders(rspheaders);
+            assert rspheadersBuilder.build().map().size() == 0;
+            assert badHeadersIndex >= 0 && badHeadersIndex < BAD_HEADERS.size() :
+                    "Unexpected badHeadersIndex value: " + badHeadersIndex;
+
+            List<Entry<String,String>> headers = BAD_HEADERS.get(badHeadersIndex);
+            System.out.println("Server replying with bad headers: " + headers);
+            List<ByteBuffer> encodeHeaders = conn.encodeHeadersOrdered(headers);
+
             List<Http2Frame> headerFrames = headerFrameSupplier.apply(streamid, encodeHeaders);
             assert headerFrames.size() > 0;  // there must always be at least 1
 
@@ -291,36 +312,4 @@
             System.err.println("Sent response headers " + rCode);
         }
     }
-
-    /*
-     * Use carefully. This class might not be suitable outside this test's
-     * context. Pay attention working with multi Map view returned from map().
-     * The reason is that header names must be lower-cased prior to any
-     * operation that depends on whether or not the map contains a specific
-     * element.
-     */
-    private static class OrderedHttpHeaders extends HttpHeadersImpl {
-
-        private final Map<String, List<String>> map = new LinkedHashMap<>();
-
-        @Override
-        public void addHeader(String name, String value) {
-            super.addHeader(name.toLowerCase(Locale.ROOT), value);
-        }
-
-        @Override
-        public void setHeader(String name, String value) {
-            super.setHeader(name.toLowerCase(Locale.ROOT), value);
-        }
-
-        @Override
-        protected Map<String, List<String>> headersMap() {
-            return map;
-        }
-
-        @Override
-        protected HttpHeadersImpl newDeepCopy() {
-            return new OrderedHttpHeaders();
-        }
-    }
 }
--- a/test/jdk/java/net/httpclient/http2/ContinuationFrameTest.java	Mon May 28 17:22:37 2018 +0100
+++ b/test/jdk/java/net/httpclient/http2/ContinuationFrameTest.java	Fri May 25 16:13:11 2018 +0100
@@ -38,6 +38,7 @@
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.net.URI;
+import java.net.http.HttpHeaders;
 import java.nio.ByteBuffer;
 import java.util.ArrayList;
 import java.util.List;
@@ -49,7 +50,7 @@
 import java.net.http.HttpRequest.BodyPublishers;
 import java.net.http.HttpResponse;
 import java.net.http.HttpResponse.BodyHandlers;
-import jdk.internal.net.http.common.HttpHeadersImpl;
+import jdk.internal.net.http.common.HttpHeadersBuilder;
 import jdk.internal.net.http.frame.ContinuationFrame;
 import jdk.internal.net.http.frame.HeaderFrame;
 import jdk.internal.net.http.frame.HeadersFrame;
@@ -212,11 +213,11 @@
             headerFrameSupplier = hfs;
         }
 
-        CFTHttp2TestExchange(int streamid, String method, HttpHeadersImpl reqheaders,
-                             HttpHeadersImpl rspheaders, URI uri, InputStream is,
+        CFTHttp2TestExchange(int streamid, String method, HttpHeaders reqheaders,
+                             HttpHeadersBuilder rspheadersBuilder, URI uri, InputStream is,
                              SSLSession sslSession, BodyOutputStream os,
                              Http2TestServerConnection conn, boolean pushAllowed) {
-            super(streamid, method, reqheaders, rspheaders, uri, is, sslSession,
+            super(streamid, method, reqheaders, rspheadersBuilder, uri, is, sslSession,
                   os, conn, pushAllowed);
 
         }
@@ -226,11 +227,12 @@
             this.responseLength = responseLength;
             if (responseLength > 0 || responseLength < 0) {
                 long clen = responseLength > 0 ? responseLength : 0;
-                rspheaders.setHeader("Content-length", Long.toString(clen));
+                rspheadersBuilder.setHeader("Content-length", Long.toString(clen));
             }
-            rspheaders.setHeader(":status", Integer.toString(rCode));
+            rspheadersBuilder.setHeader(":status", Integer.toString(rCode));
+            HttpHeaders headers = rspheadersBuilder.build();
 
-            List<ByteBuffer> encodeHeaders = conn.encodeHeaders(rspheaders);
+            List<ByteBuffer> encodeHeaders = conn.encodeHeaders(headers);
             List<Http2Frame> headerFrames = headerFrameSupplier.apply(streamid, encodeHeaders);
             assert headerFrames.size() > 0;  // there must always be at least 1
 
--- a/test/jdk/java/net/httpclient/http2/ImplicitPushCancel.java	Mon May 28 17:22:37 2018 +0100
+++ b/test/jdk/java/net/httpclient/http2/ImplicitPushCancel.java	Fri May 25 16:13:11 2018 +0100
@@ -40,18 +40,18 @@
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpHeaders;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandlers;
+import java.net.http.HttpResponse.PushPromiseHandler;
+import java.util.Collections;
 import java.util.Map;
 import java.util.Objects;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
-
-import java.net.http.HttpClient;
-import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
-import java.net.http.HttpResponse.BodyHandlers;
-import java.net.http.HttpResponse.PushPromiseHandler;
-import jdk.internal.net.http.common.HttpHeadersImpl;
 import org.testng.annotations.AfterTest;
 import org.testng.annotations.BeforeTest;
 import org.testng.annotations.Test;
@@ -177,7 +177,7 @@
             for (Map.Entry<String,String> promise : promises.entrySet()) {
                 URI uri = requestURI.resolve(promise.getKey());
                 InputStream is = new ByteArrayInputStream(promise.getValue().getBytes(UTF_8));
-                HttpHeadersImpl headers = new HttpHeadersImpl();
+                HttpHeaders headers = HttpHeaders.of(Collections.emptyMap(), (x, y) -> true);
                 exchange.serverPush(uri, headers, is);
             }
             System.err.println("Server: All pushes sent");
--- a/test/jdk/java/net/httpclient/http2/ServerPush.java	Mon May 28 17:22:37 2018 +0100
+++ b/test/jdk/java/net/httpclient/http2/ServerPush.java	Fri May 25 16:13:11 2018 +0100
@@ -53,7 +53,6 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.testng.Assert.*;
 
-
 public class ServerPush {
 
     static final int LOOPS = 13;
--- a/test/jdk/java/net/httpclient/http2/ServerPushWithDiffTypes.java	Mon May 28 17:22:37 2018 +0100
+++ b/test/jdk/java/net/httpclient/http2/ServerPushWithDiffTypes.java	Fri May 25 16:13:11 2018 +0100
@@ -46,7 +46,8 @@
 import java.net.http.HttpResponse.BodySubscribers;
 import java.util.*;
 import java.util.concurrent.*;
-import jdk.internal.net.http.common.HttpHeadersImpl;
+import java.util.function.BiPredicate;
+
 import org.testng.annotations.Test;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.testng.Assert.assertEquals;
@@ -242,14 +243,16 @@
             }
         }
 
+        static final BiPredicate<String,String> ACCEPT_ALL = (x, y) -> true;
+
         private void pushPromises(Http2TestExchange exchange) throws IOException {
             URI requestURI = exchange.getRequestURI();
             for (Map.Entry<String,String> promise : promises.entrySet()) {
                 URI uri = requestURI.resolve(promise.getKey());
                 InputStream is = new ByteArrayInputStream(promise.getValue().getBytes(UTF_8));
-                HttpHeadersImpl headers = new HttpHeadersImpl();
+                Map<String,List<String>> map = Map.of("X-Promise", List.of(promise.getKey()));
+                HttpHeaders headers = HttpHeaders.of(map, ACCEPT_ALL);
                 // TODO: add some check on headers, maybe
-                headers.addHeader("X-Promise", promise.getKey());
                 exchange.serverPush(uri, headers, is);
             }
             System.err.println("Server: All pushes sent");
--- a/test/jdk/java/net/httpclient/http2/server/Http2EchoHandler.java	Mon May 28 17:22:37 2018 +0100
+++ b/test/jdk/java/net/httpclient/http2/server/Http2EchoHandler.java	Fri May 25 16:13:11 2018 +0100
@@ -22,11 +22,11 @@
  */
 
 import java.io.*;
+import java.net.http.HttpHeaders;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
-
-import jdk.internal.net.http.common.HttpHeadersImpl;
+import jdk.internal.net.http.common.HttpHeadersBuilder;
 
 public class Http2EchoHandler implements Http2Handler {
     static final Path CWD = Paths.get(".");
@@ -40,10 +40,10 @@
             System.err.printf("EchoHandler received request to %s from %s\n",
                               t.getRequestURI(), t.getRemoteAddress());
             InputStream is = t.getRequestBody();
-            HttpHeadersImpl map = t.getRequestHeaders();
-            HttpHeadersImpl map1 = t.getResponseHeaders();
-            map1.addHeader("X-Hello", "world");
-            map1.addHeader("X-Bye", "universe");
+            HttpHeaders map = t.getRequestHeaders();
+            HttpHeadersBuilder headersBuilder = t.getResponseHeaders();
+            headersBuilder.addHeader("X-Hello", "world");
+            headersBuilder.addHeader("X-Bye", "universe");
             String fixedrequest = map.firstValue("XFixed").orElse(null);
             File outfile = Files.createTempFile(CWD, "foo", "bar").toFile();
             //System.err.println ("QQQ = " + outfile.toString());
--- a/test/jdk/java/net/httpclient/http2/server/Http2RedirectHandler.java	Mon May 28 17:22:37 2018 +0100
+++ b/test/jdk/java/net/httpclient/http2/server/Http2RedirectHandler.java	Fri May 25 16:13:11 2018 +0100
@@ -25,7 +25,7 @@
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.util.function.Supplier;
-import jdk.internal.net.http.common.HttpHeadersImpl;
+import jdk.internal.net.http.common.HttpHeadersBuilder;
 
 public class Http2RedirectHandler implements Http2Handler {
 
@@ -44,8 +44,8 @@
             System.err.printf("RedirectHandler request to %s from %s\n",
                 t.getRequestURI().toString(), t.getRemoteAddress().toString());
             System.err.println("Redirecting to: " + location);
-            HttpHeadersImpl map1 = t.getResponseHeaders();
-            map1.addHeader("Location", location);
+            HttpHeadersBuilder headersBuilder = t.getResponseHeaders();
+            headersBuilder.addHeader("Location", location);
             t.sendResponseHeaders(301, 1024);
             byte[] bb = new byte[1024];
             OutputStream os = t.getResponseBody();
--- a/test/jdk/java/net/httpclient/http2/server/Http2TestExchange.java	Mon May 28 17:22:37 2018 +0100
+++ b/test/jdk/java/net/httpclient/http2/server/Http2TestExchange.java	Fri May 25 16:13:11 2018 +0100
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2017, 2018, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -26,16 +26,16 @@
 import java.io.OutputStream;
 import java.net.URI;
 import java.net.InetSocketAddress;
+import java.net.http.HttpHeaders;
 import java.util.concurrent.CompletableFuture;
-import java.util.function.Consumer;
 import javax.net.ssl.SSLSession;
-import jdk.internal.net.http.common.HttpHeadersImpl;
+import jdk.internal.net.http.common.HttpHeadersBuilder;
 
 public interface Http2TestExchange {
 
-    HttpHeadersImpl getRequestHeaders();
+    HttpHeaders getRequestHeaders();
 
-    HttpHeadersImpl getResponseHeaders();
+    HttpHeadersBuilder getResponseHeaders();
 
     URI getRequestURI();
 
@@ -61,7 +61,7 @@
 
     boolean serverPushAllowed();
 
-    void serverPush(URI uri, HttpHeadersImpl headers, InputStream content);
+    void serverPush(URI uri, HttpHeaders headers, InputStream content);
 
     /**
      * Send a PING on this exchanges connection, and completes the returned CF
--- a/test/jdk/java/net/httpclient/http2/server/Http2TestExchangeImpl.java	Mon May 28 17:22:37 2018 +0100
+++ b/test/jdk/java/net/httpclient/http2/server/Http2TestExchangeImpl.java	Fri May 25 16:13:11 2018 +0100
@@ -26,17 +26,19 @@
 import java.io.IOException;
 import java.net.URI;
 import java.net.InetSocketAddress;
+import java.net.http.HttpHeaders;
+import java.util.List;
+import java.util.Map;
 import java.util.concurrent.CompletableFuture;
-import java.util.function.Consumer;
 import javax.net.ssl.SSLSession;
-import jdk.internal.net.http.common.HttpHeadersImpl;
+import jdk.internal.net.http.common.HttpHeadersBuilder;
 import jdk.internal.net.http.frame.HeaderFrame;
 import jdk.internal.net.http.frame.HeadersFrame;
 
 public class Http2TestExchangeImpl implements Http2TestExchange {
 
-    final HttpHeadersImpl reqheaders;
-    final HttpHeadersImpl rspheaders;
+    final HttpHeaders reqheaders;
+    final HttpHeadersBuilder rspheadersBuilder;
     final URI uri;
     final String method;
     final InputStream is;
@@ -52,8 +54,8 @@
 
     Http2TestExchangeImpl(int streamid,
                           String method,
-                          HttpHeadersImpl reqheaders,
-                          HttpHeadersImpl rspheaders,
+                          HttpHeaders reqheaders,
+                          HttpHeadersBuilder rspheadersBuilder,
                           URI uri,
                           InputStream is,
                           SSLSession sslSession,
@@ -61,7 +63,7 @@
                           Http2TestServerConnection conn,
                           boolean pushAllowed) {
         this.reqheaders = reqheaders;
-        this.rspheaders = rspheaders;
+        this.rspheadersBuilder = rspheadersBuilder;
         this.uri = uri;
         this.method = method;
         this.is = is;
@@ -74,7 +76,7 @@
     }
 
     @Override
-    public HttpHeadersImpl getRequestHeaders() {
+    public HttpHeaders getRequestHeaders() {
         return reqheaders;
     }
 
@@ -84,8 +86,8 @@
     }
 
     @Override
-    public HttpHeadersImpl getResponseHeaders() {
-        return rspheaders;
+    public HttpHeadersBuilder getResponseHeaders() {
+        return rspheadersBuilder;
     }
 
     @Override
@@ -129,13 +131,14 @@
         this.responseLength = responseLength;
         if (responseLength > 0 || responseLength < 0) {
                 long clen = responseLength > 0 ? responseLength : 0;
-            rspheaders.setHeader("Content-length", Long.toString(clen));
+            rspheadersBuilder.setHeader("Content-length", Long.toString(clen));
         }
 
-        rspheaders.setHeader(":status", Integer.toString(rCode));
+        rspheadersBuilder.setHeader(":status", Integer.toString(rCode));
+        HttpHeaders headers = rspheadersBuilder.build();
 
         Http2TestServerConnection.ResponseHeaders response
-                = new Http2TestServerConnection.ResponseHeaders(rspheaders);
+                = new Http2TestServerConnection.ResponseHeaders(headers);
         response.streamid(streamid);
         response.setFlag(HeaderFrame.END_HEADERS);
 
@@ -175,13 +178,19 @@
     }
 
     @Override
-    public void serverPush(URI uri, HttpHeadersImpl headers, InputStream content) {
-        OutgoingPushPromise pp = new OutgoingPushPromise(
-                streamid, uri, headers, content);
-        headers.setHeader(":method", "GET");
-        headers.setHeader(":scheme", uri.getScheme());
-        headers.setHeader(":authority", uri.getAuthority());
-        headers.setHeader(":path", uri.getPath());
+    public void serverPush(URI uri, HttpHeaders headers, InputStream content) {
+        HttpHeadersBuilder headersBuilder = new HttpHeadersBuilder();
+        headersBuilder.setHeader(":method", "GET");
+        headersBuilder.setHeader(":scheme", uri.getScheme());
+        headersBuilder.setHeader(":authority", uri.getAuthority());
+        headersBuilder.setHeader(":path", uri.getPath());
+        for (Map.Entry<String,List<String>> entry : headers.map().entrySet()) {
+            for (String value : entry.getValue())
+                headersBuilder.addHeader(entry.getKey(), value);
+        }
+        HttpHeaders combinedHeaders = headersBuilder.build();
+        OutgoingPushPromise pp = new OutgoingPushPromise(streamid, uri, combinedHeaders, content);
+
         try {
             conn.outputQ.put(pp);
             // writeLoop will spin up thread to read the InputStream
--- a/test/jdk/java/net/httpclient/http2/server/Http2TestExchangeSupplier.java	Mon May 28 17:22:37 2018 +0100
+++ b/test/jdk/java/net/httpclient/http2/server/Http2TestExchangeSupplier.java	Fri May 25 16:13:11 2018 +0100
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2017, 2018, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -24,7 +24,8 @@
 import javax.net.ssl.SSLSession;
 import java.io.InputStream;
 import java.net.URI;
-import jdk.internal.net.http.common.HttpHeadersImpl;
+import java.net.http.HttpHeaders;
+import jdk.internal.net.http.common.HttpHeadersBuilder;
 
 /**
  * A supplier of Http2TestExchanges. If the default Http2TestExchange impl is
@@ -39,8 +40,8 @@
 
     Http2TestExchange get(int streamid,
                           String method,
-                          HttpHeadersImpl reqheaders,
-                          HttpHeadersImpl rspheaders,
+                          HttpHeaders reqheaders,
+                          HttpHeadersBuilder rspheadersBuilder,
                           URI uri,
                           InputStream is,
                           SSLSession sslSession,
--- a/test/jdk/java/net/httpclient/http2/server/Http2TestServerConnection.java	Mon May 28 17:22:37 2018 +0100
+++ b/test/jdk/java/net/httpclient/http2/server/Http2TestServerConnection.java	Fri May 25 16:13:11 2018 +0100
@@ -33,14 +33,14 @@
 import java.net.InetAddress;
 import javax.net.ssl.*;
 import java.net.URISyntaxException;
+import java.net.http.HttpHeaders;
 import java.nio.ByteBuffer;
-import java.nio.charset.StandardCharsets;
 import java.util.*;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.function.Consumer;
-import jdk.internal.net.http.common.HttpHeadersImpl;
+import jdk.internal.net.http.common.HttpHeadersBuilder;
 import jdk.internal.net.http.frame.*;
 import jdk.internal.net.http.hpack.Decoder;
 import jdk.internal.net.http.hpack.DecodingCallback;
@@ -455,11 +455,11 @@
         outputQ.put(wup);
     }
 
-    HttpHeadersImpl decodeHeaders(List<HeaderFrame> frames) throws IOException {
-        HttpHeadersImpl headers = createNewResponseHeaders();
+    HttpHeaders decodeHeaders(List<HeaderFrame> frames) throws IOException {
+        HttpHeadersBuilder headersBuilder = createNewHeadersBuilder();
 
         DecodingCallback cb = (name, value) -> {
-            headers.addHeader(name.toString(), value.toString());
+            headersBuilder.addHeader(name.toString(), value.toString());
         };
 
         for (HeaderFrame frame : frames) {
@@ -469,7 +469,7 @@
             }
         }
         hpackIn.decode(EMPTY_BUFFER, true, cb);
-        return headers;
+        return headersBuilder.build();
     }
 
     String getRequestLine(String request) {
@@ -486,8 +486,8 @@
         return request.substring(start,end);
     }
 
-    void addHeaders(String headers, HttpHeadersImpl hdrs) {
-        String[] hh = headers.split(CRLF);
+    static void addHeaders(String headersString, HttpHeadersBuilder headersBuilder) {
+        String[] hh = headersString.split(CRLF);
         for (String header : hh) {
             int colon = header.indexOf(':');
             if (colon == -1)
@@ -496,14 +496,14 @@
             String value = header.substring(colon+1);
             while (value.startsWith(" "))
                 value = value.substring(1);
-            hdrs.addHeader(name, value);
+            headersBuilder.addHeader(name, value);
         }
     }
 
     // First stream (1) comes from a plaintext HTTP/1.1 request
     @SuppressWarnings({"rawtypes","unchecked"})
     void createPrimordialStream(Http1InitialRequest request) throws IOException {
-        HttpHeadersImpl headers = createNewResponseHeaders();
+        HttpHeadersBuilder headersBuilder = createNewHeadersBuilder();
         String requestLine = getRequestLine(request.headers);
         String[] tokens = requestLine.split(" ");
         if (!tokens[2].equals("HTTP/1.1")) {
@@ -520,18 +520,19 @@
             throw new IOException("missing Host");
         }
 
-        headers.setHeader(":method", tokens[0]);
-        headers.setHeader(":scheme", "http"); // always in this case
-        headers.setHeader(":authority", host);
+        headersBuilder.setHeader(":method", tokens[0]);
+        headersBuilder.setHeader(":scheme", "http"); // always in this case
+        headersBuilder.setHeader(":authority", host);
         String path = uri.getRawPath();
         if (uri.getRawQuery() != null)
             path = path + "?" + uri.getRawQuery();
-        headers.setHeader(":path", path);
+        headersBuilder.setHeader(":path", path);
 
         Queue q = new Queue(sentinel);
         byte[] body = getRequestBody(request);
-        addHeaders(getHeaders(request.headers), headers);
-        headers.setHeader("Content-length", Integer.toString(body.length));
+        addHeaders(getHeaders(request.headers), headersBuilder);
+        headersBuilder.setHeader("Content-length", Integer.toString(body.length));
+        HttpHeaders headers = headersBuilder.build();
 
         addRequestBodyToQueue(body, q);
         streams.put(1, q);
@@ -569,7 +570,7 @@
             }
         }
         boolean endStreamReceived = endStream;
-        HttpHeadersImpl headers = decodeHeaders(frames);
+        HttpHeaders headers = decodeHeaders(frames);
 
         // Strict to assert Client correctness. Not all servers are as strict,
         // but some are known to be.
@@ -593,7 +594,7 @@
     // for this stream/request delivered on Q
 
     @SuppressWarnings({"rawtypes","unchecked"})
-    void handleRequest(HttpHeadersImpl headers,
+    void handleRequest(HttpHeaders headers,
                        Queue queue,
                        int streamid,
                        boolean endStreamReceived)
@@ -607,7 +608,6 @@
         String authority = headers.firstValue(":authority").orElse("");
         //System.out.println("authority = " + authority);
         System.err.printf("TestServer: %s %s\n", method, path);
-        HttpHeadersImpl rspheaders = createNewResponseHeaders();
         int winsize = clientSettings.getParameter(
                 SettingsFrame.INITIAL_WINDOW_SIZE);
         //System.err.println ("Stream window size = " + winsize);
@@ -627,8 +627,9 @@
             String us = scheme + "://" + authority + path;
             URI uri = new URI(us);
             boolean pushAllowed = clientSettings.getParameter(SettingsFrame.ENABLE_PUSH) == 1;
+            HttpHeadersBuilder rspheadersBuilder = createNewHeadersBuilder();
             Http2TestExchange exchange = exchangeSupplier.get(streamid, method,
-                    headers, rspheaders, uri, bis, getSSLSession(),
+                    headers, rspheadersBuilder, uri, bis, getSSLSession(),
                     bos, this, pushAllowed);
 
             // give to user
@@ -655,8 +656,8 @@
         }
     }
 
-    protected HttpHeadersImpl createNewResponseHeaders() {
-        return new HttpHeadersImpl();
+    protected HttpHeadersBuilder createNewHeadersBuilder() {
+        return new HttpHeadersBuilder();
     }
 
     private SSLSession getSSLSession() {
@@ -745,7 +746,8 @@
         }
     }
 
-    List<ByteBuffer> encodeHeaders(HttpHeadersImpl headers) {
+    /** Encodes an group of headers, without any ordering guarantees. */
+    List<ByteBuffer> encodeHeaders(HttpHeaders headers) {
         List<ByteBuffer> buffers = new LinkedList<>();
 
         ByteBuffer buf = getBuffer();
@@ -770,6 +772,30 @@
         return buffers;
     }
 
+    /** Encodes an ordered list of headers. */
+    List<ByteBuffer> encodeHeadersOrdered(List<Map.Entry<String,String>> headers) {
+        List<ByteBuffer> buffers = new LinkedList<>();
+
+        ByteBuffer buf = getBuffer();
+        boolean encoded;
+        for (Map.Entry<String, String> entry : headers) {
+            String value = entry.getValue();
+            String key = entry.getKey().toLowerCase();
+            do {
+                hpackOut.header(key, value);
+                encoded = hpackOut.encode(buf);
+                if (!encoded) {
+                    buf.flip();
+                    buffers.add(buf);
+                    buf = getBuffer();
+                }
+            } while (!encoded);
+        }
+        buf.flip();
+        buffers.add(buf);
+        return buffers;
+    }
+
     static void closeIgnore(Closeable c) {
         try {
             c.close();
@@ -847,9 +873,9 @@
     // returns a minimal response with status 200
     // that is the response to the push promise just sent
     private ResponseHeaders getPushResponse(int streamid) {
-        HttpHeadersImpl h = createNewResponseHeaders();
-        h.addHeader(":status", "200");
-        ResponseHeaders oh = new ResponseHeaders(h);
+        HttpHeadersBuilder hb = createNewHeadersBuilder();
+        hb.addHeader(":status", "200");
+        ResponseHeaders oh = new ResponseHeaders(hb.build());
         oh.streamid(streamid);
         oh.setFlag(HeaderFrame.END_HEADERS);
         return oh;
@@ -1082,9 +1108,9 @@
     // for the hashmap.
 
     static class ResponseHeaders extends Http2Frame {
-        HttpHeadersImpl headers;
+        HttpHeaders headers;
 
-        ResponseHeaders(HttpHeadersImpl headers) {
+        ResponseHeaders(HttpHeaders headers) {
             super(0, 0);
             this.headers = headers;
         }
--- a/test/jdk/java/net/httpclient/http2/server/OutgoingPushPromise.java	Mon May 28 17:22:37 2018 +0100
+++ b/test/jdk/java/net/httpclient/http2/server/OutgoingPushPromise.java	Fri May 25 16:13:11 2018 +0100
@@ -21,22 +21,22 @@
  * questions.
  */
 
-import java.io.*;
-import java.net.*;
-import jdk.internal.net.http.common.HttpHeadersImpl;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.http.HttpHeaders;
 import jdk.internal.net.http.frame.Http2Frame;
 
 // will be converted to a PushPromiseFrame in the writeLoop
 // a thread is then created to produce the DataFrames from the InputStream
 class OutgoingPushPromise extends Http2Frame {
-    final HttpHeadersImpl headers;
+    final HttpHeaders headers;
     final URI uri;
     final InputStream is;
     final int parentStream; // not the pushed streamid
 
     public OutgoingPushPromise(int parentStream,
                                URI uri,
-                               HttpHeadersImpl headers,
+                               HttpHeaders headers,
                                InputStream is) {
         super(0,0);
         this.uri = uri;
--- a/test/jdk/java/net/httpclient/http2/server/PushHandler.java	Mon May 28 17:22:37 2018 +0100
+++ b/test/jdk/java/net/httpclient/http2/server/PushHandler.java	Fri May 25 16:13:11 2018 +0100
@@ -23,11 +23,16 @@
 
 import java.io.*;
 import java.net.*;
+import java.net.http.HttpHeaders;
 import java.nio.file.*;
-import jdk.internal.net.http.common.HttpHeadersImpl;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiPredicate;
 
 public class PushHandler implements Http2Handler {
 
+    static final BiPredicate<String,String> ACCEPT_ALL = (x, y) -> true;
+
     final Path tempFile;
     final int loops;
     final long file_size;
@@ -50,8 +55,8 @@
                 for (int i=0; i<loops; i++) {
                     InputStream is = new FileInputStream(tempFile.toFile());
                     URI u = requestURI.resolve("/x/y/z/" + Integer.toString(i));
-                    HttpHeadersImpl h = new HttpHeadersImpl();
-                    h.addHeader("X-foo", "bar");
+                    HttpHeaders h = HttpHeaders.of(Map.of("X-foo", List.of("bar")),
+                                                   ACCEPT_ALL);
                     ee.serverPush(u, h, is);
                 }
                 System.err.println ("Server: sent all pushes");
--- a/test/jdk/java/net/httpclient/offline/FixedHttpHeaders.java	Mon May 28 17:22:37 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,60 +0,0 @@
-/*
- * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.TreeMap;
-import java.net.http.HttpHeaders;
-
-/**
- * An HttpHeaders consisting of the given name value pairs.
- */
-public class FixedHttpHeaders extends HttpHeaders {
-
-    private final Map<String, List<String>> map =
-            new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
-
-    @Override
-    public Map<String, List<String>> map() {
-        return map;
-    }
-
-    /**
-     * Creates an HttpHeaders of the given name value pairs.
-     */
-    public static HttpHeaders of(String... params) {
-        Objects.requireNonNull(params);
-        if ((params.length & 0x1) != 0)
-            throw new IllegalArgumentException("odd number of params");
-        FixedHttpHeaders headers = new FixedHttpHeaders();
-        for (int i = 0; i < params.length; i += 2) {
-            String name = params[i];
-            String value = params[i + 1];
-            headers.map.computeIfAbsent(name, k -> new ArrayList<>(1))
-                       .add(value);
-        }
-        return headers;
-    }
-}
--- a/test/jdk/java/net/httpclient/offline/OfflineTesting.java	Mon May 28 17:22:37 2018 +0100
+++ b/test/jdk/java/net/httpclient/offline/OfflineTesting.java	Fri May 25 16:13:11 2018 +0100
@@ -24,19 +24,26 @@
 /*
  * @test
  * @summary Demonstrates how to achieve testing without network connections
- * @build FixedHttpHeaders DelegatingHttpClient FixedHttpResponse FixedResponseHttpClient
+ * @build DelegatingHttpClient FixedHttpResponse FixedResponseHttpClient
  * @run testng/othervm OfflineTesting
  */
 
 import java.io.IOException;
 import java.net.URI;
 import java.net.http.HttpClient;
+import java.net.http.HttpHeaders;
 import java.net.http.HttpRequest;
 import java.net.http.HttpRequest.BodyPublishers;
 import java.net.http.HttpResponse;
 import java.net.http.HttpResponse.BodyHandlers;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiPredicate;
 import org.testng.annotations.Test;
+import static java.lang.String.format;
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Objects.requireNonNull;
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertTrue;
 import static org.testng.Assert.fail;
@@ -49,8 +56,8 @@
         return FixedResponseHttpClient.createClientFrom(
                 HttpClient.newBuilder(),
                 200,
-                FixedHttpHeaders.of("Server",  "nginx",
-                                    "Content-Type", "text/html"),
+                headersOf("Server", "nginx",
+                          "Content-Type", "text/html"),
                 "A response message");
     }
 
@@ -94,11 +101,11 @@
         HttpClient client = FixedResponseHttpClient.createClientFrom(
                 HttpClient.newBuilder(),
                 404,
-                FixedHttpHeaders.of("Connection",  "keep-alive",
-                                    "Content-Length", "162",
-                                    "Content-Type", "text/html",
-                                    "Date", "Mon, 15 Jan 2018 15:01:16 GMT",
-                                    "Server", "nginx"),
+                headersOf("Connection",  "keep-alive",
+                          "Content-Length", "162",
+                          "Content-Type", "text/html",
+                          "Date", "Mon, 15 Jan 2018 15:01:16 GMT",
+                          "Server", "nginx"),
                 "<html>\n" +
                 "<head><title>404 Not Found</title></head>\n" +
                 "<body bgcolor=\"white\">\n" +
@@ -126,7 +133,7 @@
         HttpClient client = FixedResponseHttpClient.createEchoClient(
                 HttpClient.newBuilder(),
                 200,
-                FixedHttpHeaders.of("Connection",  "keep-alive"));
+                headersOf("Connection",  "keep-alive"));
 
         HttpRequest request = HttpRequest.newBuilder()
                 .uri(URI.create("http://openjdk.java.net/echo"))
@@ -146,7 +153,7 @@
         HttpClient client = FixedResponseHttpClient.createEchoClient(
                 HttpClient.newBuilder(),
                 200,
-                FixedHttpHeaders.of("Connection",  "keep-alive"));
+                headersOf("Connection",  "keep-alive"));
 
         HttpRequest request = HttpRequest.newBuilder()
                 .uri(URI.create("http://openjdk.java.net/echo"))
@@ -158,4 +165,26 @@
         assertEquals(response.statusCode(), 200);
         assertEquals(response.body(), "Hello chegar!!");
     }
+
+    // ---
+
+    public static IllegalArgumentException newIAE(String message, Object... args) {
+        return new IllegalArgumentException(format(message, args));
+    }
+
+    static final BiPredicate<String,String> ACCEPT_ALL = (x, y) -> true;
+
+    static HttpHeaders headersOf(String... params) {
+        Map<String,List<String>> map = new HashMap<>();
+        requireNonNull(params);
+        if (params.length == 0 || params.length % 2 != 0) {
+            throw newIAE("wrong number, %d, of parameters", params.length);
+        }
+        for (int i = 0; i < params.length; i += 2) {
+            String name  = params[i];
+            String value = params[i + 1];
+            map.put(name, List.of(value));
+        }
+        return HttpHeaders.of(map, ACCEPT_ALL);
+    }
 }
--- a/test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/AuthenticationFilterTest.java	Mon May 28 17:22:37 2018 +0100
+++ b/test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/AuthenticationFilterTest.java	Fri May 25 16:13:11 2018 +0100
@@ -23,7 +23,7 @@
 
 package jdk.internal.net.http;
 
-import jdk.internal.net.http.common.HttpHeadersImpl;
+import jdk.internal.net.http.common.HttpHeadersBuilder;
 import org.testng.annotations.DataProvider;
 import org.testng.annotations.Test;
 import org.testng.annotations.AfterClass;
@@ -35,17 +35,19 @@
 import java.net.ProxySelector;
 import java.net.URI;
 import java.net.URL;
-import java.net.http.HttpClient;
 import java.net.http.HttpHeaders;
 import java.net.http.HttpResponse;
 import java.net.http.HttpResponse.BodyHandlers;
 import java.security.AccessController;
 import java.util.Arrays;
 import java.util.Base64;
+import java.util.Collections;
+import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.atomic.AtomicLong;
 import java.net.http.HttpClient.Version;
+import java.util.function.BiPredicate;
 
 import static java.lang.String.format;
 import static java.lang.System.out;
@@ -163,6 +165,8 @@
         });
     }
 
+    static final BiPredicate<String,String> ACCEPT_ALL = (x, y) -> true;
+
     private void doTestAuthentication(String uri, Version v, String proxy) throws Exception {
         int colon = proxy == null ? -1 : proxy.lastIndexOf(":");
         ProxySelector ps = proxy == null ? NO_PROXY
@@ -197,8 +201,9 @@
         Exchange<?> exchange = new Exchange<>(req, multi);
         out.println("\nSimulating unauthenticated request to " + uri);
         filter.request(req, multi);
-        assertFalse(req.getSystemHeaders().firstValue(authorization(true)).isPresent());
-        assertFalse(req.getSystemHeaders().firstValue(authorization(false)).isPresent());
+        HttpHeaders hdrs = req.getSystemHeadersBuilder().build();
+        assertFalse(hdrs.firstValue(authorization(true)).isPresent());
+        assertFalse(hdrs.firstValue(authorization(false)).isPresent());
         assertEquals(authenticator.COUNTER.get(), 0);
 
         // Creates the Response to the first request, and call filter.response
@@ -207,9 +212,10 @@
         // credentials, and will also cache the credentials in the multi exchange.
         // The credentials shouldn't be put in the cache until the 200 response
         // for that request arrives.
-        HttpHeadersImpl headers = new HttpHeadersImpl();
-        headers.addHeader(authenticate(proxy!=null),
-                "Basic realm=\"earth\"");
+        HttpHeadersBuilder headersBuilder = new HttpHeadersBuilder();
+        headersBuilder.addHeader(authenticate(proxy!=null),
+                                "Basic realm=\"earth\"");
+        HttpHeaders headers = headersBuilder.build();
         Response response = new Response(req, exchange, headers, null, unauthorized, v);
         out.println("Simulating " + unauthorized
                 + " response from " + uri);
@@ -218,7 +224,7 @@
         out.println("Checking filter's response to "
                 + unauthorized + " from " + uri);
         assertTrue(next != null, "next should not be null");
-        String[] up = check(reqURI, next.getSystemHeaders(), proxy);
+        String[] up = check(reqURI, next.getSystemHeadersBuilder().build(), proxy);
         assertEquals(authenticator.COUNTER.get(), 1);
 
         // Now simulate a new successful exchange to get the credentials in the cache
@@ -230,10 +236,12 @@
         exchange = new Exchange<>(next, multi);
         filter.request(next, multi);
         out.println("Checking credentials in request header after filter for " + uri);
-        check(reqURI, next.getSystemHeaders(), proxy);
-        check(next.uri(), next.getSystemHeaders(), proxy);
+        hdrs = next.getSystemHeadersBuilder().build();
+        check(reqURI, hdrs, proxy);
+        check(next.uri(), hdrs, proxy);
         out.println("Simulating  successful response 200 from " + uri);
-        response = new Response(next, exchange, new HttpHeadersImpl(), null, 200, v);
+        HttpHeaders h = HttpHeaders.of(Collections.emptyMap(), ACCEPT_ALL);
+        response = new Response(next, exchange,h, null, 200, v);
         next = filter.response(response);
         assertTrue(next == null, "next should be null");
         assertEquals(authenticator.COUNTER.get(), 1);
@@ -263,7 +271,7 @@
         filter.request(req2, multi2);
         out.println("Check that filter has added credentials from cache for " + reqURI2
                 + " with proxy " + req2.proxy());
-        String[] up2 = check(reqURI, req2.getSystemHeaders(), proxy);
+        String[] up2 = check(reqURI, req2.getSystemHeadersBuilder().build(), proxy);
         assertTrue(Arrays.deepEquals(up, up2), format("%s:%s != %s:%s", up2[0], up2[1], up[0], up[1]));
         assertEquals(authenticator.COUNTER.get(), 1);
 
@@ -293,24 +301,25 @@
                 HttpResponse.BodyHandlers.replacing(null),
                 null, AccessController.getContext());
         filter.request(req3, multi3);
+        HttpHeaders h3 = req3.getSystemHeadersBuilder().build();
         if (proxy == null) {
             out.println("Check that filter has not added proxy credentials from cache for " + reqURI3);
-            assert !req3.getSystemHeaders().firstValue(authorization(true)).isPresent()
+            assert !h3.firstValue(authorization(true)).isPresent()
                     : format("Unexpected proxy credentials found: %s",
-                    java.util.stream.Stream.of(getAuthorization(req3.getSystemHeaders(), true))
+                    java.util.stream.Stream.of(getAuthorization(req3.getSystemHeadersBuilder().build(), true))
                             .collect(joining(":")));
-            assertFalse(req3.getSystemHeaders().firstValue(authorization(true)).isPresent());
+            assertFalse(h3.firstValue(authorization(true)).isPresent());
         } else {
             out.println("Check that filter has added proxy credentials from cache for " + reqURI3);
-            String[] up3 = check(reqURI, req3.getSystemHeaders(), proxy);
+            String[] up3 = check(reqURI, h3, proxy);
             assertTrue(Arrays.deepEquals(up, up3), format("%s:%s != %s:%s", up3[0], up3[1], up[0], up[1]));
         }
         out.println("Check that filter has not added server credentials from cache for " + reqURI3);
-        assert !req3.getSystemHeaders().firstValue(authorization(false)).isPresent()
+        assert !h3.firstValue(authorization(false)).isPresent()
                 : format("Unexpected server credentials found: %s",
-                java.util.stream.Stream.of(getAuthorization(req3.getSystemHeaders(), false))
+                java.util.stream.Stream.of(getAuthorization(h3, false))
                         .collect(joining(":")));
-        assertFalse(req3.getSystemHeaders().firstValue(authorization(false)).isPresent());
+        assertFalse(h3.firstValue(authorization(false)).isPresent());
         assertEquals(authenticator.COUNTER.get(), 1);
 
         // Now we will verify that credentials for proxies are not used for servers and
@@ -341,23 +350,24 @@
         filter.request(req4, multi4);
         out.println("Check that filter has not added proxy credentials from cache for "
                 + reqURI4 + " (proxy: " + req4.proxy()  + ")");
-        assert !req4.getSystemHeaders().firstValue(authorization(true)).isPresent()
+        HttpHeaders h4 = req4.getSystemHeadersBuilder().build();
+        assert !h4.firstValue(authorization(true)).isPresent()
                 : format("Unexpected proxy credentials found: %s",
-                java.util.stream.Stream.of(getAuthorization(req4.getSystemHeaders(), true))
+                java.util.stream.Stream.of(getAuthorization(h4, true))
                         .collect(joining(":")));
-        assertFalse(req4.getSystemHeaders().firstValue(authorization(true)).isPresent());
+        assertFalse(h4.firstValue(authorization(true)).isPresent());
         if (proxy != null) {
             out.println("Check that filter has not added server credentials from cache for "
                     + reqURI4 + " (proxy: " + req4.proxy()  + ")");
-            assert !req4.getSystemHeaders().firstValue(authorization(false)).isPresent()
+            assert !h4.firstValue(authorization(false)).isPresent()
                     : format("Unexpected server credentials found: %s",
-                    java.util.stream.Stream.of(getAuthorization(req4.getSystemHeaders(), false))
+                    java.util.stream.Stream.of(getAuthorization(h4, false))
                             .collect(joining(":")));
-            assertFalse(req4.getSystemHeaders().firstValue(authorization(false)).isPresent());
+            assertFalse(h4.firstValue(authorization(false)).isPresent());
         } else {
             out.println("Check that filter has added server credentials from cache for "
                     + reqURI4 + " (proxy: " + req4.proxy()  + ")");
-            String[] up4 = check(reqURI, req4.getSystemHeaders(), proxy);
+            String[] up4 = check(reqURI, h4, proxy);
             assertTrue(Arrays.deepEquals(up, up4),  format("%s:%s != %s:%s", up4[0], up4[1], up[0], up[1]));
         }
         assertEquals(authenticator.COUNTER.get(), 1);
@@ -381,24 +391,26 @@
             filter.request(req5, multi5);
             out.println("Check that filter has not added server credentials from cache for "
                     + reqURI + " (proxy: " + req5.proxy()  + ")");
-            assert !req5.getSystemHeaders().firstValue(authorization(false)).isPresent()
+            HttpHeaders h5 = req5.getSystemHeadersBuilder().build();
+            assert !h5.firstValue(authorization(false)).isPresent()
                     : format("Unexpected server credentials found: %s",
-                    java.util.stream.Stream.of(getAuthorization(req5.getSystemHeaders(), false))
+                    java.util.stream.Stream.of(getAuthorization(h5, false))
                             .collect(joining(":")));
-            assertFalse(req5.getSystemHeaders().firstValue(authorization(false)).isPresent());
+            assertFalse(h5.firstValue(authorization(false)).isPresent());
             out.println("Check that filter has not added proxy credentials from cache for "
                     + reqURI + " (proxy: " + req5.proxy()  + ")");
-            assert !req5.getSystemHeaders().firstValue(authorization(true)).isPresent()
+            assert !h5.firstValue(authorization(true)).isPresent()
                     : format("Unexpected proxy credentials found: %s",
-                    java.util.stream.Stream.of(getAuthorization(req5.getSystemHeaders(), true))
+                    java.util.stream.Stream.of(getAuthorization(h5, true))
                             .collect(joining(":")));
-            assertFalse(req5.getSystemHeaders().firstValue(authorization(true)).isPresent());
+            assertFalse(h5.firstValue(authorization(true)).isPresent());
             assertEquals(authenticator.COUNTER.get(), 1);
 
             // Now simulate a 401 response from the server
-            HttpHeadersImpl headers5 = new HttpHeadersImpl();
-            headers5.addHeader(authenticate(false),
-                    "Basic realm=\"earth\"");
+            HttpHeadersBuilder headers5Builder = new HttpHeadersBuilder();
+            headers5Builder.addHeader(authenticate(false),
+                               "Basic realm=\"earth\"");
+            HttpHeaders headers5 = headers5Builder.build();
             unauthorized = 401;
             Response response5 = new Response(req5, exchange5, headers5, null, unauthorized, v);
             out.println("Simulating " + unauthorized
@@ -409,12 +421,13 @@
             out.println("Checking filter's response to "
                     + unauthorized + " from " + uri);
             assertTrue(next5 != null, "next5 should not be null");
-            String[] up5 = check(reqURI, next5.getSystemHeaders(), null);
+            String[] up5 = check(reqURI, next5.getSystemHeadersBuilder().build(), null);
 
             // now simulate a 200 response from the server
             exchange5 = new Exchange<>(next5, multi5);
             filter.request(next5, multi5);
-            response5 = new Response(next5, exchange5, new HttpHeadersImpl(), null, 200, v);
+            h = HttpHeaders.of(Map.of(), ACCEPT_ALL);
+            response5 = new Response(next5, exchange5, h, null, 200, v);
             filter.response(response5);
             assertEquals(authenticator.COUNTER.get(), 2);
 
@@ -432,10 +445,11 @@
             filter.request(req6, multi6);
             out.println("Check that filter has added server credentials from cache for "
                     + reqURI + " (proxy: " + req6.proxy()  + ")");
-            check(reqURI, req6.getSystemHeaders(), null);
+            HttpHeaders h6 = req6.getSystemHeadersBuilder().build();
+            check(reqURI, h6, null);
             out.println("Check that filter has added proxy credentials from cache for "
                     + reqURI + " (proxy: " + req6.proxy()  + ")");
-            String[] up6 = check(reqURI, req6.getSystemHeaders(), proxy);
+            String[] up6 = check(reqURI, h6, proxy);
             assertTrue(Arrays.deepEquals(up, up6), format("%s:%s != %s:%s", up6[0], up6[1], up[0], up[1]));
             assertEquals(authenticator.COUNTER.get(), 2);
         }
@@ -456,18 +470,19 @@
             out.println("Check that filter has not added server credentials from cache for "
                     + reqURI7 + " (proxy: " + req7.proxy()  + ") [resolved uri: "
                     + reqURI7.resolve(".") + " should not match " + reqURI.resolve(".") + "]");
-            assert !req7.getSystemHeaders().firstValue(authorization(false)).isPresent()
+            HttpHeaders h7 = req7.getSystemHeadersBuilder().build();
+            assert !h7.firstValue(authorization(false)).isPresent()
                     : format("Unexpected server credentials found: %s",
-                    java.util.stream.Stream.of(getAuthorization(req7.getSystemHeaders(), false))
+                    java.util.stream.Stream.of(getAuthorization(h7, false))
                             .collect(joining(":")));
-            assertFalse(req7.getSystemHeaders().firstValue(authorization(false)).isPresent());
+            assertFalse(h7.firstValue(authorization(false)).isPresent());
             out.println("Check that filter has not added proxy credentials from cache for "
                     + reqURI7 + " (proxy: " + req7.proxy()  + ")");
-            assert !req7.getSystemHeaders().firstValue(authorization(true)).isPresent()
+            assert !h7.firstValue(authorization(true)).isPresent()
                     : format("Unexpected proxy credentials found: %s",
-                    java.util.stream.Stream.of(getAuthorization(req7.getSystemHeaders(), true))
+                    java.util.stream.Stream.of(getAuthorization(h7, true))
                             .collect(joining(":")));
-            assertFalse(req7.getSystemHeaders().firstValue(authorization(true)).isPresent());
+            assertFalse(h7.firstValue(authorization(true)).isPresent());
             assertEquals(authenticator.COUNTER.get(), 1);
 
         }