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 94b8505da47bd..bd92508725632 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 @@ -54,6 +54,7 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; @@ -1614,12 +1615,18 @@ private List encodeHeaders(OutgoingHeaders> frame) { // There can be no concurrent access to this buffer as all access to this buffer // and its content happen within a single critical code block section protected // by the sendLock. / (see sendFrame()) - // private final ByteBufferPool headerEncodingPool = new ByteBufferPool(); + // The cached buffer is never passed to the I/O layer directly. Instead, + // `encodeHeadersImpl()` snapshots the encoded data into compact copies, so + // the cached buffer is safe to reuse immediately. + private ByteBuffer cachedHeaderBuffer; private ByteBuffer getHeaderBuffer(int size) { - ByteBuffer buf = ByteBuffer.allocate(size); - buf.limit(size); - return buf; + if (cachedHeaderBuffer == null || cachedHeaderBuffer.capacity() < size) { + cachedHeaderBuffer = ByteBuffer.allocate(size); + } + cachedHeaderBuffer.clear(); + cachedHeaderBuffer.limit(size); + return cachedHeaderBuffer; } /* @@ -1634,8 +1641,16 @@ private ByteBuffer getHeaderBuffer(int size) { * encoding in HTTP/2... */ private List encodeHeadersImpl(int bufferSize, HttpHeaders... headers) { - ByteBuffer buffer = getHeaderBuffer(bufferSize); List buffers = new ArrayList<>(); + Consumer captureAndAddToBuffers = (this_buffer) -> { + this_buffer.flip(); + ByteBuffer copy = ByteBuffer.allocate(this_buffer.remaining()); + copy.put(this_buffer); + copy.flip(); + buffers.add(copy); + }; + + ByteBuffer buffer = getHeaderBuffer(bufferSize); for (HttpHeaders header : headers) { for (Map.Entry> e : header.map().entrySet()) { String lKey = e.getKey().toLowerCase(Locale.US); @@ -1644,16 +1659,15 @@ private List encodeHeadersImpl(int bufferSize, HttpHeaders... header hpackOut.header(lKey, value); while (!hpackOut.encode(buffer)) { if (!buffer.hasRemaining()) { - buffer.flip(); - buffers.add(buffer); - buffer = getHeaderBuffer(bufferSize); + captureAndAddToBuffers.accept(buffer); + buffer.clear(); + buffer.limit(bufferSize); } } } } } - buffer.flip(); - buffers.add(buffer); + captureAndAddToBuffers.accept(buffer); return buffers; } diff --git a/test/jdk/java/net/httpclient/http2/HeaderEncodingBufferReuseTest.java b/test/jdk/java/net/httpclient/http2/HeaderEncodingBufferReuseTest.java new file mode 100644 index 0000000000000..cf7b9d3e99b5c --- /dev/null +++ b/test/jdk/java/net/httpclient/http2/HeaderEncodingBufferReuseTest.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @summary Verifies that Http2Connection.cachedHeaderBuffer is reused + * across multiple requests on the same connection. + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.httpclient.test.lib.http2.Http2TestServer + * @run main/othervm + * --add-opens java.net.http/jdk.internal.net.http=ALL-UNNAMED + * HeaderEncodingBufferReuseTest + */ + +import java.io.IOException; +import java.lang.reflect.Field; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.nio.ByteBuffer; +import java.util.Map; + +import jdk.httpclient.test.lib.http2.Http2Handler; +import jdk.httpclient.test.lib.http2.Http2TestExchange; +import jdk.httpclient.test.lib.http2.Http2TestServer; +import jdk.test.lib.Asserts; + +public class HeaderEncodingBufferReuseTest { + public static void main(String[] args) throws Exception { + Http2TestServer server = new Http2TestServer("localhost", false, 0); + server.addHandler(new OkHandler(), "/test"); + server.start(); + String uri = "http://localhost:" + server.getAddress().getPort() + "/test"; + + try (HttpClient client = HttpClient.newBuilder() + .proxy(HttpClient.Builder.NO_PROXY) + .version(HttpClient.Version.HTTP_2) + .build()) { + + HttpResponse warmup = send(client, uri, 2); + Asserts.assertEquals(warmup.version(), HttpClient.Version.HTTP_2); + + Object conn = getHttp2Connection(client); + Asserts.assertEquals(send(client, uri, 2).statusCode(), 200); + ByteBuffer cached = (ByteBuffer) getField(conn, "cachedHeaderBuffer"); + Asserts.assertNotNull(cached); + + Asserts.assertEquals(send(client, uri, 2).statusCode(), 200); + Asserts.assertEquals(cached, getField(conn, "cachedHeaderBuffer")); + + Asserts.assertEquals(send(client, uri, 300).statusCode(), 200); + Asserts.assertEquals(cached, getField(conn, "cachedHeaderBuffer")); + + Asserts.assertEquals(send(client, uri, 2).statusCode(), 200); + Asserts.assertEquals(cached, getField(conn, "cachedHeaderBuffer")); + } finally { + server.stop(); + } + } + + static Object getField(Object obj, String name) throws Exception { + Field field = obj.getClass().getDeclaredField(name); + field.setAccessible(true); + return field.get(obj); + } + + static Object getHttp2Connection(HttpClient client) throws Exception { + Object clientImpl = getField(client, "impl"); + + var method = clientImpl.getClass().getDeclaredMethod("client2"); + method.setAccessible(true); + Object client2 = method.invoke(clientImpl); + + Object conns = getField(client2, "connections"); + Map connections = (Map) conns; + Asserts.assertEquals(1, connections.size()); + return connections.values().iterator().next(); + } + + static HttpResponse send(HttpClient client, String uri, int headerCount) throws Exception { + HttpRequest.Builder builder = HttpRequest.newBuilder(URI.create(uri)) + .POST(BodyPublishers.ofString("test")); + for (int i = 0; i < headerCount; i++) { + builder.header("X-Header-" + i, "value-" + "x".repeat(50) + "-" + i); + } + return client.send(builder.build(), BodyHandlers.discarding()); + } + + static class OkHandler implements Http2Handler { + @Override + public void handle(Http2TestExchange exchange) throws IOException { + exchange.sendResponseHeaders(200, 0); + exchange.getResponseBody().close(); + } + } +}