Skip to content

Add HttpClient5 sampler implementation with HTTP/2 support - #6742

Open
andreaslind01 wants to merge 59 commits into
apache:masterfrom
andreaslind01:httpclient5_http2
Open

Add HttpClient5 sampler implementation with HTTP/2 support#6742
andreaslind01 wants to merge 59 commits into
apache:masterfrom
andreaslind01:httpclient5_http2

Conversation

@andreaslind01

Copy link
Copy Markdown
Contributor

Description

This PR adds a new HTTP sampler implementation, HttpClient5, based on Apache HttpComponents HttpClient 5.x, and introduces a configurable HTTP Version setting (HTTP/1.1 / HTTP/2) for both the HttpClient5 and the Java implementation.

Main changes:

  • New HTTPHC5Impl (HTTPSamplerFactory.IMPL_HTTP_CLIENT5, selectable as HttpClient5 in the GUI and in JMX files):
    • Classic (blocking) client for HTTP/1.1 and an async H2 client for HTTP/2, selected via HttpVersionPolicy (FORCE_HTTP_1 / NEGOTIATE), with automatic fallback to HTTP/1.1 when the server does not offer h2 via ALPN.
    • Per-thread client caching keyed by target/proxy/timeouts/local address/version policy.
    • Support for connect & response timeouts, proxies (incl. proxy authentication), AuthManager (BASIC/DIGEST, pre-emptive BASIC), CacheManager (conditional requests via If-Modified-Since / If-None-Match), CookieManager, DNSCacheManager, response decompression (gzip/deflate/brotli), and retry handling.
    • Correct population of SampleResult metrics: sentBytes, connectTime (measured for both HTTP/1.1 and HTTP/2, including TLS), latency, headers and response code/message.
  • HTTPJavaImpl: HTTP/2 support via the JDK java.net.http.HttpClient when HTTP/2 is selected, including caching, proxies, user authentication, sentBytes accounting, connect-time measurement, reason-phrase derivation (HTTP/2 has no reason phrase) and preservation of Authorization / Proxy-Authorization headers.
  • New sampler property HTTPSampler.httpVersion (HTTPSamplerBaseSchema.httpVersion, getter/setter on HTTPSamplerBase) with a new combo box in HTTP Request and HTTP Request Defaults (http_version resource key added to all messages_*.properties).
  • CacheManager: new overloads for HC5 (ClassicHttpRequest / ClassicHttpResponse / org.apache.hc.core5.http.Header[]) and for the JDK java.net.http.HttpResponse.
  • Property httpclient.version re-purposed as the default HTTP version (HTTP/1.1 | HTTP/2) used when the sampler's HTTP Version field is empty.
  • Dependencies: httpclient5 and httpcore5 added to src/protocol/http and to the third-party BOM (httpcore5:5.3.4).
  • Documentation updated: component_reference.xml, properties_reference.xml, get-started.xml, bin/jmeter.properties.

Motivation and Context

JMeter's HTTP samplers currently only support HTTP/1.1: the HttpClient4 implementation is built on the HttpComponents 4.x line, which will not receive HTTP/2 support, and the Java implementation used the legacy HttpURLConnection. Modern web applications and APIs are increasingly served over HTTP/2, so load tests against them either could not be executed at all or did not represent realistic client behaviour (multiplexing, HPACK header compression, single connection per origin).

This change gives users a supported migration path to HttpComponents 5.x and makes it possible to run load tests over HTTP/2 — either with the fully featured HttpClient5 implementation or, for lightweight scenarios, with the JDK client in the Java implementation. Existing test plans are unaffected: HttpClient4 remains the default and an empty HTTP Version falls back to the previous HTTP/1.1 behaviour.

Fixes:

How Has This Been Tested?

  • New unit/integration tests (34 tests, all green):
    • TestHTTPHC5Features (16 tests): version selection and precedence (sampler value vs. httpclient.version vs. unsupported value), HTTP/2 usage, fallback to HTTP/1.1 when the server does not support h2, HTTP/2 via proxy, sentBytes for GET/POST, conditional requests through CacheManager, BASIC credentials from AuthManager, proxy authentication, and connectTime for HTTP/1.1 and HTTP/2.
    • TestHTTPJavaFeatures (~16 tests): version selection, HTTP/2 requests (incl. via proxy), response message / reason-phrase handling for HTTP/2, sentBytes for GET/POST in both versions, Authorization header from the HeaderManager, and connectTime for HTTP/1.1, HTTP/2 plaintext and HTTP/2 over TLS.
    • TestHTTPSamplerFactory: creation and lookup of the new HttpClient5 implementation, plus the unchanged behaviour for the existing aliases.
    • The tests run against locally started embedded HTTP/1.1, HTTP/2 (h2c and h2 over TLS) and proxy servers, so no external services are required.
  • ./gradlew classes style — compiles cleanly and reports no style/checkstyle/autostyle violations.
  • The existing src:protocol:http test suite (including JMeterTest, extended by httpVersion in the ignored-properties list) still passes.
  • Manual verification in the JMeter GUI: the new HTTP Version combo box in HTTP Request and HTTP Request Defaults is saved/restored correctly in JMX files, and requests against an HTTP/2 endpoint are reported as HTTP/2 in the View Results Tree.
  • Test environment: Windows, JDK 21 toolchain, Gradle build JMeter 6.0.0-SNAPSHOT.

Screenshots (if appropriate):

image

Types of changes

  • New feature (non-breaking change which adds functionality)

Checklist:

  • My code follows the code style of this project.
  • I have updated the documentation accordingly.

…ng, caching, proxies, and user authentication
…and improve fallback handling with new tests
…ts, including HTTP/2, with corresponding unit tests
…ers (`Authorization`, `Proxy-Authorization`) and adding unit tests for validation

@milamberspace milamberspace left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a full review of this PR — just a scoped, timely note on the dependency versions.

Apache HttpComponents Client just released 5.6.4: "Corrects application of SSL parameters in the async TLS upgrade method" (RELEASE_NOTES-5.6.x.txt). This PR pins httpclient5:5.5.1, and HTTPHC5Impl is exactly the kind of code that exercises that path — it uses the async H2 client with HttpVersionPolicy.NEGOTIATE for HTTP/2-over-TLS via ALPN, i.e. an async TLS upgrade. Worth pulling in the fix before this lands, rather than shipping the new HTTP/2 sampler with a known bug in SSL-parameter application during that exact upgrade.

See inline comment for the concrete version bump (and the matching httpcore5 pairing, since httpclient5:5.6.4 is built/tested against httpcore5:5.4.3, not 5.3.4).

This review was drafted by an AI-assisted tool and confirmed by an Apache JMeter maintainer.

Comment thread src/bom-thirdparty/build.gradle.kts Outdated
@@ -107,6 +107,8 @@ dependencies {
because("User might still rely on commons-text")
}
api("org.apache.httpcomponents.client5:httpclient5:5.5.1")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since HTTPHC5Impl (this PR) exercises HttpClient5's async TLS upgrade path for HTTP/2, worth bumping both of these before merge:

  • httpclient5: 5.5.15.6.4 — fixes "SSL parameter application in the async TLS upgrade strategy" (release notes)
  • httpcore5 / httpcore5-h2: 5.3.45.4.3 — the version httpclient5:5.6.4 is actually built and tested against (per its parent POM's httpcore.version property), so bumping only httpclient5 and leaving httpcore5 at 5.3.4 would be an incoherent pairing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — good catch. Bumped httpclient5 to 5.6.4 and httpcore5/httpcore5-h2 to 5.4.3. Confirmed the pairing you flagged: httpclient5-parent-5.6.4.pom sets <httpcore.version>5.4.3</httpcore.version>.

Not a drop-in bump though — it surfaced two real issues:

1. HTTPS/HTTP-2 handshakes broke (SSLHandshakeException: No name matching localhost found). Now that SSL parameters are actually applied on the async path, JSSE endpoint identification runs, and ClientTlsStrategyBuilder defaults to BOTH when a hostnameVerifier is set, silently overriding our NoopHostnameVerifier + TrustAllStrategy. On 5.5.1 that half was a no-op because of the bug. Fixed with .setHostVerificationPolicy(HostnameVerificationPolicy.CLIENT). This would have hit anyone testing HTTPS with a self-signed cert, so good that this landed before the sampler shipped.

2. NoClassDefFoundError in HTTPHC5Impl's static initializer — 5.6 rewrote BrotliInputStreamFactory to use the optional brotli4j, which we don't ship. Fixed by decoding br via org.brotli:dec, already a direct dependency and what HTTPHC4Impl uses.

Also switched deprecated build()buildAsync() and suppressed the new deprecation warnings (-Werror). Didn't migrate to the suggested ContentCodecRegistry — it's @Internal. Happy to revisit.

classes style clean; :src:protocol:http:test 977 passed / 0 failed.

….4.3 respectively for improved SSL parameter handling in HTTP/2
… and ensuring consistent header reporting across transports

@milamberspace milamberspace left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found a real bug while manually testing the HTTP/2 sampler against a long-running server: intermittent

java.io.IOException: Could not execute HTTP/2 request
Caused by: org.apache.hc.core5.http2.impl.nio.ConnectionClosedException: Connection is closed
	at org.apache.hc.core5.http2.impl.nio.H2Streams.shutdownAndReleaseAll(H2Streams.java:149)
	at org.apache.hc.core5.http2.impl.nio.AbstractH2StreamMultiplexer.onOutput(...)
	...

Root cause

createHttp2Client() never configures ConnectionConfig.validateAfterInactivity on the PoolingAsyncClientConnectionManagerBuilder, and ConnectionConfig.DEFAULT documents it as null (undefined). In PoolingAsyncClientConnectionManager#lease(), the re-validation step (an HTTP/2 PING before handing out a pooled connection, StaleCheckCommand for HTTP/1.1) is gated by:

final TimeValue timeValue = connectionConfig.getValidateAfterInactivity();
if (connection.isOpen() && TimeValue.isNonNegative(timeValue)) { ... }

TimeValue.isNonNegative(null) is false, so with the default config this block never runs — pooled async connections are leased straight out of the pool with zero liveness check.

Concretely: HTTP_2_CLIENTS caches the async client per JMeter thread and reuses it across iterations. If the server (or an idle load balancer/NAT) closes an idle pooled HTTP/2 connection between two samples, the next sample picks it from the pool as-is; the I/O reactor only discovers it's dead when it tries to write to it, surfacing as ConnectionClosedException deep in H2Streams.

This is made worse by disableAutomaticRetries() (called on both the classic and async builders) — correctly disabled so JMeter doesn't silently mask real server behavior/timing from the sample result, but it also removes HttpClient5's own safety net for exactly this failure mode. Without proactive pool validation, there's nothing left to catch it, and it surfaces as a hard sampler failure instead of a transparent retry.

Suggested fix

See inline comment — add .setValidateAfterInactivity(...) to the ConnectionConfig built in createHttp2Client() (worth doing for createClient()'s classic-transport config too, same gap applies there).

This review was drafted by an AI-assisted tool and confirmed by an Apache JMeter maintainer.

if (key.dnsCacheManager != null) {
connectionManagerBuilder.setDnsResolver(createDnsResolver(key.dnsCacheManager));
}
if (key.connectTimeout > 0) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth folding a setValidateAfterInactivity into this ConnectionConfig (unconditionally, not just under the connectTimeout > 0 guard) so pooled HTTP/2 connections get an HTTP/2 PING liveness check before reuse instead of being handed out straight from the pool:

ConnectionConfig.Builder connectionConfig = ConnectionConfig.custom()
        .setValidateAfterInactivity(TimeValue.ofSeconds(2));
if (key.connectTimeout > 0) {
    connectionConfig.setConnectTimeout(Timeout.ofMilliseconds(key.connectTimeout));
}
connectionManagerBuilder.setDefaultConnectionConfig(connectionConfig.build());

Without it, ConnectionConfig.DEFAULT.getValidateAfterInactivity() is null, and PoolingAsyncClientConnectionManager#lease() skips its re-validation step entirely (TimeValue.isNonNegative(null) is false), so a connection the server already closed gets reused as-is and fails mid-write with ConnectionClosedException instead of being transparently discarded and replaced.

@milamberspace

Copy link
Copy Markdown
Contributor

Following up on the stale-connection bug above with a concrete repro I ran manually against a real server (own domain, browsing-style scenario: 5 threads, 3 loops, ~800–2800ms think time between transactions on the same reused HTTP/2 connection).

Worth a regression test to prove validateAfterInactivity actually catches this rather than just proving it's configured. One nuance that matters for how the test is shaped: it's not the request count that triggers this, it's the idle gap between two requests on the same pooled connection. A tight rapid-fire loop with no pauses almost certainly won't reproduce it — my manual repro needed nothing more exotic than ~1–3s of think time between transactions, which was enough for the server side to close the idle connection before the next request picked it back up from the pool.

Suggested shape for the test:

  1. Spin up an embedded HTTP/2-over-TLS server the test controls directly (not WireMock's black-box backend — this needs to forcibly close an already-accepted connection without stopping the whole server, which WireMock doesn't expose).
  2. Fire request Expand / Collapse all buttons #1 through HTTPHC5Impl with HTTP/2 selected, get a 200.
  3. Forcibly close the underlying TCP connection from the server side right after responding (server keeps listening for new connections on the same port — just this one connection dies), simulating an idle server-side timeout.
  4. Sleep a little longer than whatever validateAfterInactivity ends up configured to (e.g. 1.1s if it's set to 1s).
  5. Fire request BUG-49753 Enhancement implemented #2 from the same HTTPSamplerBase instance / same thread, so it goes through the cached HTTP_2_CLIENTS entry and reuses the pool.
  6. Assert request BUG-49753 Enhancement implemented #2 succeeds (200, no ConnectionClosedException) — and ideally assert it would fail without the fix, e.g. by running the same test body against a build with validateAfterInactivity stripped out, or documenting in the test comment that it reproduces apache/jmeter#<this-PR>'s manual repro.

Happy to be wrong about the exact mechanics of forcing step 3 cleanly with whatever test HTTP/2 server this project already has infrastructure for (TestHTTPHC5Features already stands up embedded h2/h2c servers per the PR description) — but the idle-gap-not-volume framing is the part I'd want the test to actually exercise, since that's what makes it a faithful regression test rather than one that happens to pass for unrelated reasons.

@vlsi vlsi left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review comments — PR #6742 "Add HttpClient5 sampler implementation with HTTP/2 support"

Reviewed at head 919b9125, against merge base ad6ecbd1.

Two independent reviewers (Codex and a Claude subagent) went through the diff; the items below are the merged, verified
result. Line numbers refer to the PR head.


Blockers

B1. HTTPHC5Impl ends the sample before the body is read, so elapsed excludes the download and latency exceeds elapsed

HTTPHC5Impl.java:444-448 calls result.sampleEnd() right after executeRequest() returns. For the classic transport
that point is "status line and headers received" — the body is read afterwards, in updateResult()
readResponse() (HTTPHC5Impl.java:904). HTTPSamplerBase.readResponse() calls sampleResult.latencyEnd() on the
first body byte (HTTPSamplerBase.java:1972), which now happens after endTime was stamped.

GET a 100 MB file over HTTP/1.1 with HttpClient5: headers arrive after 50 ms, the body takes 10 s. The sample reports
elapsed ≈ 50 ms and latency ≈ 10050 ms. Elapsed, throughput, the Latency column, the aggregate report and the HTML
dashboard are all wrong, and latency > elapsed is a state JMeter's own model treats as impossible.

HTTPHC4Impl does the opposite order deliberately — HTTPHC4Impl.java:667-672, res.sampleEnd(); // Done with the sampling proper. The new HTTPJavaImpl HTTP/2 path also gets it right, so HC5 is the odd one out.

Suggested fix: read the entity inside the try block, then sampleEnd(), then fill in status code, headers, sizes and
redirect location. Add a regression test asserting result.getLatency() <= result.getTime() against a WireMock stub
with withChunkedDribbleDelay.

B2. The HttpClient5 HTTP/1.1 transport ignores SSLManager: no trust-all, no client keystore, no https.socket.protocols

HTTPHC5Impl.createClient() (HTTPHC5Impl.java:1043-1066) builds the classic connection manager without installing any
TLS socket strategy, so HttpClient falls back to SSLContexts.createSystemDefault(). JMeter's per-thread SSLContext
from JsseSSLManager — which carries the https.keyStore client certificates, the trust-all trust manager,
https.socket.protocols and https.cipherSuites — is never used, and HttpClient 5.4+ defaults
HostnameVerificationPolicy to BOTH.

Since an empty HTTP Version maps to FORCE_HTTP_1, this is the default path. Three failures follow:

  • an HTTPS target with a self-signed or internal-CA certificate fails with SSLHandshakeException, while the same plan
    works with HttpClient4 and Java;
  • client-certificate authentication configured through the JMeter keystore silently sends no certificate;
  • https.socket.protocols and https.cipherSuites have no effect.

Note the asymmetry inside the same class: the async client does install a TLS strategy
(HTTP_2_TLS_STRATEGY, HTTPHC5Impl.java:391-405), so the same URL behaves differently depending on the selected HTTP
version. HTTPHC4Impl installs LazyLayeredConnectionSocketFactory (HTTPHC4Impl.java:1106), which wraps
HttpSSLProtocolSocketFactoryJsseSSLManager.

No test covers this: every HTTPS test in TestHTTPHC5Features sets setHttpVersion("HTTP/2") and routes to the async
client.

Suggested fix: build one TLS strategy from ((JsseSSLManager) SSLManager.getInstance()).getContext() plus the two
cipher/protocol properties and NoopHostnameVerifier, and install it on both connection managers
(setTlsSocketStrategy / setTlsStrategy). Resolve the SSLContext lazily per client so the per-thread context and
resetContext() keep working.

B3. createHttp2TlsStrategy() builds a trust-all context and drops the configured client certificate

HTTPHC5Impl.java:391-405 always creates a fresh SSLContexts.custom().loadTrustMaterial(null, TrustAllStrategy…)
context. Nothing from JsseSSLManager reaches it, so HTTP/2 requests to a mutual-TLS endpoint present no client
certificate and fail TLS authentication, unlike every existing implementation.

This is the same root cause as B2 seen from the other side: there is one JMeter SSLContext and neither transport uses
it. Fixing B2 and B3 together with a single shared strategy is the right shape.

B4. HTTPJavaImpl now sends every Header Manager header twice on the HTTP/1.1 path

setupConnection() already calls setConnectionHeaders(conn, u, getHeaderManager(), getCacheManager())
(HTTPJavaImpl.java:440). The diff adds a second call in sample() (HTTPJavaImpl.java:816), and
setConnectionHeaders uses conn.addRequestProperty(n, v), which appends rather than replaces.

A plan on the legacy Java implementation with a Header Manager entry Accept: application/json now puts
Accept: application/json,application/json on the wire. Cookie, Range and Authorization are duplicated the same
way. It is silent, because res.setRequestHeaders(...) is computed inside setupConnection before the second call, so
the sample result still shows one copy.

This regresses the existing HTTP/1.1 Java sampler for everyone, and the only reason for the second call is to obtain
securityHeaders for the new calculateSentBytes.

Suggested fix: return the security headers from setupConnection (or hold them in a field) instead of re-running
setConnectionHeaders. Add a WireMock test asserting the header arrives exactly once.

B5. SHARED_HTTP_2_CLIENTS grows without bound: keyed by a per-thread SSLContext, never evicted, never closed

getHttpClient(URL) puts the SSLContext returned by ((JsseSSLManager) SSLManager.getInstance()).getContext() into
the cache key, and SSLContext does not override equals/hashCode, so the key compares by identity.
JsseSSLManager.getContext() returns a per-thread context by default (https.sessioncontext.shared defaults to false).

So with the default http.java.h2.multiplexing=true, the static SHARED_HTTP_2_CLIENTS map holds one
java.net.http.HttpClient per JMeter thread rather than one shared client — the stated multiplexing goal is not met —
and each client owns a selector thread and a connection pool.

It gets worse across iterations: HTTPHC5Impl/HTTPHC4Impl call resetContext() on every thread-group iteration when
"same user on next iteration" is off (HTTPHC5Impl.java:1281). The next HTTPS HTTP/2 sample then gets a new
SSLContext, a new map key and a new HttpClient, while the old one stays in the static map forever.

200 threads × 500 iterations against an HTTPS target accumulates up to 100 000 HttpClient instances and their selector
threads. The JVM dies with OutOfMemoryError: unable to create native thread.

Nothing ever removes entries — there is no threadFinished()/testEnded() hook for these maps — and HTTP_2_EXECUTOR
(an unbounded cached pool) is never shut down either. HTTPHC5Impl does clean up in threadFinished(); HTTPJavaImpl
does not.

Suggested fix: keep the SSLContext instance out of the key, add a threadFinished()/testEnded() hook that closes
the clients and shuts the executor down, and add a test that samples HTTPS twice with a resetContext() in between and
asserts the map does not grow.

B6. The release-jar manifest is not updated, so :src:dist:verifyReleaseDependencies fails

src/dist/src/dist/expected_release_jars.csv has no entries for httpclient5, httpcore5 or httpcore5-h2, and the
PR does not touch the file. CI already reports it:

External dependencies differ (you could update src/dist/src/dist/expected_release_jars.csv if you run
:src:dist:verifyReleaseDependencies -PupdateExpectedJars)

Run that task and commit the result. Please also confirm the new jars carry the license and NOTICE coverage the ASF
release requires.


Major

M1. Neither HTTP/2 path can be interrupted, so "Stop Test Now" leaves threads blocked

HTTPHC5Impl.interrupt() (HTTPHC5Impl.java:1304-1312) cancels currentRequest, but the request actually executed
over HTTP/2 is a copySimpleHttpRequest asyncRequest = SimpleHttpRequest.copy(request)
(HTTPHC5Impl.java:1142). The classic request never becomes the cancellable dependency of the async exchange, so
cancel() is a no-op while the sampler thread sits in responseFuture.get(...).

HTTPJavaImpl has the mirror problem: interrupt() only touches savedConn, which sampleHttp2() never assigns, so
it returns false and the thread stays blocked in client.httpClient.send(...).

Against a server that accepts the connection and never answers, an HC5 HTTP/2 sampler with no Response Timeout blocks
for the hard-coded 60 s of getHttp2ExecutionTimeoutMillis() and a Java HTTP/2 sampler blocks forever. Neither
"Stop Test Now" nor the JMeter thread interrupt shortens it.

Suggested fix: hold the Future in a volatile field and cancel it from interrupt(); for HTTPJavaImpl, use
sendAsync and keep the CompletableFuture. While you are there, drop the arbitrary 60 s ceiling — HttpClient enforces
the configured response timeout itself, and a legitimate 90-second sample currently fails at 60 s on HTTP/2 but succeeds
on HTTP/1.1.

M2. Both HTTP/2 paths buffer the whole request and response body in heap

HTTPHC5Impl.executeHttp2() converts the request entity with EntityUtils.toByteArray(requestEntity)
(HTTPHC5Impl.java:1144-1149) and executes a SimpleHttpRequest, whose SimpleHttpResponse holds the entire body as a
byte array; createClassicResponse() then wraps that array in a ByteArrayEntity — a third copy.

HTTPJavaImpl.sampleHttp2() has the same shape on the request side: CapturingHttpURLConnection.getOutputStream()
returns a ByteArrayOutputStream, so sendPostData/sendPutData write the whole upload into heap before
BodyPublishers.ofByteArray(...) is built.

Two concrete failures: a POST that uploads a 2 GB file with Files Upload dies with OutOfMemoryError on both HTTP/2
paths while it succeeds on HTTP/1.1; and downloading a 1 GB response over HTTP/2 keeps the full gigabyte in heap even
when httpsampler.max_bytes_to_store_per_request is set, because that limit is applied later, inside
HTTPSamplerBase.readResponse, when the bytes are already materialized. Under load this multiplies by the thread count.

Suggested fix: use the streaming async API — AsyncRequestBuilder with a FileEntityProducer/BasicRequestProducer,
and an AbstractBinResponseConsumer that feeds HTTPSamplerBase.readResponse's truncation logic. For HTTPJavaImpl,
publish with BodyPublishers.ofFile/ofInputStream.

M3. HTTPHC5Impl never updates the sample URL after an automatic redirect

The request config enables HttpClient's own redirect handling (HTTPHC5Impl.java:520), but sample() never writes the
final URI back into the result — there is no result.setURL(...) anywhere in the class. HTTPHC4Impl does exactly that
(HTTPHC4Impl.java:708-718).

Consequences:

  • saveConnectionCookies(response, result.getURL(), getCookieManager()) runs with the original URL, so a Set-Cookie
    issued by the redirect target is stored against the original host. A login flow
    http://www.example.com/loginhttps://auth.example.com/session stores the session cookie for www.example.com,
    and every later request is unauthenticated.
  • cacheManager.saveDetails(response, result) uses res.getUrlAsString(), so the ETag of the redirect target is cached
    under the original URL.
  • Listeners show the pre-redirect URL.

The same method also drops a redirect without a Location header silently, where HC4 raises IllegalArgumentException
(HTTPHC4Impl.java:684-686).

M4. ConnectTimeTracker stamps connectEnd() onto unrelated in-flight samples

ConnectTimeTracker.recordConnectEnd() walks all entries of activeSamples and calls result.connectEnd() on each.
The tracker belongs to an Http2Client, and with http.java.h2.multiplexing=true that client lives in the static
SHARED_HTTP_2_CLIENTS map — one instance for every JMeter thread and every origin, since HttpClientKey carries no
host or port.

Thread A samples https://a.example over an already-pooled connection while thread B opens a new connection to
https://b.example; B's TLS handshake stamps a connect time into A's result.

Separately, ConnectTimeMeasuringExecutor.execute() calls tracker.connectionEstablished() on every task the JDK
client submits — body delivery, stream callbacks, retries — not only on connection establishment, so the first such task
after a sample starts is reported as its connect time even on a fully pooled connection.

recordsConnectTimeForEveryMultiplexedSample (TestHTTPJavaFeatures.java:219-242) asserts this behavior rather than the
invariant, so it locks the defect in. There is also a cross-thread write to a SampleResult owned by another thread.

Suggested fix: scope the tracker to a connection rather than to a client, drop the executor heuristic, and change the
test to assert that a sample served by an established connection reports connectTime == 0.

M5. HTTPHC5Impl reports the decompressed length as bodySize

updateResult() sets bodySize = body.length where body is the decoded payload
(HTTPHC5Impl.java:901-914). HTTPHC4Impl derives it from connection metrics —
res.setBodySize(totalBytes - headerBytes) — that is, wire bytes; and the new HTTPJavaImpl HTTP/2 path uses a
CountingInputStream around the compressed stream.

A gzip response of 20 KB on the wire that expands to 400 KB is reported as 400 KB by HttpClient5 and as 20 KB by
HttpClient4 and Java. Switching a plan's implementation changes the Bytes column and any bandwidth SLA by a factor of
20, with no warning. headersSize is approximated differently too.

Suggested fix: wrap the raw entity stream in org.apache.jorphan.io.CountingInputStream before the decompressing
wrapper — the class HTTPJavaImpl already uses — and compute headersSize the way HC4 does. TestDecompression is
already parameterized across all three implementations; an assertion on getBytesAsLong() there would cover it.

M6. HTTPJavaImpl sent-bytes counts the human-readable body preview for file uploads

The HTTP/1.1 path computes postBodyBytes = getBytes(postBody) where postBody is the String returned by
sendPostData/sendPutData — the display form JMeter puts in the Request Body tab. For a file upload that is the
placeholder <actual file content, not shown here> plus the multipart boundary preview, not the payload.
calculateSentBytes then takes the postBodyBytes.length > 0 branch and never falls back to Content-Length.

POST a 10 MB file with Files Upload and the sample reports a few hundred sent bytes — the sent-bytes graph and the
dashboard understate upload traffic by three orders of magnitude.

The requestHeaders snapshot for the same computation is taken from conn.getRequestProperties() before
setPostHeaders/setPutHeaders has added Content-Length/Content-Type, so the header portion is under-counted too.

Suggested fix: count what is actually written by wrapping conn.getOutputStream() in a counting stream, falling back to
Content-Length.

M7. HTTPJavaImpl mutates global system properties from a static initializer

HTTPJavaImpl has static { applyHttp2SystemProperties(JMeterUtils.getJMeterProperties(), System.getProperties()); },
which writes jdk.httpclient.hpack.maxheadertablesize, jdk.httpclient.maxstreams, jdk.httpclient.windowsize,
jdk.httpclient.connectionWindowSize, jdk.httpclient.maxframesize, jdk.httpclient.keepalive.timeout.h2 and —
unconditionally — jdk.httpclient.enablepush into the JVM-wide table, as a side effect of loading the class.

Selecting the legacy Java implementation for a plain HTTP/1.1 sampler, or running TestDecompression (which
instantiates every implementation), flips jdk.httpclient.enablepush to 0 for every other component in the JVM:
JSR223 scripts, plugins, the backend listener, anything that builds its own java.net.http.HttpClient. In a
distributed-test server process the change survives across runs and cannot be undone. The setUnlessDefined guard only
protects values that are already present.

JMeter already has system.properties for exactly this. Suggested fix: apply the properties lazily and idempotently
when the first HTTP/2 sample is about to be taken, skip jdk.httpclient.enablepush when the JMeter property is absent,
or document that these belong on the command line and drop the mutation.


Design and reuse

D1. HTTPHC5Impl re-implements HTTPHC4Impl rather than extracting the shared logic

Roughly 700 of HTTPHC5Impl's 1499 lines are HTTPHC4Impl with the types substituted: getRequestHeaders,
getAllHeadersExceptCookie, setConnectionCookie, saveConnectionCookies, getOnlyCookieFromHeaders,
setConnectionHeaders, setupRequestEntity/createNameValuePairs, HttpClientKey (12 fields plus equals/
hashCode), the Kerberos SPN and strip-port logic, the route planner, and the connect-time-measuring connection
manager. CountingOutputStream (HTTPHC5Impl.java:997-1013) is derived a third time inside HTTPJavaImpl's
calculateSentBytes.

This is not a theoretical cost: B1, M3 and M5 are all cases where the HC5 copy drifted from the HC4 original within a
single PR. Lifting the cookie, entity and sent-bytes helpers into HTTPHCAbstractImpl (or a package-private utility
shared by HC4 and HC5) would have made those three impossible.

D2. CacheManager grows a third and fourth parallel API

CacheManager now carries three near-identical saveDetails overloads (HC4 HttpResponse, HC5 ClassicHttpResponse,
java.net.http.HttpResponse), two inCache, two setHeaders and two asHeaders adapters. A fix to the Vary or
cache-control handling now has to land in three places, and a core config element acquires a compile-time dependency on
both HttpClient 4 and HttpClient 5 — which also pins those versions into JMeter's public API for plugin authors.

Suggested shape: one saveDetails(ResponseHeaderSource, HTTPSampleResult), one inCache(URL, Header[]) and one
setHeaders(URL, RequestHeaderSink), with the adapters at the three call sites. The existing
CacheManager.Header/HeaderAdapter pair is already most of that abstraction.

D3. Is HTTPJavaImpl the right home for a second full HTTP/2 client?

HTTPJavaImpl grows from ~350 to ~1800 lines. Around 250 of those are a hand-written delegating SSLContext /
SSLContextSpi / SSLEngine triple whose only purpose is to observe when the TLS handshake finished, and ~60 more are
a hand-maintained IANA reason-phrase table (HTTP_REASON_PHRASES) — EnglishReasonPhraseCatalog is already on this
module's classpath.

The legacy Java implementation exists as the minimal, dependency-free fallback. Putting a second production HTTP/2
stack inside it means two independent HTTP/2 implementations to maintain, with different bugs (compare M4 against the
HC5 connect-time path) and different semantics (equals vs equalsIgnoreCase on the same stored value, see N5).

Worth deciding explicitly before merge: either HTTPJavaImpl gets HTTP/2 as a thin java.net.http passthrough without
the connect-time instrumentation and the multiplexing machinery, or the HTTP/2 support lives only in HTTPHC5Impl and
this PR shrinks by about a third. Splitting the PR along that line would also make it reviewable — 4855 lines across two
new HTTP stacks, a new GUI component, new core API and a dependency bump is a lot to land at once.

D4. HTTP/2 multiplexing across virtual users changes what the test measures

http.java.h2.multiplexing and httpclient5.h2.multiplexing both default to true, and for the Java implementation
that means one HttpClient shared by every JMeter thread. A load test then drives N virtual users over a single TCP
connection, which is not what N real browser users do — connection-level effects (congestion window, server-side
per-connection limits, TLS handshake cost) disappear from the measurement.

Multiplexing within one thread's embedded-resource downloads is clearly right. Sharing across threads is a modeling
decision that deserves a default of false, or at least an explicit note in component_reference.xml about what it
does to the results.


Minor and nits

N1. Response stream leaked when obey_contentlength short-circuits

In readResponse(HttpResponse<InputStream>, SampleResult) the if (contentLength == 0 && OBEY_CONTENT_LENGTH) branch
returns NULL_BA without closing in (response.body()). The JDK client releases an HTTP/2 stream only once the body
stream is consumed or closed, so with httpsampler.obey_contentlength=true against a target that answers
Content-Length: 0 every sample leaks a stream. Restructure as try-with-resources around response.body().

N2. Test quality

Several of the 1756 new test lines cannot fail when the behavior they name breaks:

  • enablesMessageMultiplexingWithoutRequiringHttpClient55, doesNotRequireProtocolUpgradeConfiguration and
    doesNotRequireHttpAsyncClassicAdapter read HTTPHC5Impl.class bytes and run a 90-line hand-written constant-pool
    parser (TestHTTPHC5Features.java:784-846). They assert on the shape of the compiled artifact, throw
    IOException("Unknown class-file constant-pool tag") on any tag a future JVM adds, and would pass if multiplexing
    were looked up reflectively but never invoked.
  • appliesHttp2ProtocolSettings asserts the literals 8192/250/65535/65536, which are httpcore5's own H2Config
    defaults — it re-asserts the library, breaks on a dependency bump, and never covers the JMeter property override it
    exists for.
  • setsSentBytesCorrectlyForGetRequest asserts only getSentBytes() > 0, and the POST variant only
    > "hello world".length(), so the sent-bytes arithmetic this PR adds is effectively untested. Assert the exact count
    against WireMock's request journal.
  • multiplexesConcurrentRequestsOverASingleHttp2Connection (both copies) asserts four HTTP/2 200s and would pass with
    four separate connections. Count accepted connections instead.
  • TestHTTPHC5Features sleeps validateAfterInactivityMillis() + 500 (2.5 s) and Thread.sleep(50); neither suite
    closes the static clients it creates, so I/O reactor and selector threads accumulate for the life of the test JVM.

Related: the reflective findMessageMultiplexingSetter() lookup and its "JMeter still runs with older HttpClient
versions" comment are dead weight — build.gradle.kts pins httpclient5 at 5.6.4, so the method is always present.
Dropping the reflection removes the three class-file tests with it.

N3. HttpVersionComboBox hard-codes an English label next to a localized one

HttpVersionComboBox.java:47 defines "HTTP/2 Negotiate" as a Java constant with $NON-NLS-1$, while the label beside
it goes through JMeterUtils.getResString("http_version") and was translated into all 12 bundles. A user running JMeter
in German or Japanese sees a translated field label followed by an untranslated value, and translators have no key.
component_reference.xml documents the string as user-visible, so it is UI text.

Also, http_version was inserted out of alphabetical order in all 12 bundles (messages.properties:476 sits between
html_assertion_title and html_report; messages_de.properties:19 sits right after about).

N4. HttpDefaultsGui writes httpVersion unconditionally

HttpDefaultsGui.java:157 does config.set(httpSchema.getHttpVersion(), String.valueOf(httpVersion.getSelectedItem()))
while HttpTestSampleGui.java:183-185 normalizes a blank selection to null. Every HTTP Request Defaults element
opened and saved in the GUI therefore grows an empty HTTPSampler.httpVersion property — spurious diffs in
version-controlled JMX files — and String.valueOf turns a null selection into the literal "null". Neither GUI resets
the combo in clearGui().

N5. Duplicated constants and inconsistent version matching in the new API

  • HTTPConstantsInterface gains both HTTP_VERSION_2 = "HTTP/2" and HTTP_2 = "HTTP/2" — two public constants with
    identical values and overlapping Javadoc. Callers already mix them.
  • HTTPSamplerBase.HTTP_VERSION = "HTTPSampler.httpVersion" shadows HTTPHCAbstractImpl.HTTP_VERSION = JMeterUtils.getPropDefault("httpclient.version", "1.1") by simple name inside the same hierarchy: unqualified
    HTTP_VERSION means the property value in one class and the property name in the other. The rest of
    HTTPSamplerBase now derives such constants from the schema — HTTPSamplerBaseSchema.INSTANCE.getHttpVersion() .getName() would keep that convention.
  • HTTPHC5Impl.getHttpVersionPolicy compares HTTP_VERSION_2_STRICT with equalsIgnoreCase but HTTP_VERSION_2 with
    equals, so http/2 selects HTTP/1.1 while http/2 strict selects HTTP/2. HTTPJavaImpl.isHttp2 uses
    equalsIgnoreCase for both, so the two implementations disagree about the same stored value.

N6. calculateSentBytes uses the platform default charset and models HTTP/2 as HTTP/1.1 text

Every getBytes(Charset.defaultCharset()) in HTTPHC5Impl.calculateSentBytes (HTTPHC5Impl.java:957-976) measures
header names and values in the platform encoding rather than the ISO-8859-1 the wire format uses, so the same plan
reports different sent bytes on a non-UTF-8 JVM.

More fundamentally, the method reconstructs an HTTP/1.1 request line and CRLF-separated headers even when the request
went out over HTTP/2, where headers are HPACK-compressed pseudo-headers — the reported value is systematically too high
once HPACK indexing kicks in on the second request to a host. HTTPHC4Impl takes the real count from connection
metrics (HTTPHC4Impl.java:701); HC5 exposes the same through HttpClientContext.getEndpointDetails() .getSentBytesCount().

Also: for a repeatable entity the method calls entity.writeTo(counter), which re-reads the whole upload from disk on
every sample purely to count it.

N7. httpclient.version is re-purposed but two existing readers still expect the old values

bin/jmeter.properties and properties_reference.xml now document httpclient.version as taking HTTP/1.1, HTTP/2
or HTTP/2 Strict. Two existing readers still expect 1.0/1.1:

  • AjpSampler.java:180JMeterUtils.getPropDefault("httpclient.version","1.1").equals("1.0");
  • HTTPHCAbstractImpl.java:80protected static final String HTTP_VERSION = JMeterUtils.getPropDefault( "httpclient.version", "1.1").

So the documentation is now wrong for AJP, and a user who set httpclient.version=1.1 for the old meaning gets an
unrecognized value for HC5/Java. Either introduce a separate property for the new domain, or accept both spellings and
say so in the docs.

N8. Documentation gaps

  • changes.xml never announces the headline feature. All seven entries describe HTTP/2 details and assume
    HttpClient5 already exists; there is no entry for the new sampler implementation itself or for the new
    HTTPSampler.httpVersion property.
  • The HTTP Request Defaults <properties> block in component_reference.xml gains no HTTP Version entry, although
    HttpDefaultsGui adds the field.
  • <dt><code>HTTPClient5</code></dt> (component_reference.xml:145) uses casing that does not match the alias
    HttpClient5 — it mirrors the pre-existing HTTPClient4 typo, so fixing both is optional but welcome.

andreaslind01 and others added 26 commits August 17, 2026 07:18
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…HC5Impl samplers, including interrupt handling and corresponding tests
…ownloads

Both HTTP/2 sampler paths materialized entire bodies in heap: HTTPHC5Impl
converted the request entity with EntityUtils.toByteArray() and held the
response as a byte array (copied again into a ByteArrayEntity), while
HTTPJavaImpl captured uploads into a ByteArrayOutputStream before building
BodyPublishers.ofByteArray(). Large uploads died with OutOfMemoryError where
HTTP/1.1 succeeded, and httpsampler.max_bytes_to_store_per_request could not
help downloads because it is applied only after the bytes exist.

Requests now stream: HTTPHC5Impl uses AsyncRequestBuilder with a
FileEntityProducer over a new memory-first SpillOutputStream (256 KB in heap,
then a temp file), and HTTPJavaImpl publishes with BodyPublishers.ofFile.
Direct file uploads stream with no copy at all via PathEntity.

Responses now truncate at the source: the async response consumer stores only
max_bytes_to_store_per_request bytes while still counting the full body, so the
bytes beyond the limit never reach the heap. Truncation is skipped when the
body is content-encoded (a partial gzip stream cannot be decoded) and when MD5
or recording needs the whole body; the limit is resolved on the sampler thread
because the I/O reactor threads cannot see JMeter's ThreadLocal context. That
gives the same peak heap usage as the HTTP/1.1 transport, which reads and
truncates the body as it arrives.

Separately, HTTP/2 downloads were about three times slower than the same
request over HTTP/1.1 or with the Java implementation, because
httpclient5.h2.initial_window_size defaulted to the 64 kB minimum the HTTP/2
specification mandates. The stream flow control window caps the throughput of a
response at the window divided by the round trip time, no matter how much
bandwidth is available. JMeter now announces the same 16 MB window the JDK
client uses, which is already the default of the Java implementation. On a
100 MB download this cut the time from 15 s to 4 s. The connection level window
needs no configuration, as HttpCore maximizes it on its own.

Also fixed along the way:
* multipart previews materialized the whole uploaded file in heap
* response bodies were dropped when the server sent no Content-Type
* gzip HTTP/2 responses were not decoded
* spilled upload files were deleted between internal redirect attempts
* setBodySize() reported the truncated length rather than the wire length

Extract Http2CapturingHttpURLConnection from HTTPJavaImpl and add
SpillOutputStream; commons-io is not a dependency of this module.
…meTracker to measure connection establishment duration and update sample results accordingly
… size and response bytes accounting across HTTP implementations. Update tests to validate compressed body size handling.
…oduce CountingOutputHttpURLConnection to track request body size accurately. Update sent bytes calculation and add Content-Length header when missing. Add tests for large file uploads.
- Remove eager static initializer in `HTTPJavaImpl` that mutated global JVM system properties on class loading.
- Apply HTTP/2 `jdk.httpclient.*` system properties lazily and idempotently when the first HTTP/2 client instance is created.
- Skip setting `jdk.httpclient.enablepush` when `http.java.h2.push_enabled` is not explicitly defined in JMeter properties, preserving JDK defaults for other JVM components.
- Update `TestHTTPJavaFeatures` to verify JDK defaults remain intact when properties are unset.
…th is enabled

Wrap the HTTP/2 response body stream in a try-with-resources block in
HTTPJavaImpl#readResponse(HttpResponse<InputStream>, SampleResult).
Previously, when httpsampler.obey_contentlength was set to true and the server
returned Content-Length: 0, the method returned early without closing the
response stream, leaking JDK HTTP/2 streams.
Replace tests that could not fail with ones that pin down the behaviour
they name:

- Drop the reflective setMessageMultiplexing() lookup, httpclient5 is
  pinned at 5.6.4, and remove the three tests that parsed the compiled
  class file to check for it.
- Assert the exact sent bytes against WireMock's request journal. This
  uncovered that calculateSentBytes() ignored the headers the protocol
  layer writes itself, so Host, Content-Type, Content-Encoding and
  Content-Length/Transfer-Encoding are now accounted for as well.
- Count the accepted TCP connections in both multiplexing tests instead
  of only checking four HTTP/2 200s. The relay that does this moved to a
  shared TcpRelay test helper.
- Assert JMeter's own HTTP/2 settings and the property overrides they
  exist for, instead of re-asserting the H2Config defaults of httpcore5.
- Close the clients both suites create, shorten the re-validation wait
  from 2.5 s to 0.5 s and drop a pointless sleep.
…rce keys

- Replace hardcoded "HTTP/2 Negotiate" string in HttpVersionComboBox with `http_version_2_negotiate` resource string
- Add `http_version_2_negotiate` to `messages.properties`
- Sort `http_version` property alphabetically across all 12 message bundles
- Add unit test in `TestHttpVersionComboBox` verifying HTTP/2 label rendering
…ombos in clearGui

- Normalize blank `httpVersion` and `httpImplementation` selections to `null` in `HttpDefaultsGui` and `HttpTestSampleGui`
- Reset implementation and version combo boxes in `clearGui()` for both HTTP GUI components
- Add unit tests in `TestHttpDefaultsGui` and `TestHttpTestSampleGui`
Remove HTTPConstants.HTTP_2, which duplicated HTTP_VERSION_2. Derive
HTTPSamplerBase.HTTP_VERSION from the schema and rename
HTTPHCAbstractImpl.HTTP_VERSION to DEFAULT_HTTP_VERSION, so the same
simple name no longer means both a property name and its value.

HTTPHC5Impl compared "HTTP/2" exactly but "HTTP/2 Strict" ignoring case,
so a stored "http/2" fell back to HTTP/1.1 and disagreed with
HTTPJavaImpl. Compare both ignoring case and cover it with a test.
The httpclient.version property now selects HTTP/1.1, HTTP/2 or HTTP/2 Strict,
but it historically only took 1.0 and 1.1. Two readers were left behind:
AjpSampler compared against the old spellings, so the documented values were
wrong for AJP, and HTTPHCAbstractImpl defaulted to "1.1", which is not a
recognized version for the HttpClient5 and Java implementations.

Read the property through HTTPAbstractImpl.readDefaultHttpVersion(), which maps
the legacy values 1.0 and 1.1 to HTTP/1.1, and let AjpSampler accept both 1.0
and HTTP/1.0 for its request line. Document both spellings and the AJP behaviour
in jmeter.properties, properties_reference.xml and changes.xml.
changes.xml described only the HTTP/2 details and assumed the HttpClient5
implementation already existed, so the headline features of this release
were missing: the new sampler implementation itself and the per-sampler
HTTP Version field backed by the new HTTPSampler.httpVersion property.

Add a changes.xml entry for each, and document HTTP Version in the
HTTP Request Defaults properties block of component_reference.xml, which
HttpDefaultsGui gained but the reference never listed.

Also spell the implementations HttpClient4 and HttpClient5 as the aliases
do, instead of the pre-existing HTTPClient4/HTTPClient5 typo, in
component_reference.xml and in the properties_reference.xml section title.
… sampler implementations

HTTPHC5Impl carried its own copy of the HttpClient4 cookie, header and form
entity handling, and CountingOutputStream existed a fourth time. Lift the
type-independent parts into HTTPHCAbstractImpl behind a HeaderIterable seam,
add HTTPMessageSizes for the wire lengths shared with HTTPJavaImpl, and reuse
jorphan's CountingOutputStream.

Along the way the copies that had drifted are unified: HttpClient5 now strips
a default port from a Host header of the Header Manager, multiple Cookie
headers are joined, sent bytes are measured with ISO-8859-1, and jorphan's
CountingOutputStream counts single byte writes again.
Replace the per implementation saveDetails/inCache/setHeaders overloads with
saveDetails(ResponseHeaderSource, HTTPSampleResult), inCache(URL, RequestHeaderSource)
and setHeaders(URL, RequestHeaderSink), so the Vary and cache-control handling lives in
one place and the config element no longer compiles against HttpClient 5. The sampler
implementations adapt their requests and responses at the call sites, and the
HttpClient 4 typed methods stay as deprecated delegators.
The Java implementation shared one HttpClient with all threads, so a test
drove N virtual users over a single connection and lost connection level
effects. The clients are now held in an InheritableThreadLocal, so a thread
still multiplexes its own requests and the parallel downloads of its embedded
resources over one connection per origin, while each thread connects on its
own. The former behaviour is available with the new
http.java.h2.share_connections_between_threads property, which replaces
http.java.h2.multiplexing and defaults to false.
The threads that download embedded resources in parallel are pooled and
shared by all JMeter threads, so an InheritableThreadLocal bound them to
whichever JMeter thread created them. They kept using that thread's HTTP
client, which failed samples of other threads with IOException, "Socket
closed" or "Connection pool shut down" as soon as the owning thread
finished or started a new iteration.

Look the clients up by the JMeterContext the downloader thread adopts
from the JMeter thread it works for, in the HttpClient4, HttpClient5 and
Java sampler implementations. This keeps HTTP/2 multiplexing between a
sample and its embedded resources and stops a thread from closing
clients another one is still using.
JMeter hands out an SSLContext per thread, so a pooled thread downloading
embedded resources considered the HTTP/2 client of the JMeter thread it works
for outdated, replaced it and shut it down while the sample and its sibling
downloads were still using it, failing them with
"java.io.IOException: shutdownNow".
The Java sampler implementation is left as it was, so HTTPJavaImpl keeps using
HttpURLConnection and the HTTP Version combo box offers HTTP/1.1 only for it, as
for HttpClient4. HTTP/2 stays with the HttpClient5 implementation.

Drop the helpers only that extension needed (ConnectTimeTracker,
CountingOutputHttpURLConnection, Http2CapturingHttpURLConnection, the
HTTPAbstractImpl testEnded hook), the http.java.h2.* properties and the
documentation of both. The shared parts of the earlier bugfixes stay:
HTTPMessageSizes, SpillOutputStream, the HTTP client neutral CacheManager API
and the jorphan CountingOutputStream fix.
@andreaslind01

Copy link
Copy Markdown
Contributor Author

Thanks for the review @vlsi.

All findings have been addressed.

Concerning D3, the changes in HTTPJavaImpl have been reverted. The additional HTTP/2 implementation based on java.net.http is therefore no longer part of this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants