From 9f7b5d682865e70f1f16e7cc23a04a2fff99bf7c Mon Sep 17 00:00:00 2001 From: David <3dgiordano@gmail.com> Date: Thu, 2 Jul 2026 20:31:25 -0300 Subject: [PATCH 1/2] Preserve connection keep alive Jetty does not preserve it because HTTP 1.1 does not require it, but it is preserved for compatibility with JMeter. --- .../jmeter/http2/core/HTTP2JettyClient.java | 6 +- .../core/JmeterRequestHeadersSupport.java | 103 ++++++++++++++ .../core/HttpKeepAliveSampleHeadersTest.java | 132 ++++++++++++++++++ .../core/JmeterRequestHeadersSupportTest.java | 77 ++++++++++ 4 files changed, 317 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/blazemeter/jmeter/http2/core/JmeterRequestHeadersSupport.java create mode 100644 src/test/java/com/blazemeter/jmeter/http2/core/HttpKeepAliveSampleHeadersTest.java create mode 100644 src/test/java/com/blazemeter/jmeter/http2/core/JmeterRequestHeadersSupportTest.java diff --git a/src/main/java/com/blazemeter/jmeter/http2/core/HTTP2JettyClient.java b/src/main/java/com/blazemeter/jmeter/http2/core/HTTP2JettyClient.java index 9b7231f..9b7f3fa 100644 --- a/src/main/java/com/blazemeter/jmeter/http2/core/HTTP2JettyClient.java +++ b/src/main/java/com/blazemeter/jmeter/http2/core/HTTP2JettyClient.java @@ -1138,6 +1138,7 @@ public HTTPSampleResult retryWithHTTP11Only(HTTP2Sampler sampler, HTTPSampleResu // Copy body if present setBody(http11Request, sampler, result); + JmeterRequestHeadersSupport.prepareFromSampler(http11Request, sampler.getUseKeepAlive()); // Send request lowLevelDebug("Sending HTTP/1.1 fallback request"); @@ -1199,6 +1200,7 @@ private ContentResponse sendWithHTTP11Only(Request originalRequest, // will be lost, but this is an edge case. } ensureHostHeader(http11Request, uri); + JmeterRequestHeadersSupport.copySamplerHeaderState(originalRequest, http11Request); configureContentDecodersAndCapture(httpClientHttp1Only, http11Request); @@ -1324,6 +1326,7 @@ private void samplePrepareRequest(Request request, result.sampleStart(); setBody(request, sampler, result); + JmeterRequestHeadersSupport.prepareFromSampler(request, sampler.getUseKeepAlive()); initializeSentBytes(result, request); } @@ -3452,7 +3455,8 @@ private String getSerializedRequestHeaders(Request request, boolean refresh) { if (!refresh && cached instanceof String) { return (String) cached; } - String serialized = buildHeadersString(request.getHeaders()); + String serialized = buildHeadersString( + JmeterRequestHeadersSupport.headersForSampleResult(request)); request.attribute(ATTR_REQUEST_HEADERS_SERIALIZED, serialized); return serialized; } diff --git a/src/main/java/com/blazemeter/jmeter/http2/core/JmeterRequestHeadersSupport.java b/src/main/java/com/blazemeter/jmeter/http2/core/JmeterRequestHeadersSupport.java new file mode 100644 index 0000000..f8d5bda --- /dev/null +++ b/src/main/java/com/blazemeter/jmeter/http2/core/JmeterRequestHeadersSupport.java @@ -0,0 +1,103 @@ +package com.blazemeter.jmeter.http2.core; + +import org.apache.jmeter.protocol.http.util.HTTPConstants; +import org.eclipse.jetty.client.Request; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpVersion; + +/** + * Preserves JMeter-visible request headers in {@code HTTPSampleResult#getRequestHeaders()} + * when Jetty omits or strips them on the wire. + * + *
Counterpart of {@link JmeterCompressionHeadersSupport} for outbound request headers.
+ */
+final class JmeterRequestHeadersSupport {
+
+ static final String ATTR_USE_KEEPALIVE = "bzm.useKeepAlive";
+
+ private JmeterRequestHeadersSupport() {
+ }
+
+ /**
+ * Applies sampler-driven request headers before send, matching {@code HTTPHC4Impl#setupRequest}.
+ */
+ static void prepareFromSampler(Request request, boolean useKeepAlive) {
+ if (request == null) {
+ return;
+ }
+ request.attribute(ATTR_USE_KEEPALIVE, useKeepAlive);
+ applyConnectionHeader(request, useKeepAlive);
+ // TODO: HC4 sends explicit empty User-Agent when none is configured (disableDefaultUserAgent).
+ // prepareEmptyUserAgentHeader(request);
+ }
+
+ /**
+ * Copies sampler header settings to a cloned request (for example HTTP/1.1 fallback).
+ */
+ static void copySamplerHeaderState(Request from, Request to) {
+ if (from == null || to == null) {
+ return;
+ }
+ Object useKeepAlive = from.getAttributes().get(ATTR_USE_KEEPALIVE);
+ if (useKeepAlive != null) {
+ to.attribute(ATTR_USE_KEEPALIVE, useKeepAlive);
+ }
+ }
+
+ /**
+ * Request headers to record in the sample after Jetty may have adjusted the live fields.
+ */
+ static HttpFields headersForSampleResult(Request request) {
+ if (request == null) {
+ return HttpFields.EMPTY;
+ }
+ HttpFields.Mutable merged = HttpFields.build(request.getHeaders());
+ restoreConnectionHeaderForSample(request, merged);
+ // restoreEmptyUserAgentForSample(request, merged);
+ return merged;
+ }
+
+ private static void applyConnectionHeader(Request request, boolean useKeepAlive) {
+ if (!shouldSendConnectionHeader(request)) {
+ return;
+ }
+ HttpFields.Mutable mutableHeaders = mutableHeaders(request);
+ if (mutableHeaders == null || mutableHeaders.contains(HttpHeader.CONNECTION)) {
+ return;
+ }
+ if (useKeepAlive) {
+ mutableHeaders.put(HTTPConstants.HEADER_CONNECTION, HTTPConstants.KEEP_ALIVE);
+ } else {
+ mutableHeaders.put(HTTPConstants.HEADER_CONNECTION, HTTPConstants.CONNECTION_CLOSE);
+ }
+ }
+
+ private static void restoreConnectionHeaderForSample(Request request, HttpFields.Mutable headers) {
+ Object useKeepAlive = request.getAttributes().get(ATTR_USE_KEEPALIVE);
+ if (useKeepAlive == null || !shouldSendConnectionHeader(request)) {
+ return;
+ }
+ if (Boolean.TRUE.equals(useKeepAlive)) {
+ headers.put(HTTPConstants.HEADER_CONNECTION, HTTPConstants.KEEP_ALIVE);
+ } else {
+ headers.put(HTTPConstants.HEADER_CONNECTION, HTTPConstants.CONNECTION_CLOSE);
+ }
+ }
+
+ private static boolean shouldSendConnectionHeader(Request request) {
+ if (request != null && request.getVersion() == HttpVersion.HTTP_2) {
+ return false;
+ }
+ HttpFields headers = request.getHeaders();
+ return headers == null || !headers.contains(HttpHeader.CONNECTION);
+ }
+
+ private static HttpFields.Mutable mutableHeaders(Request request) {
+ HttpFields headers = request.getHeaders();
+ if (headers instanceof HttpFields.Mutable) {
+ return (HttpFields.Mutable) headers;
+ }
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/blazemeter/jmeter/http2/core/HttpKeepAliveSampleHeadersTest.java b/src/test/java/com/blazemeter/jmeter/http2/core/HttpKeepAliveSampleHeadersTest.java
new file mode 100644
index 0000000..92f8e22
--- /dev/null
+++ b/src/test/java/com/blazemeter/jmeter/http2/core/HttpKeepAliveSampleHeadersTest.java
@@ -0,0 +1,132 @@
+package com.blazemeter.jmeter.http2.core;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import com.blazemeter.jmeter.http2.sampler.HTTP2Sampler;
+import com.blazemeter.jmeter.http2.sampler.JMeterTestUtils;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import org.apache.jmeter.protocol.http.sampler.HTTPSampleResult;
+import org.apache.jmeter.protocol.http.util.HTTPConstants;
+import org.apache.jmeter.util.JMeterUtils;
+import org.junit.After;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class HttpKeepAliveSampleHeadersTest {
+
+ private ExecutorService executor;
+
+ @BeforeClass
+ public static void setupJmeter() {
+ JMeterTestUtils.setupJmeterEnv();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (executor != null) {
+ executor.shutdownNow();
+ executor.awaitTermination(5, TimeUnit.SECONDS);
+ executor = null;
+ }
+ }
+
+ @Test
+ public void sampleRequestHeadersIncludeKeepAliveWhenEnabled() throws Exception {
+ configureLegacyHttp1Only();
+
+ try (ServerSocket serverSocket = new ServerSocket(0)) {
+ int port = serverSocket.getLocalPort();
+ Future