From 836d09034338c388c2a314e40a7f74e691ead48e Mon Sep 17 00:00:00 2001
From: Alexey Bakhtin <abakhtin@openjdk.org>
Date: Fri, 27 Sep 2024 09:18:37 -0700
Subject: [PATCH] 8328286: Enhance HTTP client

Reviewed-by: mbalao
Backport-of: cf8dc79f392c8ec3414d8b36803f026852c4e386
---
 .../java/net/doc-files/net-properties.html    |   9 +
 .../classes/sun/net/www/MessageHeader.java    |  55 ++++
 .../www/protocol/http/HttpURLConnection.java  |  23 +-
 src/java.base/share/conf/net.properties       |  17 ++
 .../jdk/internal/net/http/Exchange.java       |  38 ++-
 .../internal/net/http/Http1HeaderParser.java  |  49 ++-
 .../internal/net/http/Http2ClientImpl.java    |  79 +++--
 .../internal/net/http/Http2Connection.java    | 281 ++++++++++++++----
 .../jdk/internal/net/http/HttpClientImpl.java |   4 +-
 .../internal/net/http/HttpRequestImpl.java    |   4 +-
 .../net/http/ResponseBodyHandlers.java        |  12 +-
 .../classes/jdk/internal/net/http/Stream.java | 114 ++++++-
 .../net/http/common/HeaderDecoder.java        |   6 +-
 .../jdk/internal/net/http/common/Utils.java   |  13 +
 .../jdk/internal/net/http/hpack/Decoder.java  |  80 ++++-
 .../net/http/hpack/DecodingCallback.java      |  14 +
 .../jdk/internal/net/http/hpack/Encoder.java  |  14 +-
 .../classes/sun/net/httpserver/Request.java   |  35 ++-
 .../sun/net/httpserver/ServerConfig.java      |  18 +-
 .../http2/PushPromiseContinuation.java        |   9 +-
 .../test/lib/common/HttpServerAdapters.java   |  12 +-
 .../test/lib/http2/HpackTestEncoder.java      | 174 +++++++++++
 .../test/lib/http2/Http2TestExchange.java     |  13 +
 .../test/lib/http2/Http2TestExchangeImpl.java |  16 +-
 .../lib/http2/Http2TestServerConnection.java  | 158 +++++++---
 25 files changed, 1074 insertions(+), 173 deletions(-)
 create mode 100644 test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/HpackTestEncoder.java

diff --git a/src/java.base/share/classes/java/net/doc-files/net-properties.html b/src/java.base/share/classes/java/net/doc-files/net-properties.html
index b7ed2f9924f..2c10d18a46e 100644
--- a/src/java.base/share/classes/java/net/doc-files/net-properties.html
+++ b/src/java.base/share/classes/java/net/doc-files/net-properties.html
@@ -240,6 +240,15 @@ of proxies.</P>
 	</OL>
 	<P>The channel binding tokens generated are of the type "tls-server-end-point" as defined in
            RFC 5929.</P>
+
+	<LI><P><B>{@systemProperty jdk.http.maxHeaderSize}</B> (default: 393216 or 384kB)<BR>
+	This is the maximum header field section size that a client is prepared to accept.
+	This is computed as the sum of the size of the uncompressed header name, plus
+	the size of the uncompressed header value, plus an overhead of 32 bytes for
+	each field section line. If a peer sends a field section that exceeds this
+	size a {@link java.net.ProtocolException ProtocolException} will be raised.
+	This applies to all versions of the HTTP protocol. A value of zero or a negative
+	value means no limit. If left unspecified, the default value is 393216 bytes.
 </UL>
 <P>All these properties are checked only once at startup.</P>
 <a id="AddressCache"></a>
diff --git a/src/java.base/share/classes/sun/net/www/MessageHeader.java b/src/java.base/share/classes/sun/net/www/MessageHeader.java
index 22b16407dd2..4a60ab482c7 100644
--- a/src/java.base/share/classes/sun/net/www/MessageHeader.java
+++ b/src/java.base/share/classes/sun/net/www/MessageHeader.java
@@ -30,6 +30,8 @@
 package sun.net.www;
 
 import java.io.*;
+import java.lang.reflect.Array;
+import java.net.ProtocolException;
 import java.util.Collections;
 import java.util.*;
 
@@ -46,11 +48,32 @@ class MessageHeader {
     private String values[];
     private int nkeys;
 
+    // max number of bytes for headers, <=0 means unlimited;
+    // this corresponds to the length of the names, plus the length
+    // of the values, plus an overhead of 32 bytes per name: value
+    // pair.
+    // Note: we use the same definition as HTTP/2 SETTINGS_MAX_HEADER_LIST_SIZE
+    // see RFC 9113, section 6.5.2.
+    // https://www.rfc-editor.org/rfc/rfc9113.html#SETTINGS_MAX_HEADER_LIST_SIZE
+    private final int maxHeaderSize;
+
+    // Aggregate size of the field lines (name + value + 32) x N
+    // that have been parsed and accepted so far.
+    // This is defined as a long to force promotion to long
+    // and avoid overflows; see checkNewSize;
+    private long size;
+
     public MessageHeader () {
+       this(0);
+    }
+
+    public MessageHeader (int maxHeaderSize) {
+        this.maxHeaderSize = maxHeaderSize;
         grow();
     }
 
     public MessageHeader (InputStream is) throws java.io.IOException {
+        maxHeaderSize = 0;
         parseHeader(is);
     }
 
@@ -477,10 +500,28 @@ class MessageHeader {
     public void parseHeader(InputStream is) throws java.io.IOException {
         synchronized (this) {
             nkeys = 0;
+            size = 0;
         }
         mergeHeader(is);
     }
 
+    private void checkMaxHeaderSize(int sz) throws ProtocolException {
+        if (maxHeaderSize > 0) checkNewSize(size, sz, 0);
+    }
+
+    private long checkNewSize(long size, int name, int value) throws ProtocolException {
+        // See SETTINGS_MAX_HEADER_LIST_SIZE, RFC 9113, section 6.5.2.
+        long newSize = size + name + value + 32;
+        if (maxHeaderSize > 0 && newSize > maxHeaderSize) {
+            Arrays.fill(keys, 0, nkeys, null);
+            Arrays.fill(values,0, nkeys, null);
+            nkeys = 0;
+            throw new ProtocolException(String.format("Header size too big: %s > %s",
+                    newSize, maxHeaderSize));
+        }
+        return newSize;
+    }
+
     /** Parse and merge a MIME header from an input stream. */
     @SuppressWarnings("fallthrough")
     public void mergeHeader(InputStream is) throws java.io.IOException {
@@ -494,7 +535,15 @@ class MessageHeader {
             int c;
             boolean inKey = firstc > ' ';
             s[len++] = (char) firstc;
+            checkMaxHeaderSize(len);
     parseloop:{
+                // We start parsing for a new name value pair here.
+                // The max header size includes an overhead of 32 bytes per
+                // name value pair.
+                // See SETTINGS_MAX_HEADER_LIST_SIZE, RFC 9113, section 6.5.2.
+                long maxRemaining = maxHeaderSize > 0
+                        ? maxHeaderSize - size - 32
+                        : Long.MAX_VALUE;
                 while ((c = is.read()) >= 0) {
                     switch (c) {
                       case ':':
@@ -528,6 +577,9 @@ class MessageHeader {
                         s = ns;
                     }
                     s[len++] = (char) c;
+                    if (maxHeaderSize > 0 && len > maxRemaining) {
+                        checkMaxHeaderSize(len);
+                    }
                 }
                 firstc = -1;
             }
@@ -549,6 +601,9 @@ class MessageHeader {
                 v = new String();
             else
                 v = String.copyValueOf(s, keyend, len - keyend);
+            int klen = k == null ? 0 : k.length();
+
+            size = checkNewSize(size, klen, v.length());
             add(k, v);
         }
     }
diff --git a/src/java.base/share/classes/sun/net/www/protocol/http/HttpURLConnection.java b/src/java.base/share/classes/sun/net/www/protocol/http/HttpURLConnection.java
index 7dc9f99eb18..288ebfd8504 100644
--- a/src/java.base/share/classes/sun/net/www/protocol/http/HttpURLConnection.java
+++ b/src/java.base/share/classes/sun/net/www/protocol/http/HttpURLConnection.java
@@ -171,6 +171,8 @@ public class HttpURLConnection extends java.net.HttpURLConnection {
      */
     private static int bufSize4ES = 0;
 
+    private static final int maxHeaderSize;
+
     /*
      * Restrict setting of request headers through the public api
      * consistent with JavaScript XMLHttpRequest2 with a few
@@ -285,6 +287,19 @@ public class HttpURLConnection extends java.net.HttpURLConnection {
         } else {
             restrictedHeaderSet = null;
         }
+
+        int defMaxHeaderSize = 384 * 1024;
+        String maxHeaderSizeStr = getNetProperty("jdk.http.maxHeaderSize");
+        int maxHeaderSizeVal = defMaxHeaderSize;
+        if (maxHeaderSizeStr != null) {
+            try {
+                maxHeaderSizeVal = Integer.parseInt(maxHeaderSizeStr);
+            } catch (NumberFormatException n) {
+                maxHeaderSizeVal = defMaxHeaderSize;
+            }
+        }
+        if (maxHeaderSizeVal < 0) maxHeaderSizeVal = 0;
+        maxHeaderSize = maxHeaderSizeVal;
     }
 
     static final String httpVersion = "HTTP/1.1";
@@ -759,7 +774,7 @@ public class HttpURLConnection extends java.net.HttpURLConnection {
                 }
                 ps = (PrintStream) http.getOutputStream();
                 connected=true;
-                responses = new MessageHeader();
+                responses = new MessageHeader(maxHeaderSize);
                 setRequests=false;
                 writeRequests();
             }
@@ -917,7 +932,7 @@ public class HttpURLConnection extends java.net.HttpURLConnection {
             throws IOException {
         super(checkURL(u));
         requests = new MessageHeader();
-        responses = new MessageHeader();
+        responses = new MessageHeader(maxHeaderSize);
         userHeaders = new MessageHeader();
         this.handler = handler;
         instProxy = p;
@@ -2872,7 +2887,7 @@ public class HttpURLConnection extends java.net.HttpURLConnection {
         }
 
         // clear out old response headers!!!!
-        responses = new MessageHeader();
+        responses = new MessageHeader(maxHeaderSize);
         if (stat == HTTP_USE_PROXY) {
             /* This means we must re-request the resource through the
              * proxy denoted in the "Location:" field of the response.
@@ -3062,7 +3077,7 @@ public class HttpURLConnection extends java.net.HttpURLConnection {
             } catch (IOException e) { }
         }
         responseCode = -1;
-        responses = new MessageHeader();
+        responses = new MessageHeader(maxHeaderSize);
         connected = false;
     }
 
diff --git a/src/java.base/share/conf/net.properties b/src/java.base/share/conf/net.properties
index 67f294355a1..2aa9a9630be 100644
--- a/src/java.base/share/conf/net.properties
+++ b/src/java.base/share/conf/net.properties
@@ -130,3 +130,20 @@ jdk.http.auth.tunneling.disabledSchemes=Basic
 #jdk.http.ntlm.transparentAuth=trustedHosts
 #
 jdk.http.ntlm.transparentAuth=disabled
+
+#
+# Maximum HTTP field section size that a client is prepared to accept
+#
+# jdk.http.maxHeaderSize=393216
+#
+# This is the maximum header field section size that a client is prepared to accept.
+# This is computed as the sum of the size of the uncompressed header name, plus
+# the size of the uncompressed header value, plus an overhead of 32 bytes for
+# each field section line. If a peer sends a field section that exceeds this
+# size a {@link java.net.ProtocolException ProtocolException} will be raised.
+# This applies to all versions of the HTTP protocol. A value of zero or a negative
+# value means no limit. If left unspecified, the default value is 393216 bytes
+# or 384kB.
+#
+# Note: This property is currently used by the JDK Reference implementation. It
+# is not guaranteed to be examined and used by other implementations.
diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Exchange.java b/src/java.net.http/share/classes/jdk/internal/net/http/Exchange.java
index c2f8390e461..60d2b2b410a 100644
--- a/src/java.net.http/share/classes/jdk/internal/net/http/Exchange.java
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/Exchange.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2024, 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
@@ -40,6 +40,7 @@ import java.util.Map;
 import java.util.Optional;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.Function;
 import java.net.http.HttpClient;
 import java.net.http.HttpHeaders;
@@ -68,6 +69,8 @@ import static jdk.internal.net.http.common.Utils.permissionForProxy;
  */
 final class Exchange<T> {
 
+    static final int MAX_NON_FINAL_RESPONSES =
+            Utils.getIntegerNetProperty("jdk.httpclient.maxNonFinalResponses", 8);
     final Logger debug = Utils.getDebugLogger(this::dbgString, Utils.DEBUG);
 
     final HttpRequestImpl request;
@@ -92,6 +95,8 @@ final class Exchange<T> {
     // exchange so that it can be aborted/timed out mid setup.
     final ConnectionAborter connectionAborter = new ConnectionAborter();
 
+    final AtomicInteger nonFinalResponses = new AtomicInteger();
+
     Exchange(HttpRequestImpl request, MultiExchange<T> multi) {
         this.request = request;
         this.upgrading = false;
@@ -315,7 +320,7 @@ final class Exchange<T> {
 
     public void h2Upgrade() {
         upgrading = true;
-        request.setH2Upgrade(client.client2());
+        request.setH2Upgrade(this);
     }
 
     synchronized IOException getCancelCause() {
@@ -416,6 +421,7 @@ final class Exchange<T> {
             Log.logResponse(r1::toString);
             int rcode = r1.statusCode();
             if (rcode == 100) {
+                nonFinalResponses.incrementAndGet();
                 Log.logTrace("Received 100-Continue: sending body");
                 if (debug.on()) debug.log("Received 100-Continue for %s", r1);
                 CompletableFuture<Response> cf =
@@ -492,12 +498,20 @@ final class Exchange<T> {
                         + rsp.statusCode());
             }
             assert exchImpl != null : "Illegal state - current exchange isn't set";
-            // ignore this Response and wait again for the subsequent response headers
-            final CompletableFuture<Response> cf = exchImpl.getResponseAsync(parentExecutor);
-            // we recompose the CF again into the ignore1xxResponse check/function because
-            // the 1xx response is allowed to be sent multiple times for a request, before
-            // a final response arrives
-            return cf.thenCompose(this::ignore1xxResponse);
+            int count = nonFinalResponses.incrementAndGet();
+            if (MAX_NON_FINAL_RESPONSES > 0 && (count < 0 || count > MAX_NON_FINAL_RESPONSES)) {
+                return MinimalFuture.failedFuture(
+                        new ProtocolException(String.format(
+                                "Too many interim responses received: %s > %s",
+                                count, MAX_NON_FINAL_RESPONSES)));
+            } else {
+                // ignore this Response and wait again for the subsequent response headers
+                final CompletableFuture<Response> cf = exchImpl.getResponseAsync(parentExecutor);
+                // we recompose the CF again into the ignore1xxResponse check/function because
+                // the 1xx response is allowed to be sent multiple times for a request, before
+                // a final response arrives
+                return cf.thenCompose(this::ignore1xxResponse);
+            }
         } else {
             // return the already completed future
             return MinimalFuture.completedFuture(rsp);
@@ -759,6 +773,14 @@ final class Exchange<T> {
         return multi.version();
     }
 
+    boolean pushEnabled() {
+        return pushGroup != null;
+    }
+
+    String h2cSettingsStrings() {
+        return client.client2().getSettingsString(pushEnabled());
+    }
+
     String dbgString() {
         return dbgTag;
     }
diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http1HeaderParser.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http1HeaderParser.java
index 669c173e3f8..8c796193015 100644
--- a/src/java.net.http/share/classes/jdk/internal/net/http/Http1HeaderParser.java
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http1HeaderParser.java
@@ -25,6 +25,7 @@
 
 package jdk.internal.net.http;
 
+import java.io.IOException;
 import java.net.ProtocolException;
 import java.nio.BufferUnderflowException;
 import java.nio.ByteBuffer;
@@ -53,6 +54,12 @@ class Http1HeaderParser {
     private int responseCode;
     private HttpHeaders headers;
     private Map<String,List<String>> privateMap = new HashMap<>();
+    private long size;
+
+    private static final int K = 1024;
+    private static  final int MAX_HTTP_HEADER_SIZE = Utils.getIntegerNetProperty(
+            "jdk.http.maxHeaderSize",
+                Integer.MIN_VALUE, Integer.MAX_VALUE, 384 * K, true);
 
     enum State { INITIAL,
                  STATUS_LINE,
@@ -164,11 +171,16 @@ class Http1HeaderParser {
         return (char)(input.get() & 0xFF);
     }
 
-    private void readResumeStatusLine(ByteBuffer input) {
+    private void readResumeStatusLine(ByteBuffer input) throws ProtocolException {
+        final long max = MAX_HTTP_HEADER_SIZE - size - 32 - sb.length();
+        int count = 0;
         char c = 0;
         while (input.hasRemaining() && (c = get(input)) != CR) {
             if (c == LF) break;
             sb.append(c);
+            if (++count > max) {
+                checkMaxHeaderSize(sb.length());
+            }
         }
         if (c == CR) {
             state = State.STATUS_LINE_FOUND_CR;
@@ -185,6 +197,7 @@ class Http1HeaderParser {
         }
 
         statusLine = sb.toString();
+        size = size + 32 + statusLine.length();
         sb = new StringBuilder();
         if (!statusLine.startsWith("HTTP/1.")) {
             throw protocolException("Invalid status line: \"%s\"", statusLine);
@@ -205,7 +218,23 @@ class Http1HeaderParser {
         state = State.STATUS_LINE_END;
     }
 
-    private void maybeStartHeaders(ByteBuffer input) {
+    private void checkMaxHeaderSize(int sz) throws ProtocolException {
+        long s = size + sz + 32;
+        if (MAX_HTTP_HEADER_SIZE > 0 && s > MAX_HTTP_HEADER_SIZE) {
+            throw new ProtocolException(String.format("Header size too big: %s > %s",
+                    s, MAX_HTTP_HEADER_SIZE));
+        }
+    }
+    static private long newSize(long size, int name, int value) throws ProtocolException {
+        long newSize = size + name + value + 32;
+        if (MAX_HTTP_HEADER_SIZE > 0 && newSize > MAX_HTTP_HEADER_SIZE) {
+            throw new ProtocolException(String.format("Header size too big: %s > %s",
+                    newSize, MAX_HTTP_HEADER_SIZE));
+        }
+        return newSize;
+    }
+
+    private void maybeStartHeaders(ByteBuffer input) throws ProtocolException {
         assert state == State.STATUS_LINE_END;
         assert sb.length() == 0;
         char c = get(input);
@@ -215,6 +244,7 @@ class Http1HeaderParser {
             state = State.STATUS_LINE_END_LF;
         } else {
             sb.append(c);
+            checkMaxHeaderSize(sb.length());
             state = State.HEADER;
         }
     }
@@ -232,9 +262,11 @@ class Http1HeaderParser {
         }
     }
 
-    private void readResumeHeader(ByteBuffer input) {
+    private void readResumeHeader(ByteBuffer input) throws ProtocolException {
         assert state == State.HEADER;
         assert input.hasRemaining();
+        final long max = MAX_HTTP_HEADER_SIZE - size - 32 - sb.length();
+        int count = 0;
         while (input.hasRemaining()) {
             char c = get(input);
             if (c == CR) {
@@ -248,6 +280,9 @@ class Http1HeaderParser {
             if (c == HT)
                 c = SP;
             sb.append(c);
+            if (++count > max) {
+                checkMaxHeaderSize(sb.length());
+            }
         }
     }
 
@@ -268,12 +303,12 @@ class Http1HeaderParser {
         if (!Utils.isValidValue(value)) {
             throw protocolException("Invalid header value \"%s: %s\"", name, value);
         }
-
+        size = newSize(size, name.length(), value.length());
         privateMap.computeIfAbsent(name.toLowerCase(Locale.US),
                                    k -> new ArrayList<>()).add(value);
     }
 
-    private void resumeOrLF(ByteBuffer input) {
+    private void resumeOrLF(ByteBuffer input) throws ProtocolException {
         assert state == State.HEADER_FOUND_CR || state == State.HEADER_FOUND_LF;
         char c = state == State.HEADER_FOUND_LF ? LF : get(input);
         if (c == LF) {
@@ -283,10 +318,12 @@ class Http1HeaderParser {
             state = State.HEADER_FOUND_CR_LF;
         } else if (c == SP || c == HT) {
             sb.append(SP); // parity with MessageHeaders
+            checkMaxHeaderSize(sb.length());
             state = State.HEADER;
         } else {
             sb = new StringBuilder();
             sb.append(c);
+            checkMaxHeaderSize(1);
             state = State.HEADER;
         }
     }
@@ -312,6 +349,7 @@ class Http1HeaderParser {
         } else if (c == SP || c == HT) {
             assert sb.length() != 0;
             sb.append(SP); // continuation line
+            checkMaxHeaderSize(sb.length());
             state = State.HEADER;
         } else {
             if (sb.length() > 0) {
@@ -322,6 +360,7 @@ class Http1HeaderParser {
                 addHeaderFromString(headerString);
             }
             sb.append(c);
+            checkMaxHeaderSize(sb.length());
             state = State.HEADER;
         }
     }
diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http2ClientImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http2ClientImpl.java
index 9f74a70d318..0f2db7738fc 100644
--- a/src/java.net.http/share/classes/jdk/internal/net/http/Http2ClientImpl.java
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http2ClientImpl.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2024, 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
@@ -38,7 +38,6 @@ import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.CompletableFuture;
-import jdk.internal.net.http.common.Log;
 import jdk.internal.net.http.common.Logger;
 import jdk.internal.net.http.common.MinimalFuture;
 import jdk.internal.net.http.common.Utils;
@@ -48,6 +47,7 @@ import static jdk.internal.net.http.frame.SettingsFrame.ENABLE_PUSH;
 import static jdk.internal.net.http.frame.SettingsFrame.HEADER_TABLE_SIZE;
 import static jdk.internal.net.http.frame.SettingsFrame.MAX_CONCURRENT_STREAMS;
 import static jdk.internal.net.http.frame.SettingsFrame.MAX_FRAME_SIZE;
+import static jdk.internal.net.http.frame.SettingsFrame.MAX_HEADER_LIST_SIZE;
 
 /**
  *  Http2 specific aspects of HttpClientImpl
@@ -94,14 +94,19 @@ class Http2ClientImpl {
     CompletableFuture<Http2Connection> getConnectionFor(HttpRequestImpl req,
                                                         Exchange<?> exchange) {
         String key = Http2Connection.keyFor(req);
+        boolean pushEnabled = exchange.pushEnabled();
 
         synchronized (this) {
             Http2Connection connection = connections.get(key);
             if (connection != null) {
                 try {
-                    if (connection.closed || !connection.reserveStream(true)) {
+                    if (connection.closed
+                            || !connection.reserveStream(true, pushEnabled)) {
                         if (debug.on())
-                            debug.log("removing found closed or closing connection: %s", connection);
+                            debug.log("removing connection from pool since " +
+                                    "it couldn't be reserved for use%s: %s",
+                                    pushEnabled ? " with server push enabled" :
+                                            "", connection);
                         deleteConnection(connection);
                     } else {
                         // fast path if connection already exists
@@ -128,7 +133,7 @@ class Http2ClientImpl {
                     synchronized (Http2ClientImpl.this) {
                         if (conn != null) {
                             try {
-                                conn.reserveStream(true);
+                                conn.reserveStream(true, exchange.pushEnabled());
                             } catch (IOException e) {
                                 throw new UncheckedIOException(e); // shouldn't happen
                             }
@@ -161,10 +166,21 @@ class Http2ClientImpl {
         synchronized(this) {
             Http2Connection c1 = connections.putIfAbsent(key, c);
             if (c1 != null) {
-                c.setFinalStream();
-                if (debug.on())
-                    debug.log("existing entry in connection pool for %s", key);
-                return false;
+                if (c.serverPushEnabled() && !c1.serverPushEnabled()) {
+                    c1.setFinalStream();
+                    connections.remove(key, c1);
+                    connections.put(key, c);
+                    if (debug.on()) {
+                        debug.log("Replacing %s with %s in connection pool", c1, c);
+                    }
+                    if (c1.shouldClose()) c1.close();
+                    return  true;
+                } else {
+                    c.setFinalStream();
+                    if (debug.on())
+                        debug.log("existing entry in connection pool for %s", key);
+                    return false;
+                }
             }
             if (debug.on())
                 debug.log("put in the connection pool: %s", c);
@@ -204,8 +220,8 @@ class Http2ClientImpl {
     }
 
     /** Returns the client settings as a base64 (url) encoded string */
-    String getSettingsString() {
-        SettingsFrame sf = getClientSettings();
+    String getSettingsString(boolean defaultServerPush) {
+        SettingsFrame sf = getClientSettings(defaultServerPush);
         byte[] settings = sf.toByteArray(); // without the header
         Base64.Encoder encoder = Base64.getUrlEncoder()
                                        .withoutPadding();
@@ -215,14 +231,7 @@ class Http2ClientImpl {
     private static final int K = 1024;
 
     private static int getParameter(String property, int min, int max, int defaultValue) {
-        int value =  Utils.getIntegerNetProperty(property, defaultValue);
-        // use default value if misconfigured
-        if (value < min || value > max) {
-            Log.logError("Property value for {0}={1} not in [{2}..{3}]: " +
-                    "using default={4}", property, value, min, max, defaultValue);
-            value = defaultValue;
-        }
-        return value;
+        return Utils.getIntegerNetProperty(property, min, max, defaultValue, true);
     }
 
     // used for the connection window, to have a connection window size
@@ -243,7 +252,18 @@ class Http2ClientImpl {
                 streamWindow, Integer.MAX_VALUE, defaultValue);
     }
 
-    SettingsFrame getClientSettings() {
+    /**
+     * This method is used to test whether pushes are globally
+     * disabled on all connections.
+     * @return true if pushes are globally disabled on all connections
+     */
+    boolean serverPushDisabled() {
+        return getParameter(
+                "jdk.httpclient.enablepush",
+                0, 1, 1) == 0;
+    }
+
+    SettingsFrame getClientSettings(boolean defaultServerPush) {
         SettingsFrame frame = new SettingsFrame();
         // default defined for HTTP/2 is 4 K, we use 16 K.
         frame.setParameter(HEADER_TABLE_SIZE, getParameter(
@@ -252,14 +272,15 @@ class Http2ClientImpl {
         // O: does not accept push streams. 1: accepts push streams.
         frame.setParameter(ENABLE_PUSH, getParameter(
                 "jdk.httpclient.enablepush",
-                0, 1, 1));
+                0, 1, defaultServerPush ? 1 : 0));
         // HTTP/2 recommends to set the number of concurrent streams
-        // no lower than 100. We use 100. 0 means no stream would be
-        // accepted. That would render the client to be non functional,
-        // so we won't let 0 be configured for our Http2ClientImpl.
+        // no lower than 100. We use 100, unless push promises are
+        // disabled.
+        int initialServerStreams = frame.getParameter(ENABLE_PUSH) == 0
+                ? 0 : 100;
         frame.setParameter(MAX_CONCURRENT_STREAMS, getParameter(
                 "jdk.httpclient.maxstreams",
-                1, Integer.MAX_VALUE, 100));
+                0, Integer.MAX_VALUE, initialServerStreams));
         // Maximum size is 2^31-1. Don't allow window size to be less
         // than the minimum frame size as this is likely to be a
         // configuration error. HTTP/2 specify a default of 64 * K -1,
@@ -272,6 +293,14 @@ class Http2ClientImpl {
         frame.setParameter(MAX_FRAME_SIZE, getParameter(
                 "jdk.httpclient.maxframesize",
                 16 * K, 16 * K * K -1, 16 * K));
+        // Maximum field section size we're prepared to accept
+        // This is the uncompressed name + value size + 32 per field line
+        int maxHeaderSize = getParameter(
+                "jdk.http.maxHeaderSize",
+                Integer.MIN_VALUE, Integer.MAX_VALUE, 384 * K);
+        // If the property is <= 0 the value is unlimited
+        if (maxHeaderSize <= 0) maxHeaderSize = -1;
+        frame.setParameter(MAX_HEADER_LIST_SIZE, maxHeaderSize);
         return frame;
     }
 }
diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http2Connection.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http2Connection.java
index 920e5cefa95..f363231e1c8 100644
--- a/src/java.net.http/share/classes/jdk/internal/net/http/Http2Connection.java
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http2Connection.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2021, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2024, 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
@@ -29,6 +29,7 @@ import java.io.EOFException;
 import java.io.IOException;
 import java.io.UncheckedIOException;
 import java.net.InetSocketAddress;
+import java.net.ProtocolException;
 import java.net.URI;
 import java.nio.ByteBuffer;
 import java.nio.charset.StandardCharsets;
@@ -44,6 +45,8 @@ import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.Flow;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Function;
 import java.util.function.Supplier;
 import javax.net.ssl.SSLEngine;
@@ -245,6 +248,45 @@ class Http2Connection  {
         }
     }
 
+    private final class PushPromiseDecoder extends HeaderDecoder implements DecodingCallback {
+
+        final int parentStreamId;
+        final int pushPromiseStreamId;
+        final Stream<?> parent;
+        final AtomicReference<Throwable> errorRef = new AtomicReference<>();
+
+        PushPromiseDecoder(int parentStreamId, int pushPromiseStreamId, Stream<?> parent) {
+            this.parentStreamId = parentStreamId;
+            this.pushPromiseStreamId = pushPromiseStreamId;
+            this.parent = parent;
+        }
+
+        @Override
+        protected void addHeader(String name, String value) {
+            if (errorRef.get() == null) {
+                super.addHeader(name, value);
+            }
+        }
+
+        @Override
+        public void onMaxHeaderListSizeReached(long size, int maxHeaderListSize) throws ProtocolException {
+            try {
+                DecodingCallback.super.onMaxHeaderListSizeReached(size, maxHeaderListSize);
+            } catch (ProtocolException pe) {
+                if (parent != null) {
+                    if (errorRef.compareAndSet(null, pe)) {
+                        // cancel the parent stream
+                        resetStream(pushPromiseStreamId, ResetFrame.REFUSED_STREAM);
+                        parent.onProtocolError(pe);
+                    }
+                } else {
+                    // interrupt decoding and closes the connection
+                    throw pe;
+                }
+            }
+        }
+    }
+
     volatile boolean closed;
 
     //-------------------------------------
@@ -265,7 +307,7 @@ class Http2Connection  {
     private final Decoder hpackIn;
     final SettingsFrame clientSettings;
     private volatile SettingsFrame serverSettings;
-    private record PushContinuationState(HeaderDecoder pushContDecoder, PushPromiseFrame pushContFrame) {}
+    private record PushContinuationState(PushPromiseDecoder pushContDecoder, PushPromiseFrame pushContFrame) {}
     private volatile PushContinuationState pushContinuationState;
     private final String key; // for HttpClientImpl.connections map
     private final FramesDecoder framesDecoder;
@@ -279,12 +321,24 @@ class Http2Connection  {
     private final FramesController framesController = new FramesController();
     private final Http2TubeSubscriber subscriber;
     final ConnectionWindowUpdateSender windowUpdater;
-    private volatile Throwable cause;
+    private final AtomicReference<Throwable> cause = new AtomicReference<>();
     private volatile Supplier<ByteBuffer> initial;
     private volatile Stream<?> initialStream;
 
+    private ValidatingHeadersConsumer orphanedConsumer;
+    private final AtomicInteger orphanedHeaders = new AtomicInteger();
+
     static final int DEFAULT_FRAME_SIZE = 16 * 1024;
+    static final int MAX_LITERAL_WITH_INDEXING =
+            Utils.getIntegerNetProperty("jdk.httpclient.maxLiteralWithIndexing",512);
 
+    // The maximum number of HEADER frames, CONTINUATION frames, or PUSH_PROMISE frames
+    // referring to an already closed or non-existent stream that a client will accept to
+    // process. Receiving frames referring to non-existent or closed streams doesn't necessarily
+    // constitute an HTTP/2 protocol error, but receiving too many may indicate a problem
+    // with the connection. If this limit is reached, a {@link java.net.ProtocolException
+    // ProtocolException} will be raised and the connection will be closed.
+    static final int MAX_ORPHANED_HEADERS = 1024;
 
     // TODO: need list of control frames from other threads
     // that need to be sent
@@ -292,19 +346,21 @@ class Http2Connection  {
     private Http2Connection(HttpConnection connection,
                             Http2ClientImpl client2,
                             int nextstreamid,
-                            String key) {
+                            String key,
+                            boolean defaultServerPush) {
         this.connection = connection;
         this.client2 = client2;
         this.subscriber = new Http2TubeSubscriber(client2.client());
         this.nextstreamid = nextstreamid;
         this.key = key;
-        this.clientSettings = this.client2.getClientSettings();
+        this.clientSettings = this.client2.getClientSettings(defaultServerPush);
         this.framesDecoder = new FramesDecoder(this::processFrame,
                 clientSettings.getParameter(SettingsFrame.MAX_FRAME_SIZE));
         // serverSettings will be updated by server
         this.serverSettings = SettingsFrame.defaultRFCSettings();
         this.hpackOut = new Encoder(serverSettings.getParameter(HEADER_TABLE_SIZE));
-        this.hpackIn = new Decoder(clientSettings.getParameter(HEADER_TABLE_SIZE));
+        this.hpackIn = new Decoder(clientSettings.getParameter(HEADER_TABLE_SIZE),
+                clientSettings.getParameter(MAX_HEADER_LIST_SIZE), MAX_LITERAL_WITH_INDEXING);
         if (debugHpack.on()) {
             debugHpack.log("For the record:" + super.toString());
             debugHpack.log("Decoder created: %s", hpackIn);
@@ -323,14 +379,16 @@ class Http2Connection  {
     private Http2Connection(HttpConnection connection,
                     Http2ClientImpl client2,
                     Exchange<?> exchange,
-                    Supplier<ByteBuffer> initial)
+                    Supplier<ByteBuffer> initial,
+                    boolean defaultServerPush)
         throws IOException, InterruptedException
     {
         this(connection,
                 client2,
                 3, // stream 1 is registered during the upgrade
-                keyFor(connection));
-        reserveStream(true);
+                keyFor(connection),
+                defaultServerPush);
+        reserveStream(true, clientSettings.getFlag(ENABLE_PUSH));
         Log.logTrace("Connection send window size {0} ", windowController.connectionWindowSize());
 
         Stream<?> initialStream = createStream(exchange);
@@ -363,7 +421,8 @@ class Http2Connection  {
                                                           Exchange<?> exchange,
                                                           Supplier<ByteBuffer> initial)
     {
-        return MinimalFuture.supply(() -> new Http2Connection(connection, client2, exchange, initial));
+        return MinimalFuture.supply(() -> new Http2Connection(connection, client2, exchange, initial,
+                exchange.pushEnabled()));
     }
 
     // Requires TLS handshake. So, is really async
@@ -387,7 +446,8 @@ class Http2Connection  {
                   .thenCompose(notused-> {
                       CompletableFuture<Http2Connection> cf = new MinimalFuture<>();
                       try {
-                          Http2Connection hc = new Http2Connection(request, h2client, connection);
+                          Http2Connection hc = new Http2Connection(request, h2client,
+                                  connection, exchange.pushEnabled());
                           cf.complete(hc);
                       } catch (IOException e) {
                           cf.completeExceptionally(e);
@@ -402,13 +462,15 @@ class Http2Connection  {
      */
     private Http2Connection(HttpRequestImpl request,
                             Http2ClientImpl h2client,
-                            HttpConnection connection)
+                            HttpConnection connection,
+                            boolean defaultServerPush)
         throws IOException
     {
         this(connection,
              h2client,
              1,
-             keyFor(request));
+             keyFor(request),
+             defaultServerPush);
 
         Log.logTrace("Connection send window size {0} ", windowController.connectionWindowSize());
 
@@ -431,15 +493,21 @@ class Http2Connection  {
     // if false returned then a new Http2Connection is required
     // if true, the stream may be assigned to this connection
     // for server push, if false returned, then the stream should be cancelled
-    synchronized boolean reserveStream(boolean clientInitiated) throws IOException {
+    synchronized boolean reserveStream(boolean clientInitiated, boolean pushEnabled) throws IOException {
         if (finalStream) {
             return false;
         }
-        if (clientInitiated && (lastReservedClientStreamid + 2) >= MAX_CLIENT_STREAM_ID) {
+        // If requesting to reserve a stream for an exchange for which push is enabled,
+        // we will reserve the stream in this connection only if this connection is also
+        // push enabled, unless pushes are globally disabled.
+        boolean pushCompatible = !clientInitiated || !pushEnabled
+                || this.serverPushEnabled()
+                || client2.serverPushDisabled();
+        if (clientInitiated && (lastReservedClientStreamid >= MAX_CLIENT_STREAM_ID -2  || !pushCompatible)) {
             setFinalStream();
             client2.deleteConnection(this);
             return false;
-        } else if (!clientInitiated && (lastReservedServerStreamid + 2) >= MAX_SERVER_STREAM_ID) {
+        } else if (!clientInitiated && (lastReservedServerStreamid >= MAX_SERVER_STREAM_ID - 2)) {
             setFinalStream();
             client2.deleteConnection(this);
             return false;
@@ -464,6 +532,10 @@ class Http2Connection  {
         return true;
     }
 
+    synchronized boolean shouldClose() {
+        return finalStream() && streams.isEmpty();
+    }
+
     /**
      * Throws an IOException if h2 was not negotiated
      */
@@ -591,6 +663,10 @@ class Http2Connection  {
         return this.key;
     }
 
+    public boolean serverPushEnabled() {
+        return clientSettings.getParameter(SettingsFrame.ENABLE_PUSH) == 1;
+    }
+
     boolean offerConnection() {
         return client2.offerConnection(this);
     }
@@ -689,7 +765,7 @@ class Http2Connection  {
     }
 
     Throwable getRecordedCause() {
-        return cause;
+        return cause.get();
     }
 
     void shutdown(Throwable t) {
@@ -699,6 +775,7 @@ class Http2Connection  {
             if (closed == true) return;
             closed = true;
         }
+        cause.compareAndSet(null, t);
         if (Log.errors()) {
             if (!(t instanceof EOFException) || isActive()) {
                 Log.logError(t);
@@ -706,9 +783,8 @@ class Http2Connection  {
                 Log.logError("Shutting down connection: {0}", t.getMessage());
             }
         }
-        Throwable initialCause = this.cause;
-        if (initialCause == null) this.cause = t;
         client2.deleteConnection(this);
+        subscriber.stop(cause.get());
         for (Stream<?> s : streams.values()) {
             try {
                 s.connectionClosing(t);
@@ -762,17 +838,39 @@ class Http2Connection  {
                 return;
             }
 
+            if (frame instanceof PushPromiseFrame && !serverPushEnabled()) {
+                String protocolError = "received a PUSH_PROMISE when SETTINGS_ENABLE_PUSH is 0";
+                protocolError(ResetFrame.PROTOCOL_ERROR, protocolError);
+                return;
+            }
+
             Stream<?> stream = getStream(streamid);
+            var nextstreamid = this.nextstreamid;
+            if (stream == null && (streamid & 0x01) == 0x01 && streamid >= nextstreamid) {
+                String protocolError = String.format(
+                        "received a frame for a non existing streamid(%s) >= nextstreamid(%s)",
+                        streamid, nextstreamid);
+                protocolError(ResetFrame.PROTOCOL_ERROR, protocolError);
+                return;
+            }
             if (stream == null && pushContinuationState == null) {
                 // Should never receive a frame with unknown stream id
 
-                if (frame instanceof HeaderFrame) {
+                if (frame instanceof HeaderFrame hf) {
+                    String protocolError = checkMaxOrphanedHeadersExceeded(hf);
+                    if (protocolError != null) {
+                        protocolError(ResetFrame.PROTOCOL_ERROR, protocolError);
+                        return;
+                    }
                     // always decode the headers as they may affect
                     // connection-level HPACK decoding state
-                    DecodingCallback decoder = new ValidatingHeadersConsumer()::onDecoded;
+                    if (orphanedConsumer == null || frame.getClass() != ContinuationFrame.class) {
+                        orphanedConsumer = new ValidatingHeadersConsumer();
+                    }
+                    DecodingCallback decoder = orphanedConsumer::onDecoded;
                     try {
-                        decodeHeaders((HeaderFrame) frame, decoder);
-                    } catch (UncheckedIOException e) {
+                        decodeHeaders(hf, decoder);
+                    } catch (IOException | UncheckedIOException e) {
                         protocolError(ResetFrame.PROTOCOL_ERROR, e.getMessage());
                         return;
                     }
@@ -800,29 +898,41 @@ class Http2Connection  {
 
             // While push frame is not null, the only acceptable frame on this
             // stream is a Continuation frame
-            if (pushContinuationState != null) {
+            PushContinuationState pcs = pushContinuationState;
+            if (pcs != null) {
                 if (frame instanceof ContinuationFrame cf) {
+                    if (stream == null) {
+                        String protocolError = checkMaxOrphanedHeadersExceeded(cf);
+                        if (protocolError != null) {
+                            protocolError(ResetFrame.PROTOCOL_ERROR, protocolError);
+                            return;
+                        }
+                    }
                     try {
-                        if (streamid == pushContinuationState.pushContFrame.streamid())
-                            handlePushContinuation(stream, cf);
-                        else
-                            protocolError(ErrorFrame.PROTOCOL_ERROR, "Received a Continuation Frame with an " +
-                                    "unexpected stream id");
-                    } catch (UncheckedIOException e) {
+                        if (streamid == pcs.pushContFrame.streamid())
+                            handlePushContinuation(pcs, stream, cf);
+                        else {
+                            String protocolError = "Received a CONTINUATION with " +
+                                    "unexpected stream id: " + streamid + " != "
+                                    + pcs.pushContFrame.streamid();
+                            protocolError(ErrorFrame.PROTOCOL_ERROR, protocolError);
+                        }
+                    } catch (IOException | UncheckedIOException e) {
                         debug.log("Error handling Push Promise with Continuation: " + e.getMessage(), e);
                         protocolError(ErrorFrame.PROTOCOL_ERROR, e.getMessage());
                         return;
                     }
                 } else {
                     pushContinuationState = null;
-                    protocolError(ErrorFrame.PROTOCOL_ERROR, "Expected a Continuation frame but received " + frame);
+                    String protocolError = "Expected a CONTINUATION frame but received " + frame;
+                    protocolError(ErrorFrame.PROTOCOL_ERROR, protocolError);
                     return;
                 }
             } else {
                 if (frame instanceof PushPromiseFrame pp) {
                     try {
                         handlePushPromise(stream, pp);
-                    } catch (UncheckedIOException e) {
+                    } catch (IOException | UncheckedIOException e) {
                         protocolError(ErrorFrame.PROTOCOL_ERROR, e.getMessage());
                         return;
                     }
@@ -830,7 +940,7 @@ class Http2Connection  {
                     // decode headers
                     try {
                         decodeHeaders(hf, stream.rspHeadersConsumer());
-                    } catch (UncheckedIOException e) {
+                    } catch (IOException | UncheckedIOException e) {
                         debug.log("Error decoding headers: " + e.getMessage(), e);
                         protocolError(ErrorFrame.PROTOCOL_ERROR, e.getMessage());
                         return;
@@ -843,6 +953,16 @@ class Http2Connection  {
         }
     }
 
+    private String checkMaxOrphanedHeadersExceeded(HeaderFrame hf) {
+        if (MAX_ORPHANED_HEADERS > 0 ) {
+            int orphaned = orphanedHeaders.incrementAndGet();
+            if (orphaned < 0 || orphaned > MAX_ORPHANED_HEADERS) {
+               return "Too many orphaned header frames received on connection";
+            }
+        }
+        return null;
+    }
+
     final void dropDataFrame(DataFrame df) {
         if (closed) return;
         if (debug.on()) {
@@ -867,38 +987,65 @@ class Http2Connection  {
     private <T> void handlePushPromise(Stream<T> parent, PushPromiseFrame pp)
         throws IOException
     {
+        int promisedStreamid = pp.getPromisedStream();
+        if ((promisedStreamid & 0x01) != 0x00) {
+            throw new ProtocolException("Received PUSH_PROMISE for stream " + promisedStreamid);
+        }
+        int streamId = pp.streamid();
+        if ((streamId & 0x01) != 0x01) {
+            throw new ProtocolException("Received PUSH_PROMISE on stream " + streamId);
+        }
         // always decode the headers as they may affect connection-level HPACK
         // decoding state
         assert pushContinuationState == null;
-        HeaderDecoder decoder = new HeaderDecoder();
-        decodeHeaders(pp, decoder::onDecoded);
-        int promisedStreamid = pp.getPromisedStream();
+        PushPromiseDecoder decoder = new PushPromiseDecoder(streamId, promisedStreamid, parent);
+        decodeHeaders(pp, decoder);
         if (pp.endHeaders()) {
-            completePushPromise(promisedStreamid, parent, decoder.headers());
+            if (decoder.errorRef.get() == null) {
+                completePushPromise(promisedStreamid, parent, decoder.headers());
+            }
         } else {
             pushContinuationState = new PushContinuationState(decoder, pp);
         }
     }
 
-    private <T> void handlePushContinuation(Stream<T> parent, ContinuationFrame cf)
+    private <T> void handlePushContinuation(PushContinuationState pcs, Stream<T> parent, ContinuationFrame cf)
             throws IOException {
-        var pcs = pushContinuationState;
-        decodeHeaders(cf, pcs.pushContDecoder::onDecoded);
+        assert pcs.pushContFrame.streamid() == cf.streamid() : String.format(
+                    "Received CONTINUATION on a different stream %s != %s",
+                    cf.streamid(), pcs.pushContFrame.streamid());
+        decodeHeaders(cf, pcs.pushContDecoder);
         // if all continuations are sent, set pushWithContinuation to null
         if (cf.endHeaders()) {
-            completePushPromise(pcs.pushContFrame.getPromisedStream(), parent,
-                    pcs.pushContDecoder.headers());
+            if (pcs.pushContDecoder.errorRef.get() == null) {
+                completePushPromise(pcs.pushContFrame.getPromisedStream(), parent,
+                        pcs.pushContDecoder.headers());
+            }
             pushContinuationState = null;
         }
     }
 
     private <T> void completePushPromise(int promisedStreamid, Stream<T> parent, HttpHeaders headers)
             throws IOException {
+        if (parent == null) {
+            resetStream(promisedStreamid, ResetFrame.REFUSED_STREAM);
+            return;
+        }
         HttpRequestImpl parentReq = parent.request;
+        if (promisedStreamid < nextPushStream) {
+            // From RFC 9113 section 5.1.1:
+            // The identifier of a newly established stream MUST be numerically
+            // greater than all streams that the initiating endpoint has
+            // opened or reserved.
+            protocolError(ResetFrame.PROTOCOL_ERROR, String.format(
+                    "Unexpected stream identifier: %s < %s", promisedStreamid, nextPushStream));
+            return;
+        }
         if (promisedStreamid != nextPushStream) {
+            // we don't support skipping stream ids;
             resetStream(promisedStreamid, ResetFrame.PROTOCOL_ERROR);
             return;
-        } else if (!reserveStream(false)) {
+        } else if (!reserveStream(false, true)) {
             resetStream(promisedStreamid, ResetFrame.REFUSED_STREAM);
             return;
         } else {
@@ -1019,9 +1166,15 @@ class Http2Connection  {
     private void protocolError(int errorCode, String msg)
         throws IOException
     {
+        String protocolError = "protocol error" + (msg == null?"":(": " + msg));
+        ProtocolException protocolException =
+                new ProtocolException(protocolError);
+        framesDecoder.close(protocolError);
+        subscriber.stop(protocolException);
+        if (debug.on()) debug.log("Sending GOAWAY due to " + protocolException);
         GoAwayFrame frame = new GoAwayFrame(0, errorCode);
         sendFrame(frame);
-        shutdown(new IOException("protocol error" + (msg == null?"":(": " + msg))));
+        shutdown(protocolException);
     }
 
     private void handleSettings(SettingsFrame frame)
@@ -1161,7 +1314,7 @@ class Http2Connection  {
 
     <T> Stream.PushedStream<T> createPushStream(Stream<T> parent, Exchange<T> pushEx) {
         PushGroup<T> pg = parent.exchange.getPushGroup();
-        return new Stream.PushedStream<>(pg, this, pushEx);
+        return new Stream.PushedStream<>(parent, pg, this, pushEx);
     }
 
     <T> void putStream(Stream<T> stream, int streamid) {
@@ -1223,16 +1376,18 @@ class Http2Connection  {
     private List<ByteBuffer> encodeHeadersImpl(int bufferSize, HttpHeaders... headers) {
         ByteBuffer buffer = getHeaderBuffer(bufferSize);
         List<ByteBuffer> buffers = new ArrayList<>();
-        for(HttpHeaders header : headers) {
+        for (HttpHeaders header : headers) {
             for (Map.Entry<String, List<String>> e : header.map().entrySet()) {
                 String lKey = e.getKey().toLowerCase(Locale.US);
                 List<String> values = e.getValue();
                 for (String value : values) {
                     hpackOut.header(lKey, value);
                     while (!hpackOut.encode(buffer)) {
-                        buffer.flip();
-                        buffers.add(buffer);
-                        buffer =  getHeaderBuffer(bufferSize);
+                        if (!buffer.hasRemaining()) {
+                            buffer.flip();
+                            buffers.add(buffer);
+                            buffer = getHeaderBuffer(bufferSize);
+                        }
                     }
                 }
             }
@@ -1295,6 +1450,8 @@ class Http2Connection  {
                     Stream<?> stream = registerNewStream(oh);
                     // provide protection from inserting unordered frames between Headers and Continuation
                     if (stream != null) {
+                        // we are creating a new stream: reset orphaned header count
+                        orphanedHeaders.set(0);
                         publisher.enqueue(encodeHeaders(oh, stream));
                     }
                 } else {
@@ -1353,7 +1510,7 @@ class Http2Connection  {
         private volatile Flow.Subscription subscription;
         private volatile boolean completed;
         private volatile boolean dropped;
-        private volatile Throwable error;
+        private final AtomicReference<Throwable> errorRef = new AtomicReference<>();
         private final ConcurrentLinkedQueue<ByteBuffer> queue
                 = new ConcurrentLinkedQueue<>();
         private final SequentialScheduler scheduler =
@@ -1374,10 +1531,9 @@ class Http2Connection  {
                     asyncReceive(buffer);
                 }
             } catch (Throwable t) {
-                Throwable x = error;
-                if (x == null) error = t;
+                errorRef.compareAndSet(null, t);
             } finally {
-                Throwable x = error;
+                Throwable x = errorRef.get();
                 if (x != null) {
                     if (debug.on()) debug.log("Stopping scheduler", x);
                     scheduler.stop();
@@ -1412,6 +1568,7 @@ class Http2Connection  {
 
         @Override
         public void onNext(List<ByteBuffer> item) {
+            if (completed) return;
             if (debug.on()) debug.log(() -> "onNext: got " + Utils.remaining(item)
                     + " bytes in " + item.size() + " buffers");
             queue.addAll(item);
@@ -1420,19 +1577,21 @@ class Http2Connection  {
 
         @Override
         public void onError(Throwable throwable) {
+            if (completed) return;
             if (debug.on()) debug.log(() -> "onError: " + throwable);
-            error = throwable;
+            errorRef.compareAndSet(null, throwable);
             completed = true;
             runOrSchedule();
         }
 
         @Override
         public void onComplete() {
+            if (completed) return;
             String msg = isActive()
                     ? "EOF reached while reading"
                     : "Idle connection closed by HTTP/2 peer";
             if (debug.on()) debug.log(msg);
-            error = new EOFException(msg);
+            errorRef.compareAndSet(null, new EOFException(msg));
             completed = true;
             runOrSchedule();
         }
@@ -1444,6 +1603,18 @@ class Http2Connection  {
             // then we might not need the 'dropped' boolean?
             dropped = true;
         }
+
+        void stop(Throwable error) {
+            if (errorRef.compareAndSet(null, error)) {
+                completed = true;
+                scheduler.stop();
+                queue.clear();
+                if (subscription != null) {
+                    subscription.cancel();
+                }
+                queue.clear();
+            }
+        }
     }
 
     synchronized boolean isActive() {
diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/HttpClientImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/HttpClientImpl.java
index 671ae9b7ca4..bb0dd914afb 100644
--- a/src/java.net.http/share/classes/jdk/internal/net/http/HttpClientImpl.java
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/HttpClientImpl.java
@@ -584,7 +584,9 @@ final class HttpClientImpl extends HttpClient implements Trackable {
                 // SSLException
                 throw new SSLException(msg, throwable);
             } else if (throwable instanceof ProtocolException) {
-                throw new ProtocolException(msg);
+                ProtocolException pe = new ProtocolException(msg);
+                pe.initCause(throwable);
+                throw pe;
             } else if (throwable instanceof IOException) {
                 throw new IOException(msg, throwable);
             } else {
diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/HttpRequestImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/HttpRequestImpl.java
index d2b908dffa5..27e65446844 100644
--- a/src/java.net.http/share/classes/jdk/internal/net/http/HttpRequestImpl.java
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/HttpRequestImpl.java
@@ -287,10 +287,10 @@ public class HttpRequestImpl extends HttpRequest implements WebSocketRequest {
 
     InetSocketAddress authority() { return authority; }
 
-    void setH2Upgrade(Http2ClientImpl h2client) {
+    void setH2Upgrade(Exchange<?> exchange) {
         systemHeadersBuilder.setHeader("Connection", "Upgrade, HTTP2-Settings");
         systemHeadersBuilder.setHeader("Upgrade", "h2c");
-        systemHeadersBuilder.setHeader("HTTP2-Settings", h2client.getSettingsString());
+        systemHeadersBuilder.setHeader("HTTP2-Settings", exchange.h2cSettingsStrings());
     }
 
     @Override
diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/ResponseBodyHandlers.java b/src/java.net.http/share/classes/jdk/internal/net/http/ResponseBodyHandlers.java
index 66d89ae1fc5..22e03238d21 100644
--- a/src/java.net.http/share/classes/jdk/internal/net/http/ResponseBodyHandlers.java
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/ResponseBodyHandlers.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2018, 2021, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2018, 2024, 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
@@ -37,6 +37,7 @@ import java.nio.file.Paths;
 import java.security.AccessControlContext;
 import java.security.AccessController;
 import java.util.List;
+import java.util.Objects;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ConcurrentMap;
 import java.util.function.Function;
@@ -137,16 +138,21 @@ public final class ResponseBodyHandlers {
             if (!initiatingURI.getHost().equalsIgnoreCase(pushRequestURI.getHost()))
                 return;
 
+            String initiatingScheme = initiatingURI.getScheme();
+            String pushRequestScheme = pushRequestURI.getScheme();
+
+            if (!initiatingScheme.equalsIgnoreCase(pushRequestScheme)) return;
+
             int initiatingPort = initiatingURI.getPort();
             if (initiatingPort == -1 ) {
-                if ("https".equalsIgnoreCase(initiatingURI.getScheme()))
+                if ("https".equalsIgnoreCase(initiatingScheme))
                     initiatingPort = 443;
                 else
                     initiatingPort = 80;
             }
             int pushPort = pushRequestURI.getPort();
             if (pushPort == -1 ) {
-                if ("https".equalsIgnoreCase(pushRequestURI.getScheme()))
+                if ("https".equalsIgnoreCase(pushRequestScheme))
                     pushPort = 443;
                 else
                     pushPort = 80;
diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Stream.java b/src/java.net.http/share/classes/jdk/internal/net/http/Stream.java
index e781467db6b..0b9cf64d490 100644
--- a/src/java.net.http/share/classes/jdk/internal/net/http/Stream.java
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/Stream.java
@@ -30,6 +30,7 @@ import java.io.IOException;
 import java.io.UncheckedIOException;
 import java.lang.invoke.MethodHandles;
 import java.lang.invoke.VarHandle;
+import java.net.ProtocolException;
 import java.net.URI;
 import java.nio.ByteBuffer;
 import java.util.ArrayList;
@@ -41,6 +42,7 @@ import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.Executor;
 import java.util.concurrent.Flow;
 import java.util.concurrent.Flow.Subscription;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.BiPredicate;
 import java.net.http.HttpClient;
@@ -48,10 +50,13 @@ import java.net.http.HttpHeaders;
 import java.net.http.HttpRequest;
 import java.net.http.HttpResponse;
 import java.net.http.HttpResponse.BodySubscriber;
+
 import jdk.internal.net.http.common.*;
 import jdk.internal.net.http.frame.*;
 import jdk.internal.net.http.hpack.DecodingCallback;
 
+import static jdk.internal.net.http.Exchange.MAX_NON_FINAL_RESPONSES;
+
 /**
  * Http/2 Stream handling.
  *
@@ -136,6 +141,9 @@ class Stream<T> extends ExchangeImpl<T> {
     private volatile boolean closed;
     private volatile boolean endStreamSent;
     private volatile boolean finalResponseCodeReceived;
+    private volatile boolean trailerReceived;
+    private AtomicInteger nonFinalResponseCount = new AtomicInteger();
+
     // Indicates the first reason that was invoked when sending a ResetFrame
     // to the server. A streamState of 0 indicates that no reset was sent.
     // (see markStream(int code)
@@ -460,28 +468,62 @@ class Stream<T> extends ExchangeImpl<T> {
     // The Hpack decoder decodes into one of these consumers of name,value pairs
 
     DecodingCallback rspHeadersConsumer() {
-        return rspHeadersConsumer::onDecoded;
+        return rspHeadersConsumer;
+    }
+
+    String checkInterimResponseCountExceeded() {
+        // this is also checked by Exchange - but tracking it here too provides
+        // a more informative message.
+        int count = nonFinalResponseCount.incrementAndGet();
+        if (MAX_NON_FINAL_RESPONSES > 0 && (count < 0 || count > MAX_NON_FINAL_RESPONSES)) {
+            return String.format(
+                    "Stream %s PROTOCOL_ERROR: too many interim responses received: %s > %s",
+                    streamid, count, MAX_NON_FINAL_RESPONSES);
+        }
+        return null;
     }
 
     protected void handleResponse() throws IOException {
         HttpHeaders responseHeaders = responseHeadersBuilder.build();
 
         if (!finalResponseCodeReceived) {
-            responseCode = (int) responseHeaders
-                    .firstValueAsLong(":status")
-                    .orElseThrow(() -> new IOException("no statuscode in response"));
+            try {
+                responseCode = (int) responseHeaders
+                        .firstValueAsLong(":status")
+                        .orElseThrow(() -> new ProtocolException(String.format(
+                                "Stream %s PROTOCOL_ERROR: no status code in response",
+                                streamid)));
+            } catch (ProtocolException cause) {
+                cancelImpl(cause, ResetFrame.PROTOCOL_ERROR);
+                rspHeadersConsumer.reset();
+                return;
+            }
+
+            String protocolErrorMsg = null;
             // If informational code, response is partially complete
-            if (responseCode < 100 || responseCode > 199)
+            if (responseCode < 100 || responseCode > 199) {
                 this.finalResponseCodeReceived = true;
+            } else {
+                protocolErrorMsg = checkInterimResponseCountExceeded();
+            }
+
+            if (protocolErrorMsg != null) {
+                if (debug.on()) {
+                    debug.log(protocolErrorMsg);
+                }
+                cancelImpl(new ProtocolException(protocolErrorMsg), ResetFrame.PROTOCOL_ERROR);
+                rspHeadersConsumer.reset();
+                return;
+            }
 
             response = new Response(
                     request, exchange, responseHeaders, connection(),
                     responseCode, HttpClient.Version.HTTP_2);
 
-        /* TODO: review if needs to be removed
-           the value is not used, but in case `content-length` doesn't parse as
-           long, there will be NumberFormatException. If left as is, make sure
-           code up the stack handles NFE correctly. */
+            /* TODO: review if needs to be removed
+               the value is not used, but in case `content-length` doesn't parse as
+               long, there will be NumberFormatException. If left as is, make sure
+               code up the stack handles NFE correctly. */
             responseHeaders.firstValueAsLong("content-length");
 
             if (Log.headers()) {
@@ -500,6 +542,15 @@ class Stream<T> extends ExchangeImpl<T> {
                 Log.dumpHeaders(sb, "    ", responseHeaders);
                 Log.logHeaders(sb.toString());
             }
+            if (trailerReceived) {
+                String protocolErrorMsg = String.format(
+                        "Stream %s PROTOCOL_ERROR: trailers already received", streamid);
+                if (debug.on()) {
+                    debug.log(protocolErrorMsg);
+                }
+                cancelImpl(new ProtocolException(protocolErrorMsg), ResetFrame.PROTOCOL_ERROR);
+            }
+            trailerReceived = true;
             rspHeadersConsumer.reset();
         }
 
@@ -1057,7 +1108,7 @@ class Stream<T> extends ExchangeImpl<T> {
 
     /**
      * A List of responses relating to this stream. Normally there is only
-     * one response, but intermediate responses like 100 are allowed
+     * one response, but interim responses like 100 are allowed
      * and must be passed up to higher level before continuing. Deals with races
      * such as if responses are returned before the CFs get created by
      * getResponseAsync()
@@ -1242,7 +1293,7 @@ class Stream<T> extends ExchangeImpl<T> {
         cancelImpl(e, ResetFrame.CANCEL);
     }
 
-    private void cancelImpl(final Throwable e, final int resetFrameErrCode) {
+    void cancelImpl(final Throwable e, final int resetFrameErrCode) {
         errorRef.compareAndSet(null, e);
         if (debug.on()) {
             if (streamid == 0) debug.log("cancelling stream: %s", (Object)e);
@@ -1311,6 +1362,7 @@ class Stream<T> extends ExchangeImpl<T> {
     }
 
     static class PushedStream<T> extends Stream<T> {
+        final Stream<T> parent;
         final PushGroup<T> pushGroup;
         // push streams need the response CF allocated up front as it is
         // given directly to user via the multi handler callback function.
@@ -1320,16 +1372,17 @@ class Stream<T> extends ExchangeImpl<T> {
         HttpResponse.BodyHandler<T> pushHandler;
         private volatile boolean finalPushResponseCodeReceived;
 
-        PushedStream(PushGroup<T> pushGroup,
+        PushedStream(Stream<T> parent,
+                     PushGroup<T> pushGroup,
                      Http2Connection connection,
                      Exchange<T> pushReq) {
             // ## no request body possible, null window controller
             super(connection, pushReq, null);
+            this.parent = parent;
             this.pushGroup = pushGroup;
             this.pushReq = pushReq.request();
             this.pushCF = new MinimalFuture<>();
             this.responseCF = new MinimalFuture<>();
-
         }
 
         CompletableFuture<HttpResponse<T>> responseCF() {
@@ -1422,7 +1475,16 @@ class Stream<T> extends ExchangeImpl<T> {
                     .orElse(-1);
 
                 if (responseCode == -1) {
-                    completeResponseExceptionally(new IOException("No status code"));
+                    cancelImpl(new ProtocolException("No status code"), ResetFrame.PROTOCOL_ERROR);
+                    rspHeadersConsumer.reset();
+                    return;
+                } else if (responseCode >= 100 && responseCode < 200) {
+                    String protocolErrorMsg = checkInterimResponseCountExceeded();
+                    if (protocolErrorMsg != null) {
+                        cancelImpl(new ProtocolException(protocolErrorMsg), ResetFrame.PROTOCOL_ERROR);
+                        rspHeadersConsumer.reset();
+                        return;
+                    }
                 }
 
                 this.finalPushResponseCodeReceived = true;
@@ -1504,7 +1566,9 @@ class Stream<T> extends ExchangeImpl<T> {
         return connection.dbgString() + "/Stream("+streamid+")";
     }
 
-    private class HeadersConsumer extends ValidatingHeadersConsumer {
+    private class HeadersConsumer extends ValidatingHeadersConsumer implements DecodingCallback {
+
+        boolean maxHeaderListSizeReached;
 
         @Override
         public void reset() {
@@ -1517,6 +1581,9 @@ class Stream<T> extends ExchangeImpl<T> {
         public void onDecoded(CharSequence name, CharSequence value)
             throws UncheckedIOException
         {
+            if (maxHeaderListSizeReached) {
+                return;
+            }
             try {
                 String n = name.toString();
                 String v = value.toString();
@@ -1539,6 +1606,23 @@ class Stream<T> extends ExchangeImpl<T> {
         protected String formatMessage(String message, String header) {
             return "malformed response: " + super.formatMessage(message, header);
         }
+
+        @Override
+        public void onMaxHeaderListSizeReached(long size, int maxHeaderListSize) throws ProtocolException {
+            if (maxHeaderListSizeReached) return;
+            try {
+                DecodingCallback.super.onMaxHeaderListSizeReached(size, maxHeaderListSize);
+            } catch (ProtocolException cause) {
+                maxHeaderListSizeReached = true;
+                // If this is a push stream: cancel the parent.
+                if (Stream.this instanceof Stream.PushedStream<?> ps) {
+                    ps.parent.onProtocolError(cause);
+                }
+                // cancel the stream, continue processing
+                onProtocolError(cause);
+                reset();
+            }
+        }
     }
 
     private static final VarHandle STREAM_STATE;
diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/common/HeaderDecoder.java b/src/java.net.http/share/classes/jdk/internal/net/http/common/HeaderDecoder.java
index 62d03844d2e..d81f52e6630 100644
--- a/src/java.net.http/share/classes/jdk/internal/net/http/common/HeaderDecoder.java
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/HeaderDecoder.java
@@ -39,7 +39,11 @@ public class HeaderDecoder extends ValidatingHeadersConsumer {
         String n = name.toString();
         String v = value.toString();
         super.onDecoded(n, v);
-        headersBuilder.addHeader(n, v);
+        addHeader(n, v);
+    }
+
+    protected void addHeader(String name, String value) {
+        headersBuilder.addHeader(name, value);
     }
 
     public HttpHeaders headers() {
diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java b/src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java
index 0e0c9a3eb9e..323042de3d7 100644
--- a/src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java
@@ -541,6 +541,19 @@ public final class Utils {
                 Integer.parseInt(System.getProperty(name, String.valueOf(defaultValue))));
     }
 
+    public static int getIntegerNetProperty(String property, int min, int max, int defaultValue, boolean log) {
+        int value =  Utils.getIntegerNetProperty(property, defaultValue);
+        // use default value if misconfigured
+        if (value < min || value > max) {
+            if (log && Log.errors()) {
+                Log.logError("Property value for {0}={1} not in [{2}..{3}]: " +
+                        "using default={4}", property, value, min, max, defaultValue);
+            }
+            value = defaultValue;
+        }
+        return value;
+    }
+
     public static SSLParameters copySSLParameters(SSLParameters p) {
         SSLParameters p1 = new SSLParameters();
         p1.setAlgorithmConstraints(p.getAlgorithmConstraints());
diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/hpack/Decoder.java b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/Decoder.java
index 9d57a734ac3..881be12c67c 100644
--- a/src/java.net.http/share/classes/jdk/internal/net/http/hpack/Decoder.java
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/Decoder.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2014, 2018, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2014, 2024, 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
@@ -27,6 +27,7 @@ package jdk.internal.net.http.hpack;
 import jdk.internal.net.http.hpack.HPACK.Logger;
 
 import java.io.IOException;
+import java.net.ProtocolException;
 import java.nio.ByteBuffer;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicLong;
@@ -107,12 +108,16 @@ public final class Decoder {
     private final StringReader stringReader;
     private final StringBuilder name;
     private final StringBuilder value;
+    private final int maxHeaderListSize;
+    private final int maxIndexed;
     private int intValue;
     private boolean firstValueRead;
     private boolean firstValueIndex;
     private boolean nameHuffmanEncoded;
     private boolean valueHuffmanEncoded;
     private int capacity;
+    private long size;
+    private int indexed;
 
     /**
      * Constructs a {@code Decoder} with the specified initial capacity of the
@@ -129,6 +134,31 @@ public final class Decoder {
      *         if capacity is negative
      */
     public Decoder(int capacity) {
+        this(capacity, 0, 0);
+    }
+
+    /**
+     * Constructs a {@code Decoder} with the specified initial capacity of the
+     * header table, a max header list size, and a maximum number of literals
+     * with indexing per header section.
+     *
+     * <p> The value of the capacity has to be agreed between decoder and encoder out-of-band,
+     * e.g. by a protocol that uses HPACK
+     * (see <a href="https://tools.ietf.org/html/rfc7541#section-4.2">4.2. Maximum Table Size</a>).
+     *
+     * @param capacity
+     *         a non-negative integer
+     * @param maxHeaderListSize
+     *         a maximum value for the header list size. This is the uncompressed
+     *         names size + uncompressed values size + 32 bytes per field line
+     * @param maxIndexed
+     *         the maximum number of literal with indexing we're prepared to handle
+     *         for a header field section
+     *
+     * @throws IllegalArgumentException
+     *         if capacity is negative
+     */
+    public Decoder(int capacity, int maxHeaderListSize, int maxIndexed) {
         id = DECODERS_IDS.incrementAndGet();
         logger = HPACK.getLogger().subLogger("Decoder#" + id);
         if (logger.isLoggable(NORMAL)) {
@@ -145,6 +175,8 @@ public final class Decoder {
                               toString(), hashCode);
             });
         }
+        this.maxHeaderListSize = maxHeaderListSize;
+        this.maxIndexed = maxIndexed;
         setMaxCapacity0(capacity);
         table = new SimpleHeaderTable(capacity, logger.subLogger("HeaderTable"));
         integerReader = new IntegerReader();
@@ -242,22 +274,25 @@ public final class Decoder {
         requireNonNull(consumer, "consumer");
         if (logger.isLoggable(NORMAL)) {
             logger.log(NORMAL, () -> format("reading %s, end of header block? %s",
-                                            headerBlock, endOfHeaderBlock));
+                    headerBlock, endOfHeaderBlock));
         }
         while (headerBlock.hasRemaining()) {
             proceed(headerBlock, consumer);
         }
         if (endOfHeaderBlock && state != State.READY) {
             logger.log(NORMAL, () -> format("unexpected end of %s representation",
-                                            state));
+                    state));
             throw new IOException("Unexpected end of header block");
         }
+        if (endOfHeaderBlock) {
+            size = indexed = 0;
+        }
     }
 
     private void proceed(ByteBuffer input, DecodingCallback action)
             throws IOException {
         switch (state) {
-            case READY                  ->  resumeReady(input);
+            case READY                  ->  resumeReady(input, action);
             case INDEXED                ->  resumeIndexed(input, action);
             case LITERAL                ->  resumeLiteral(input, action);
             case LITERAL_WITH_INDEXING  ->  resumeLiteralWithIndexing(input, action);
@@ -268,7 +303,7 @@ public final class Decoder {
         }
     }
 
-    private void resumeReady(ByteBuffer input) {
+    private void resumeReady(ByteBuffer input, DecodingCallback action) throws IOException {
         int b = input.get(input.position()) & 0xff; // absolute read
         State s = states.get(b);
         if (logger.isLoggable(EXTRA)) {
@@ -289,6 +324,9 @@ public final class Decoder {
                 }
                 break;
             case LITERAL_WITH_INDEXING:
+                if (maxIndexed > 0 && ++indexed > maxIndexed) {
+                    action.onMaxLiteralWithIndexingReached(indexed, maxIndexed);
+                }
                 state = State.LITERAL_WITH_INDEXING;
                 firstValueIndex = (b & 0b0011_1111) != 0;
                 if (firstValueIndex) {
@@ -315,6 +353,12 @@ public final class Decoder {
         }
     }
 
+    private void checkMaxHeaderListSize(long sz, DecodingCallback consumer) throws ProtocolException {
+        if (maxHeaderListSize > 0 && sz > maxHeaderListSize) {
+            consumer.onMaxHeaderListSizeReached(sz, maxHeaderListSize);
+        }
+    }
+
     //              0   1   2   3   4   5   6   7
     //            +---+---+---+---+---+---+---+---+
     //            | 1 |        Index (7+)         |
@@ -332,6 +376,8 @@ public final class Decoder {
         }
         try {
             SimpleHeaderTable.HeaderField f = getHeaderFieldAt(intValue);
+            size = size + 32 + f.name.length() + f.value.length();
+            checkMaxHeaderListSize(size, action);
             action.onIndexed(intValue, f.name, f.value);
         } finally {
             state = State.READY;
@@ -374,7 +420,7 @@ public final class Decoder {
     //
     private void resumeLiteral(ByteBuffer input, DecodingCallback action)
             throws IOException {
-        if (!completeReading(input)) {
+        if (!completeReading(input, action)) {
             return;
         }
         try {
@@ -385,6 +431,8 @@ public final class Decoder {
                             intValue, value, valueHuffmanEncoded));
                 }
                 SimpleHeaderTable.HeaderField f = getHeaderFieldAt(intValue);
+                size = size + 32 + f.name.length() + value.length();
+                checkMaxHeaderListSize(size, action);
                 action.onLiteral(intValue, f.name, value, valueHuffmanEncoded);
             } else {
                 if (logger.isLoggable(NORMAL)) {
@@ -392,6 +440,8 @@ public final class Decoder {
                             "literal without indexing ('%s', huffman=%b, '%s', huffman=%b)",
                             name, nameHuffmanEncoded, value, valueHuffmanEncoded));
                 }
+                size = size + 32 + name.length() + value.length();
+                checkMaxHeaderListSize(size, action);
                 action.onLiteral(name, nameHuffmanEncoded, value, valueHuffmanEncoded);
             }
         } finally {
@@ -425,7 +475,7 @@ public final class Decoder {
     private void resumeLiteralWithIndexing(ByteBuffer input,
                                            DecodingCallback action)
             throws IOException {
-        if (!completeReading(input)) {
+        if (!completeReading(input, action)) {
             return;
         }
         try {
@@ -445,6 +495,8 @@ public final class Decoder {
                 }
                 SimpleHeaderTable.HeaderField f = getHeaderFieldAt(intValue);
                 n = f.name;
+                size = size + 32 + n.length() + v.length();
+                checkMaxHeaderListSize(size, action);
                 action.onLiteralWithIndexing(intValue, n, v, valueHuffmanEncoded);
             } else {
                 n = name.toString();
@@ -453,6 +505,8 @@ public final class Decoder {
                             "literal with incremental indexing ('%s', huffman=%b, '%s', huffman=%b)",
                             n, nameHuffmanEncoded, value, valueHuffmanEncoded));
                 }
+                size = size + 32 + n.length() + v.length();
+                checkMaxHeaderListSize(size, action);
                 action.onLiteralWithIndexing(n, nameHuffmanEncoded, v, valueHuffmanEncoded);
             }
             table.put(n, v);
@@ -486,7 +540,7 @@ public final class Decoder {
     private void resumeLiteralNeverIndexed(ByteBuffer input,
                                            DecodingCallback action)
             throws IOException {
-        if (!completeReading(input)) {
+        if (!completeReading(input, action)) {
             return;
         }
         try {
@@ -497,6 +551,8 @@ public final class Decoder {
                             intValue, value, valueHuffmanEncoded));
                 }
                 SimpleHeaderTable.HeaderField f = getHeaderFieldAt(intValue);
+                size = size + 32 + f.name.length() + value.length();
+                checkMaxHeaderListSize(size, action);
                 action.onLiteralNeverIndexed(intValue, f.name, value, valueHuffmanEncoded);
             } else {
                 if (logger.isLoggable(NORMAL)) {
@@ -504,6 +560,8 @@ public final class Decoder {
                             "literal never indexed ('%s', huffman=%b, '%s', huffman=%b)",
                             name, nameHuffmanEncoded, value, valueHuffmanEncoded));
                 }
+                size = size + 32 + name.length() + value.length();
+                checkMaxHeaderListSize(size, action);
                 action.onLiteralNeverIndexed(name, nameHuffmanEncoded, value, valueHuffmanEncoded);
             }
         } finally {
@@ -541,7 +599,7 @@ public final class Decoder {
         }
     }
 
-    private boolean completeReading(ByteBuffer input) throws IOException {
+    private boolean completeReading(ByteBuffer input, DecodingCallback action) throws IOException {
         if (!firstValueRead) {
             if (firstValueIndex) {
                 if (!integerReader.read(input)) {
@@ -551,6 +609,8 @@ public final class Decoder {
                 integerReader.reset();
             } else {
                 if (!stringReader.read(input, name)) {
+                    long sz = size + 32 + name.length();
+                    checkMaxHeaderListSize(sz, action);
                     return false;
                 }
                 nameHuffmanEncoded = stringReader.isHuffmanEncoded();
@@ -560,6 +620,8 @@ public final class Decoder {
             return false;
         } else {
             if (!stringReader.read(input, value)) {
+                long sz = size + 32 + name.length() + value.length();
+                checkMaxHeaderListSize(sz, action);
                 return false;
             }
         }
diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/hpack/DecodingCallback.java b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/DecodingCallback.java
index 5e9df860feb..228f9bf0206 100644
--- a/src/java.net.http/share/classes/jdk/internal/net/http/hpack/DecodingCallback.java
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/DecodingCallback.java
@@ -24,6 +24,7 @@
  */
 package jdk.internal.net.http.hpack;
 
+import java.net.ProtocolException;
 import java.nio.ByteBuffer;
 
 /**
@@ -292,4 +293,17 @@ public interface DecodingCallback {
      *         new capacity of the header table
      */
     default void onSizeUpdate(int capacity) { }
+
+    default void onMaxHeaderListSizeReached(long size, int maxHeaderListSize)
+        throws ProtocolException {
+        throw new ProtocolException(String
+                .format("Size exceeds MAX_HEADERS_LIST_SIZE: %s > %s",
+                        size, maxHeaderListSize));
+    }
+
+    default void onMaxLiteralWithIndexingReached(long indexed, int maxIndexed)
+            throws ProtocolException {
+        throw new ProtocolException(String.format("Too many literal with indexing: %s > %s",
+                indexed, maxIndexed));
+    }
 }
diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/hpack/Encoder.java b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/Encoder.java
index 4188937b1ad..c603e917ca4 100644
--- a/src/java.net.http/share/classes/jdk/internal/net/http/hpack/Encoder.java
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/Encoder.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2014, 2018, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2014, 2024, 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
@@ -258,9 +258,10 @@ public class Encoder {
                 }
             }
         }
+        assert encoding : "encoding is false";
     }
 
-    private boolean isHuffmanBetterFor(CharSequence value) {
+    protected final boolean isHuffmanBetterFor(CharSequence value) {
         // prefer Huffman encoding only if it is strictly smaller than Latin-1
         return huffmanWriter.lengthOf(value) < value.length();
     }
@@ -340,6 +341,10 @@ public class Encoder {
         return 0;
     }
 
+    protected final int tableIndexOf(CharSequence name, CharSequence value) {
+        return getHeaderTable().indexOf(name, value);
+    }
+
     /**
      * Encodes the {@linkplain #header(CharSequence, CharSequence) set up}
      * header into the given buffer.
@@ -380,6 +385,7 @@ public class Encoder {
             writer.reset(); // FIXME: WHY?
             encoding = false;
         }
+        assert done || encoding : "done: " + done + ", encoding: " + encoding;
         return done;
     }
 
@@ -542,4 +548,8 @@ public class Encoder {
                     "Previous encoding operation hasn't finished yet");
         }
     }
+
+    protected final Logger logger() {
+        return logger;
+    }
 }
diff --git a/src/jdk.httpserver/share/classes/sun/net/httpserver/Request.java b/src/jdk.httpserver/share/classes/sun/net/httpserver/Request.java
index 03e2a9ee8f5..0e136d80437 100644
--- a/src/jdk.httpserver/share/classes/sun/net/httpserver/Request.java
+++ b/src/jdk.httpserver/share/classes/sun/net/httpserver/Request.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2005, 2021, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2005, 2024, 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
@@ -42,8 +42,10 @@ class Request {
     private SocketChannel chan;
     private InputStream is;
     private OutputStream os;
+    private final int maxReqHeaderSize;
 
     Request (InputStream rawInputStream, OutputStream rawout) throws IOException {
+        this.maxReqHeaderSize = ServerConfig.getMaxReqHeaderSize();
         is = rawInputStream;
         os = rawout;
         do {
@@ -76,6 +78,7 @@ class Request {
     public String readLine () throws IOException {
         boolean gotCR = false, gotLF = false;
         pos = 0; lineBuf = new StringBuffer();
+        long lsize = 32;
         while (!gotLF) {
             int c = is.read();
             if (c == -1) {
@@ -88,20 +91,27 @@ class Request {
                     gotCR = false;
                     consume (CR);
                     consume (c);
+                    lsize = lsize + 2;
                 }
             } else {
                 if (c == CR) {
                     gotCR = true;
                 } else {
                     consume (c);
+                    lsize = lsize + 1;
                 }
             }
+            if (maxReqHeaderSize > 0 && lsize > maxReqHeaderSize) {
+                throw new IOException("Maximum header (" +
+                        "sun.net.httpserver.maxReqHeaderSize) exceeded, " +
+                        ServerConfig.getMaxReqHeaderSize() + ".");
+            }
         }
         lineBuf.append (buf, 0, pos);
         return new String (lineBuf);
     }
 
-    private void consume (int c) {
+    private void consume (int c) throws IOException {
         if (pos == BUF_LEN) {
             lineBuf.append (buf);
             pos = 0;
@@ -139,13 +149,22 @@ class Request {
             len = 1;
             firstc = c;
         }
+        long hsize = startLine.length() + 32L;
 
         while (firstc != LF && firstc != CR && firstc >= 0) {
             int keyend = -1;
             int c;
             boolean inKey = firstc > ' ';
             s[len++] = (char) firstc;
+            hsize = hsize + 1;
     parseloop:{
+                // We start parsing for a new name value pair here.
+                // The max header size includes an overhead of 32 bytes per
+                // name value pair.
+                // See SETTINGS_MAX_HEADER_LIST_SIZE, RFC 9113, section 6.5.2.
+                long maxRemaining = maxReqHeaderSize > 0
+                        ? maxReqHeaderSize - hsize - 32
+                        : Long.MAX_VALUE;
                 while ((c = is.read()) >= 0) {
                     switch (c) {
                       /*fallthrough*/
@@ -179,6 +198,11 @@ class Request {
                         s = ns;
                     }
                     s[len++] = (char) c;
+                    if (maxReqHeaderSize > 0 && len > maxRemaining) {
+                        throw new IOException("Maximum header (" +
+                                "sun.net.httpserver.maxReqHeaderSize) exceeded, " +
+                                ServerConfig.getMaxReqHeaderSize() + ".");
+                    }
                 }
                 firstc = -1;
             }
@@ -206,6 +230,13 @@ class Request {
                         "sun.net.httpserver.maxReqHeaders) exceeded, " +
                         ServerConfig.getMaxReqHeaders() + ".");
             }
+            hsize = hsize + len + 32;
+            if (maxReqHeaderSize > 0 && hsize > maxReqHeaderSize) {
+                throw new IOException("Maximum header (" +
+                        "sun.net.httpserver.maxReqHeaderSize) exceeded, " +
+                        ServerConfig.getMaxReqHeaderSize() + ".");
+            }
+
             if (k == null) {  // Headers disallows null keys, use empty string
                 k = "";       // instead to represent invalid key
             }
diff --git a/src/jdk.httpserver/share/classes/sun/net/httpserver/ServerConfig.java b/src/jdk.httpserver/share/classes/sun/net/httpserver/ServerConfig.java
index 303db604a19..63d6a57258a 100644
--- a/src/jdk.httpserver/share/classes/sun/net/httpserver/ServerConfig.java
+++ b/src/jdk.httpserver/share/classes/sun/net/httpserver/ServerConfig.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2005, 2022, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2005, 2024, 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
@@ -49,6 +49,7 @@ class ServerConfig {
     // timing out request/response if max request/response time is configured
     private static final long DEFAULT_REQ_RSP_TIMER_TASK_SCHEDULE_MILLIS = 1000;
     private static final int  DEFAULT_MAX_REQ_HEADERS = 200;
+    private static final int  DEFAULT_MAX_REQ_HEADER_SIZE = 380 * 1024;
     private static final long DEFAULT_DRAIN_AMOUNT = 64 * 1024;
 
     private static long idleTimerScheduleMillis;
@@ -62,6 +63,9 @@ class ServerConfig {
     private static int maxIdleConnections;
     // The maximum number of request headers allowable
     private static int maxReqHeaders;
+    // a maximum value for the header list size. This is the
+    // names size + values size + 32 bytes per field line
+    private static int maxReqHeadersSize;
     // max time a request or response is allowed to take
     private static long maxReqTime;
     private static long maxRspTime;
@@ -104,6 +108,14 @@ class ServerConfig {
                             "sun.net.httpserver.maxReqHeaders",
                             DEFAULT_MAX_REQ_HEADERS);
 
+                    // a value <= 0 means unlimited
+                    maxReqHeadersSize = Integer.getInteger(
+                            "sun.net.httpserver.maxReqHeaderSize",
+                            DEFAULT_MAX_REQ_HEADER_SIZE);
+                    if (maxReqHeadersSize <= 0) {
+                        maxReqHeadersSize = 0;
+                    }
+
                     maxReqTime = Long.getLong("sun.net.httpserver.maxReqTime",
                             DEFAULT_MAX_REQ_TIME);
 
@@ -212,6 +224,10 @@ class ServerConfig {
         return maxReqHeaders;
     }
 
+    static int getMaxReqHeaderSize() {
+        return maxReqHeadersSize;
+    }
+
     /**
      * @return Returns the maximum amount of time the server will wait for the request to be read
      * completely. This method can return a value of 0 or negative to imply no maximum limit has
diff --git a/test/jdk/java/net/httpclient/http2/PushPromiseContinuation.java b/test/jdk/java/net/httpclient/http2/PushPromiseContinuation.java
index 152b3d194eb..cc6bc1a9515 100644
--- a/test/jdk/java/net/httpclient/http2/PushPromiseContinuation.java
+++ b/test/jdk/java/net/httpclient/http2/PushPromiseContinuation.java
@@ -49,6 +49,7 @@ import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.net.ProtocolException;
 import java.net.URI;
 import java.net.http.HttpClient;
 import java.net.http.HttpHeaders;
@@ -192,7 +193,8 @@ public class PushPromiseContinuation {
                 client.sendAsync(hreq, HttpResponse.BodyHandlers.ofString(UTF_8), pph);
 
         CompletionException t = expectThrows(CompletionException.class, () -> cf.join());
-        assertEquals(t.getCause().getClass(), IOException.class, "Expected an IOException but got " + t.getCause());
+        assertEquals(t.getCause().getClass(), ProtocolException.class,
+                "Expected a ProtocolException but got " + t.getCause());
         System.err.println("Client received the following expected exception: " + t.getCause());
         faultyServer.stop();
     }
@@ -219,7 +221,10 @@ public class PushPromiseContinuation {
 
     static class Http2PushPromiseHeadersExchangeImpl extends Http2TestExchangeImpl {
 
-        Http2PushPromiseHeadersExchangeImpl(int streamid, String method, HttpHeaders reqheaders, HttpHeadersBuilder rspheadersBuilder, URI uri, InputStream is, SSLSession sslSession, BodyOutputStream os, Http2TestServerConnection conn, boolean pushAllowed) {
+        Http2PushPromiseHeadersExchangeImpl(int streamid, String method, HttpHeaders reqheaders,
+                                            HttpHeadersBuilder rspheadersBuilder, URI uri, InputStream is,
+                                            SSLSession sslSession, BodyOutputStream os,
+                                            Http2TestServerConnection conn, boolean pushAllowed) {
             super(streamid, method, reqheaders, rspheadersBuilder, uri, is, sslSession, os, conn, pushAllowed);
         }
 
diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/common/HttpServerAdapters.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/common/HttpServerAdapters.java
index 623644df5a6..6bbcc4c3741 100644
--- a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/common/HttpServerAdapters.java
+++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/common/HttpServerAdapters.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2018, 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2018, 2024, 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
@@ -240,6 +240,7 @@ public interface HttpServerAdapters {
         public abstract String getRequestMethod();
         public abstract void close();
         public abstract InetSocketAddress getRemoteAddress();
+        public abstract InetSocketAddress getLocalAddress();
         public void serverPush(URI uri, HttpHeaders headers, byte[] body) {
             ByteArrayInputStream bais = new ByteArrayInputStream(body);
             serverPush(uri, headers, bais);
@@ -302,7 +303,10 @@ public interface HttpServerAdapters {
             public InetSocketAddress getRemoteAddress() {
                 return exchange.getRemoteAddress();
             }
-
+            @Override
+            public InetSocketAddress getLocalAddress() {
+                return exchange.getLocalAddress();
+            }
             @Override
             public URI getRequestURI() { return exchange.getRequestURI(); }
             @Override
@@ -363,6 +367,10 @@ public interface HttpServerAdapters {
             public InetSocketAddress getRemoteAddress() {
                 return exchange.getRemoteAddress();
             }
+            @Override
+            public InetSocketAddress getLocalAddress() {
+                return exchange.getLocalAddress();
+            }
 
             @Override
             public URI getRequestURI() { return exchange.getRequestURI(); }
diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/HpackTestEncoder.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/HpackTestEncoder.java
new file mode 100644
index 00000000000..f54a4a766b8
--- /dev/null
+++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/HpackTestEncoder.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (c) 2015, 2023, 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.
+ */
+
+package jdk.httpclient.test.lib.http2;
+
+import java.util.function.*;
+
+import jdk.internal.net.http.hpack.Encoder;
+
+import static java.lang.String.format;
+import static java.util.Objects.requireNonNull;
+import static jdk.internal.net.http.hpack.HPACK.Logger.Level.EXTRA;
+import static jdk.internal.net.http.hpack.HPACK.Logger.Level.NORMAL;
+
+public class HpackTestEncoder extends Encoder {
+
+    public HpackTestEncoder(int maxCapacity) {
+        super(maxCapacity);
+    }
+
+    /**
+     * Sets up the given header {@code (name, value)} with possibly sensitive
+     * value.
+     *
+     * <p> If the {@code value} is sensitive (think security, secrecy, etc.)
+     * this encoder will compress it using a special representation
+     * (see <a href="https://tools.ietf.org/html/rfc7541#section-6.2.3">6.2.3.  Literal Header Field Never Indexed</a>).
+     *
+     * <p> Fixates {@code name} and {@code value} for the duration of encoding.
+     *
+     * @param name
+     *         the name
+     * @param value
+     *         the value
+     * @param sensitive
+     *         whether or not the value is sensitive
+     *
+     * @throws NullPointerException
+     *         if any of the arguments are {@code null}
+     * @throws IllegalStateException
+     *         if the encoder hasn't fully encoded the previous header, or
+     *         hasn't yet started to encode it
+     * @see #header(CharSequence, CharSequence)
+     * @see DecodingCallback#onDecoded(CharSequence, CharSequence, boolean)
+     */
+    public void header(CharSequence name,
+                       CharSequence value,
+                       boolean sensitive) throws IllegalStateException {
+        if (sensitive || getMaxCapacity() == 0) {
+            super.header(name, value, true);
+        } else {
+            header(name, value, false, (n,v) -> false);
+        }
+    }
+    /**
+     * Sets up the given header {@code (name, value)} with possibly sensitive
+     * value.
+     *
+     * <p> If the {@code value} is sensitive (think security, secrecy, etc.)
+     * this encoder will compress it using a special representation
+     * (see <a href="https://tools.ietf.org/html/rfc7541#section-6.2.3">6.2.3.  Literal Header Field Never Indexed</a>).
+     *
+     * <p> Fixates {@code name} and {@code value} for the duration of encoding.
+     *
+     * @param name
+     *         the name
+     * @param value
+     *         the value
+     * @param insertionPolicy
+     *         a bipredicate to indicate whether a name value pair
+     *         should be added to the dynamic table
+     *
+     * @throws NullPointerException
+     *         if any of the arguments are {@code null}
+     * @throws IllegalStateException
+     *         if the encoder hasn't fully encoded the previous header, or
+     *         hasn't yet started to encode it
+     * @see #header(CharSequence, CharSequence)
+     * @see DecodingCallback#onDecoded(CharSequence, CharSequence, boolean)
+     */
+    public void header(CharSequence name,
+                       CharSequence value,
+                       BiPredicate<CharSequence, CharSequence> insertionPolicy)
+            throws IllegalStateException {
+        header(name, value, false, insertionPolicy);
+    }
+
+    /**
+     * Sets up the given header {@code (name, value)} with possibly sensitive
+     * value.
+     *
+     * <p> If the {@code value} is sensitive (think security, secrecy, etc.)
+     * this encoder will compress it using a special representation
+     * (see <a href="https://tools.ietf.org/html/rfc7541#section-6.2.3">
+     *     6.2.3.  Literal Header Field Never Indexed</a>).
+     *
+     * <p> Fixates {@code name} and {@code value} for the duration of encoding.
+     *
+     * @param name
+     *         the name
+     * @param value
+     *         the value
+     * @param sensitive
+     *         whether or not the value is sensitive
+     * @param insertionPolicy
+     *         a bipredicate to indicate whether a name value pair
+     *         should be added to the dynamic table
+     *
+     * @throws NullPointerException
+     *         if any of the arguments are {@code null}
+     * @throws IllegalStateException
+     *         if the encoder hasn't fully encoded the previous header, or
+     *         hasn't yet started to encode it
+     * @see #header(CharSequence, CharSequence)
+     * @see DecodingCallback#onDecoded(CharSequence, CharSequence, boolean)
+     */
+    public void header(CharSequence name,
+                       CharSequence value,
+                       boolean sensitive,
+                       BiPredicate<CharSequence, CharSequence> insertionPolicy)
+            throws IllegalStateException {
+        if (sensitive == true || getMaxCapacity() == 0 || !insertionPolicy.test(name, value)) {
+            super.header(name, value, sensitive);
+            return;
+        }
+        var logger = logger();
+        // Arguably a good balance between complexity of implementation and
+        // efficiency of encoding
+        requireNonNull(name, "name");
+        requireNonNull(value, "value");
+        var t = getHeaderTable();
+        int index = tableIndexOf(name, value);
+        if (logger.isLoggable(NORMAL)) {
+            logger.log(NORMAL, () -> format("encoding with indexing ('%s', '%s'): index:%s",
+                    name, value, index));
+        }
+        if (index > 0) {
+            indexed(index);
+        } else {
+            boolean huffmanValue = isHuffmanBetterFor(value);
+            if (index < 0) {
+                literalWithIndexing(-index, value, huffmanValue);
+            } else {
+                boolean huffmanName = isHuffmanBetterFor(name);
+                literalWithIndexing(name, huffmanName, value, huffmanValue);
+            }
+        }
+    }
+
+    protected int calculateCapacity(int maxCapacity) {
+        return maxCapacity;
+    }
+
+}
diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestExchange.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestExchange.java
index 208a8d54e08..1a8b5d92af5 100644
--- a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestExchange.java
+++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestExchange.java
@@ -29,9 +29,12 @@ import java.io.OutputStream;
 import java.net.URI;
 import java.net.InetSocketAddress;
 import java.net.http.HttpHeaders;
+import java.util.List;
 import java.util.concurrent.CompletableFuture;
+import java.util.function.BiPredicate;
 import javax.net.ssl.SSLSession;
 import jdk.internal.net.http.common.HttpHeadersBuilder;
+import jdk.internal.net.http.frame.Http2Frame;
 
 public interface Http2TestExchange {
 
@@ -53,6 +56,12 @@ public interface Http2TestExchange {
 
     void sendResponseHeaders(int rCode, long responseLength) throws IOException;
 
+    default void sendResponseHeaders(int rCode, long responseLength,
+                                     BiPredicate<CharSequence, CharSequence> insertionPolicy)
+            throws IOException {
+        sendResponseHeaders(rCode, responseLength);
+    }
+
     InetSocketAddress getRemoteAddress();
 
     int getResponseCode();
@@ -65,6 +74,10 @@ public interface Http2TestExchange {
 
     void serverPush(URI uri, HttpHeaders headers, InputStream content);
 
+    default void sendFrames(List<Http2Frame> frames) throws IOException {
+        throw new UnsupportedOperationException("not implemented");
+    }
+
     /**
      * Send a PING on this exchanges connection, and completes the returned CF
      * with the number of milliseconds it took to get a valid response.
diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestExchangeImpl.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestExchangeImpl.java
index 8f7c154e656..5ac6ab0fd26 100644
--- a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestExchangeImpl.java
+++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestExchangeImpl.java
@@ -26,6 +26,7 @@ package jdk.httpclient.test.lib.http2;
 import jdk.internal.net.http.common.HttpHeadersBuilder;
 import jdk.internal.net.http.frame.HeaderFrame;
 import jdk.internal.net.http.frame.HeadersFrame;
+import jdk.internal.net.http.frame.Http2Frame;
 
 import javax.net.ssl.SSLSession;
 import java.io.IOException;
@@ -37,6 +38,7 @@ import java.net.http.HttpHeaders;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.CompletableFuture;
+import java.util.function.BiPredicate;
 
 public class Http2TestExchangeImpl implements Http2TestExchange {
 
@@ -130,8 +132,13 @@ public class Http2TestExchangeImpl implements Http2TestExchange {
         return os;
     }
 
-    @Override
     public void sendResponseHeaders(int rCode, long responseLength) throws IOException {
+        sendResponseHeaders(rCode, responseLength, (n,v) -> false);
+    }
+    @Override
+    public void sendResponseHeaders(int rCode, long responseLength,
+                                    BiPredicate<CharSequence, CharSequence> insertionPolicy)
+            throws IOException {
         this.responseLength = responseLength;
         if (responseLength !=0 && rCode != 204 && !isHeadRequest()) {
                 long clen = responseLength > 0 ? responseLength : 0;
@@ -142,7 +149,7 @@ public class Http2TestExchangeImpl implements Http2TestExchange {
         HttpHeaders headers = rspheadersBuilder.build();
 
         Http2TestServerConnection.ResponseHeaders response
-                = new Http2TestServerConnection.ResponseHeaders(headers);
+                = new Http2TestServerConnection.ResponseHeaders(headers, insertionPolicy);
         response.streamid(streamid);
         response.setFlag(HeaderFrame.END_HEADERS);
 
@@ -156,6 +163,11 @@ public class Http2TestExchangeImpl implements Http2TestExchange {
         System.err.println("Sent response headers " + rCode);
     }
 
+    @Override
+    public void sendFrames(List<Http2Frame> frames) throws IOException {
+        conn.sendFrames(frames);
+    }
+
     @Override
     public InetSocketAddress getRemoteAddress() {
         return (InetSocketAddress) conn.socket.getRemoteSocketAddress();
diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestServerConnection.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestServerConnection.java
index 94f11e843ce..102c7a56721 100644
--- a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestServerConnection.java
+++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestServerConnection.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2024, 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,6 +24,8 @@
 package jdk.httpclient.test.lib.http2;
 
 import jdk.internal.net.http.common.HttpHeadersBuilder;
+import jdk.internal.net.http.common.Log;
+import jdk.internal.net.http.frame.ContinuationFrame;
 import jdk.internal.net.http.frame.DataFrame;
 import jdk.internal.net.http.frame.ErrorFrame;
 import jdk.internal.net.http.frame.FramesDecoder;
@@ -78,10 +80,13 @@ import java.util.Random;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.ExecutorService;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.function.BiPredicate;
 import java.util.function.Consumer;
 
 import static java.nio.charset.StandardCharsets.ISO_8859_1;
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static jdk.internal.net.http.frame.SettingsFrame.DEFAULT_MAX_FRAME_SIZE;
 import static jdk.internal.net.http.frame.SettingsFrame.HEADER_TABLE_SIZE;
 
 /**
@@ -100,7 +105,7 @@ public class Http2TestServerConnection {
     final Http2TestExchangeSupplier exchangeSupplier;
     final InputStream is;
     final OutputStream os;
-    volatile Encoder hpackOut;
+    volatile HpackTestEncoder hpackOut;
     volatile Decoder hpackIn;
     volatile SettingsFrame clientSettings;
     final SettingsFrame serverSettings;
@@ -393,7 +398,9 @@ public class Http2TestServerConnection {
     }
 
     public int getMaxFrameSize() {
-        return clientSettings.getParameter(SettingsFrame.MAX_FRAME_SIZE);
+        var max = clientSettings.getParameter(SettingsFrame.MAX_FRAME_SIZE);
+        if (max <= 0) max = DEFAULT_MAX_FRAME_SIZE;
+        return max;
     }
 
     /** Sends a pre-canned HTTP/1.1 response. */
@@ -454,7 +461,7 @@ public class Http2TestServerConnection {
         //System.out.println("ServerSettings: " + serverSettings);
         //System.out.println("ClientSettings: " + clientSettings);
 
-        hpackOut = new Encoder(serverSettings.getParameter(HEADER_TABLE_SIZE));
+        hpackOut = new HpackTestEncoder(serverSettings.getParameter(HEADER_TABLE_SIZE));
         hpackIn = new Decoder(clientSettings.getParameter(HEADER_TABLE_SIZE));
 
         if (!secure) {
@@ -740,6 +747,14 @@ public class Http2TestServerConnection {
         }
     }
 
+    public void sendFrames(List<Http2Frame> frames) throws IOException {
+        synchronized (outputQ) {
+            for (var frame : frames) {
+                outputQ.put(frame);
+            }
+        }
+    }
+
     protected HttpHeadersBuilder createNewHeadersBuilder() {
         return new HttpHeadersBuilder();
     }
@@ -850,26 +865,38 @@ public class Http2TestServerConnection {
         return (streamid & 0x01) == 0x00;
     }
 
+    final ReentrantLock headersLock = new ReentrantLock();
+
     /** Encodes an group of headers, without any ordering guarantees. */
     public List<ByteBuffer> encodeHeaders(HttpHeaders headers) {
+        return encodeHeaders(headers, (n,v) -> false);
+    }
+
+    public List<ByteBuffer> encodeHeaders(HttpHeaders headers,
+                                          BiPredicate<CharSequence, CharSequence> insertionPolicy) {
         List<ByteBuffer> buffers = new LinkedList<>();
 
         ByteBuffer buf = getBuffer();
         boolean encoded;
-        for (Map.Entry<String, List<String>> entry : headers.map().entrySet()) {
-            List<String> values = entry.getValue();
-            String key = entry.getKey().toLowerCase();
-            for (String value : values) {
-                do {
-                    hpackOut.header(key, value);
-                    encoded = hpackOut.encode(buf);
-                    if (!encoded) {
-                        buf.flip();
-                        buffers.add(buf);
-                        buf = getBuffer();
-                    }
-                } while (!encoded);
+        headersLock.lock();
+        try {
+            for (Map.Entry<String, List<String>> entry : headers.map().entrySet()) {
+                List<String> values = entry.getValue();
+                String key = entry.getKey().toLowerCase();
+                for (String value : values) {
+                    hpackOut.header(key, value, insertionPolicy);
+                    do {
+                        encoded = hpackOut.encode(buf);
+                        if (!encoded && !buf.hasRemaining()) {
+                            buf.flip();
+                            buffers.add(buf);
+                            buf = getBuffer();
+                        }
+                    } while (!encoded);
+                }
             }
+        } finally {
+            headersLock.unlock();
         }
         buf.flip();
         buffers.add(buf);
@@ -882,18 +909,23 @@ public class Http2TestServerConnection {
 
         ByteBuffer buf = getBuffer();
         boolean encoded;
-        for (Map.Entry<String, String> entry : headers) {
-            String value = entry.getValue();
-            String key = entry.getKey().toLowerCase();
-            do {
+        headersLock.lock();
+        try {
+            for (Map.Entry<String, String> entry : headers) {
+                String value = entry.getValue();
+                String key = entry.getKey().toLowerCase();
                 hpackOut.header(key, value);
-                encoded = hpackOut.encode(buf);
-                if (!encoded) {
-                    buf.flip();
-                    buffers.add(buf);
-                    buf = getBuffer();
-                }
-            } while (!encoded);
+                do {
+                    encoded = hpackOut.encode(buf);
+                    if (!encoded && !buf.hasRemaining()) {
+                        buf.flip();
+                        buffers.add(buf);
+                        buf = getBuffer();
+                    }
+                } while (!encoded);
+            }
+        } finally {
+            headersLock.unlock();
         }
         buf.flip();
         buffers.add(buf);
@@ -920,10 +952,50 @@ public class Http2TestServerConnection {
                         break;
                     } else throw x;
                 }
-                if (frame instanceof ResponseHeaders) {
-                    ResponseHeaders rh = (ResponseHeaders)frame;
-                    HeadersFrame hf = new HeadersFrame(rh.streamid(), rh.getFlags(), encodeHeaders(rh.headers));
-                    writeFrame(hf);
+                if (frame instanceof ResponseHeaders rh) {
+                    var buffers = encodeHeaders(rh.headers, rh.insertionPolicy);
+                    int maxFrameSize = Math.min(rh.getMaxFrameSize(), getMaxFrameSize() - 64);
+                    int next = 0;
+                    int cont = 0;
+                    do {
+                        // If the total size of headers exceeds the max frame
+                        // size we need to split the headers into one
+                        // HeadersFrame + N x ContinuationFrames
+                        int remaining = maxFrameSize;
+                        var list = new ArrayList<ByteBuffer>(buffers.size());
+                        for (; next < buffers.size(); next++) {
+                            var b = buffers.get(next);
+                            var len = b.remaining();
+                            if (!b.hasRemaining()) continue;
+                            if (len <= remaining) {
+                                remaining -= len;
+                                list.add(b);
+                            } else {
+                                if (next == 0) {
+                                    list.add(b.slice(b.position(), remaining));
+                                    b.position(b.position() + remaining);
+                                    remaining = 0;
+                                }
+                                break;
+                            }
+                        }
+                        int flags = rh.getFlags();
+                        if (next != buffers.size()) {
+                            flags = flags & ~HeadersFrame.END_HEADERS;
+                        }
+                        if (cont > 0)  {
+                            flags = flags & ~HeadersFrame.END_STREAM;
+                        }
+                        HeaderFrame hf = cont == 0
+                                ? new HeadersFrame(rh.streamid(), flags, list)
+                                : new ContinuationFrame(rh.streamid(), flags, list);
+                        if (Log.headers()) {
+                            // avoid too much chatter: log only if Log.headers() is enabled
+                            System.err.println("TestServer writing " + hf);
+                        }
+                        writeFrame(hf);
+                        cont++;
+                    } while (next < buffers.size());
                 } else if (frame instanceof OutgoingPushPromise) {
                     handlePush((OutgoingPushPromise)frame);
                 } else
@@ -1234,11 +1306,29 @@ public class Http2TestServerConnection {
     // for the hashmap.
 
     static class ResponseHeaders extends Http2Frame {
-        HttpHeaders headers;
+        final HttpHeaders headers;
+        final BiPredicate<CharSequence, CharSequence> insertionPolicy;
 
-        ResponseHeaders(HttpHeaders headers) {
+        final int maxFrameSize;
+
+        public ResponseHeaders(HttpHeaders headers) {
+            this(headers, (n,v) -> false);
+        }
+        public ResponseHeaders(HttpHeaders headers, BiPredicate<CharSequence, CharSequence> insertionPolicy) {
+            this(headers, insertionPolicy, Integer.MAX_VALUE);
+        }
+
+        public ResponseHeaders(HttpHeaders headers,
+                               BiPredicate<CharSequence, CharSequence> insertionPolicy,
+                               int maxFrameSize) {
             super(0, 0);
             this.headers = headers;
+            this.insertionPolicy = insertionPolicy;
+            this.maxFrameSize = maxFrameSize;
+        }
+
+        public int getMaxFrameSize() {
+            return maxFrameSize;
         }
 
     }
-- 
GitLab