Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -174,15 +174,20 @@ private static boolean hostnameVerificationDisabledValue() {
public static final int SLICE_THRESHOLD = 32;

/**
* Allocated buffer size. Must never be higher than 16K. But can be lower
* if smaller allocation units preferred. HTTP/2 mandates that all
* implementations support frame payloads of at least 16K.
* The capacity of ephemeral {@link ByteBuffer}s allocated to pass data to and from the client.
* It is ensured to have a value between 1 and 2^14 (16,384).
*/
private static final int DEFAULT_BUFSIZE = 16 * 1024;

public static final int BUFSIZE = getIntegerNetProperty(
"jdk.httpclient.bufsize", DEFAULT_BUFSIZE
);
"jdk.httpclient.bufsize", 1,
// We cap at 2^14 (16,384) for two main reasons:
// - The initial frame size is 2^14 (RFC 9113)
// - SSL record layer fragments data in chunks of 2^14 bytes or less (RFC 5246)
1 << 14,
// We choose 2^14 (16,384) as the default, because:
// 1. It maximizes throughput within the limits described above
// 2. It is small enough to not create a GC bottleneck when it is partially filled
1 << 14,
true);

public static final BiPredicate<String,String> ACCEPT_ALL = (x,y) -> true;

Expand Down
4 changes: 3 additions & 1 deletion src/java.net.http/share/classes/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@
* depending on the context. These restrictions cannot be overridden by this property.
* </li>
* <li><p><b>{@systemProperty jdk.httpclient.bufsize}</b> (default: 16384 bytes or 16 kB)<br>
* The size to use for internal allocated buffers in bytes.
* The capacity of internal ephemeral buffers allocated to pass data to and from the
* client, in bytes. Valid values are in the range [1, 2^14 (16384)].
* If an invalid value is provided, the default value is used.
* </li>
* <li><p><b>{@systemProperty jdk.httpclient.connectionPoolSize}</b> (default: 0)<br>
* The maximum number of connections to keep in the HTTP/1.1 keep alive cache. A value of 0
Expand Down
161 changes: 161 additions & 0 deletions test/jdk/java/net/httpclient/BufferSize1Test.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/

import jdk.httpclient.test.lib.common.HttpServerAdapters;
import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestServer;
import jdk.internal.net.http.common.Utils;
import jdk.test.lib.net.SimpleSSLContext;

import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpClient.Version;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;

import static java.net.http.HttpClient.Builder.NO_PROXY;
import static java.net.http.HttpClient.Version.HTTP_3;
import static java.net.http.HttpOption.H3_DISCOVERY;
import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY;

/*
* @test
* @bug 8367976
* @summary Verifies that setting the `jdk.httpclient.bufsize` system property
* to its lowest possible value, 1, does not wedge the client
* @library /test/jdk/java/net/httpclient/lib
* /test/lib
* @run main/othervm -Djdk.httpclient.bufsize=1 -Dtest.httpVersion=HTTP_1_1 BufferSize1Test
* @run main/othervm -Djdk.httpclient.bufsize=1 -Dtest.httpVersion=HTTP_1_1 -Dtest.sslEnabled BufferSize1Test
* @run main/othervm -Djdk.httpclient.bufsize=1 -Dtest.httpVersion=HTTP_2 BufferSize1Test
* @run main/othervm -Djdk.httpclient.bufsize=1 -Dtest.httpVersion=HTTP_2 -Dtest.sslEnabled BufferSize1Test
* @run main/othervm -Djdk.httpclient.bufsize=1 -Dtest.httpVersion=HTTP_3 BufferSize1Test
*/

public class BufferSize1Test {

public static void main(String[] args) throws Exception {

// Verify `Utils.BUFSIZE`
if (Utils.BUFSIZE != 1) {
throw new AssertionError("Unexpected `Utils.BUFSIZE`: " + Utils.BUFSIZE);
}

// Create the server
var version = Version.valueOf(System.getProperty("test.httpVersion"));
var sslContext = System.getProperty("test.sslEnabled") != null || HTTP_3.equals(version)
? new SimpleSSLContext().get()
: null;
try (var server = switch (version) {
case HTTP_1_1, HTTP_2 -> HttpTestServer.create(version, sslContext);
case HTTP_3 -> HttpTestServer.create(HTTP_3_URI_ONLY, sslContext);
}) {

// Add the handler and start the server
var serverHandlerPath = "/" + BufferSize1Test.class.getSimpleName();
server.addHandler(new BodyEchoingHandler(), serverHandlerPath);
server.start();

// Create the client
try (var client = createClient(version, sslContext)) {

// Create the request with body to ensure that `ByteBuffer`s
// will be used throughout the entire end-to-end interaction.
byte[] requestBodyBytes = "body".repeat(1000).getBytes(StandardCharsets.US_ASCII);
var request = createRequest(sslContext, server, serverHandlerPath, version, requestBodyBytes);

// Execute and verify the request, twice for certainty.
requestAndVerify(client, request, requestBodyBytes);
requestAndVerify(client, request, requestBodyBytes);

}

}

}

private static HttpClient createClient(Version version, SSLContext sslContext) {
var clientBuilder = HttpServerAdapters
.createClientBuilderFor(version)
.proxy(NO_PROXY)
.version(version);
if (sslContext != null) {
clientBuilder.sslContext(sslContext);
}
return clientBuilder.build();
}

private static HttpRequest createRequest(SSLContext sslContext, HttpTestServer server, String serverHandlerPath, Version version, byte[] requestBodyBytes) {
var requestUri = URI.create(String.format(
"%s://%s%s/x",
sslContext == null ? "http" : "https",
server.serverAuthority(),
serverHandlerPath));
var requestBuilder = HttpRequest
.newBuilder(requestUri)
.version(version)
.POST(HttpRequest.BodyPublishers.ofByteArray(requestBodyBytes));
if (HTTP_3.equals(version)) {
requestBuilder.setOption(H3_DISCOVERY, HTTP_3_URI_ONLY);
}
return requestBuilder.build();
}

private static void requestAndVerify(HttpClient client, HttpRequest request, byte[] requestBodyBytes)
throws IOException, InterruptedException {
var response = client.send(request, HttpResponse.BodyHandlers.ofByteArray());
if (response.statusCode() != 200) {
throw new AssertionError("Was expecting status code 200, found: " + response.statusCode());
}
byte[] responseBodyBytes = response.body();
int mismatchIndex = Arrays.mismatch(requestBodyBytes, responseBodyBytes);
if (mismatchIndex >= 0) {
var message = String.format(
"Response body (%s bytes) mismatches the request body (%s bytes) at index %s!",
responseBodyBytes.length, requestBodyBytes.length, mismatchIndex);
throw new AssertionError(message);
}
}

private static final class BodyEchoingHandler implements HttpServerAdapters.HttpTestHandler {

@Override
public void handle(HttpServerAdapters.HttpTestExchange exchange) throws IOException {
try (exchange) {
byte[] body;
try (var requestBodyStream = exchange.getRequestBody()) {
body = requestBodyStream.readAllBytes();
}
exchange.sendResponseHeaders(200, body.length);
try (var responseBodyStream = exchange.getResponseBody()) {
responseBodyStream.write(body);
}
}
}

}

}
92 changes: 92 additions & 0 deletions test/jdk/java/net/httpclient/BufferSizePropertyClampTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/

import jdk.test.lib.process.ProcessTools;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

/*
* @test
* @bug 8367976
* @summary Verifies that the `jdk.httpclient.bufsize` system property is
* clamped correctly
* @library /test/lib
* @run junit BufferSizePropertyClampTest
*/

class BufferSizePropertyClampTest {

private static Path scriptPath;

@BeforeAll
static void setUp() throws IOException {
// Create a Java file that prints the `Utils::BUFSIZE` value
scriptPath = Path.of("UtilsBUFSIZE.java");
Files.write(scriptPath, List.of("void main() { IO.println(jdk.internal.net.http.common.Utils.BUFSIZE); }"));
}

@AfterAll
static void tearDown() throws IOException {
Files.deleteIfExists(scriptPath);
}

@ParameterizedTest
@ValueSource(ints = {-1, 0, (2 << 14) + 1})
void test(int invalidBufferSize) throws Exception {

// Run the Java file
var outputAnalyzer = ProcessTools.executeTestJava(
"--add-exports", "java.net.http/jdk.internal.net.http.common=ALL-UNNAMED",
"-Djdk.httpclient.HttpClient.log=errors",
"-Djdk.httpclient.bufsize=" + invalidBufferSize,
scriptPath.toString());
outputAnalyzer.shouldHaveExitValue(0);

// Verify stderr
List<String> stderrLines = outputAnalyzer.stderrAsLines();
assertEquals(2, stderrLines.size(), "Expected 2 lines, found: " + stderrLines);
assertTrue(
stderrLines.get(0).endsWith("jdk.internal.net.http.common.Utils getIntegerNetProperty"),
"Unexpected line: " + stderrLines.get(0));
assertEquals(
"INFO: ERROR: Property value for jdk.httpclient.bufsize=" + invalidBufferSize + " not in [1..16384]: using default=16384",
stderrLines.get(1).replaceAll(",", ""));

// Verify stdout
var stdoutLines = outputAnalyzer.stdoutAsLines();
assertEquals(1, stdoutLines.size(), "Expected one line, found: " + stdoutLines);
assertEquals("16384", stdoutLines.get(0));

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,6 @@
* @run junit OfByteArrayTest
*
* @comment Using `main/othervm` to initiate tests that depend on a custom-configured JVM
* @run main/othervm -Djdk.httpclient.bufsize=-1 OfByteArrayTest testInvalidBufferSize
* @run main/othervm -Djdk.httpclient.bufsize=0 OfByteArrayTest testInvalidBufferSize
* @run main/othervm -Djdk.httpclient.bufsize=3 OfByteArrayTest testChunking "" 0 0 ""
* @run main/othervm -Djdk.httpclient.bufsize=3 OfByteArrayTest testChunking a 0 0 ""
* @run main/othervm -Djdk.httpclient.bufsize=3 OfByteArrayTest testChunking a 1 0 ""
Expand Down Expand Up @@ -88,7 +86,6 @@ void testInvalidOffsetOrLength(String contentText, int offset, int length) {
*/
public static void main(String[] args) throws InterruptedException {
switch (args[0]) {
case "testInvalidBufferSize" -> testInvalidBufferSize();
case "testChunking" -> testChunking(
parseStringArg(args[1]),
Integer.parseInt(args[2]),
Expand All @@ -102,10 +99,6 @@ private static String parseStringArg(String arg) {
return arg == null || arg.trim().equals("\"\"") ? "" : arg;
}

private static void testInvalidBufferSize() {
assertThrows(IllegalArgumentException.class, () -> HttpRequest.BodyPublishers.ofByteArray(new byte[1]));
}

private static void testChunking(
String contentText, int offset, int length, String expectedBuffersText)
throws InterruptedException {
Expand Down