Skip to content

Add h2c prior-knowledge client support#12555

Merged
yawkat merged 6 commits into
5.0.xfrom
feat/h2c-prior-knowledge-client
May 8, 2026
Merged

Add h2c prior-knowledge client support#12555
yawkat merged 6 commits into
5.0.xfrom
feat/h2c-prior-knowledge-client

Conversation

@yawkat

@yawkat yawkat commented Mar 23, 2026

Copy link
Copy Markdown
Member

Summary

  • add a dedicated H2C_PRIOR_KNOWLEDGE plaintext client mode
  • initialize Netty HTTP/2 connections eagerly for prior-knowledge cleartext clients
  • add end-to-end integration coverage for Micronaut Netty client h2c prior knowledge against a real embedded server
  • keep a focused negative unit test for prior-knowledge against an HTTP/1 peer

Verification

  • JAVA_HOME=/usr/lib/jvm/java-25-openjdk ./gradlew :micronaut-http-server-netty:test --tests io.micronaut.http.server.netty.http2.H2cPriorKnowledgeClientSpec

Resolves #10762

@graemerocher graemerocher added the type: enhancement New feature or request label Mar 23, 2026
Copilot AI review requested due to automatic review settings April 20, 2026 09:55

Copilot AI 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.

Pull request overview

Adds Netty HTTP client support for h2c prior-knowledge (cleartext HTTP/2 without HTTP/1.1 upgrade), including eager HTTP/2 pipeline initialization and test coverage (integration + negative unit test).

Changes:

  • Introduce new PlaintextMode.H2C_PRIOR_KNOWLEDGE selection in HttpVersionSelection.
  • Add a Netty ConnectionManager initializer that configures HTTP/2 prior-knowledge connections without waiting for an upgrade/settings handshake.
  • Add test coverage: an end-to-end embedded-server spec plus a focused negative test against an HTTP/1 peer.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
http-server-netty/src/test/groovy/io/micronaut/http/server/netty/http2/H2cPriorKnowledgeClientSpec.groovy New embedded-server integration spec validating h2c prior-knowledge for blocking + streaming client APIs.
http-client/src/test/groovy/io/micronaut/http/client/netty/ConnectionManagerSpec.groovy Adds a negative unit test verifying failure behavior against an HTTP/1 peer when configured for h2c prior knowledge.
http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java Implements a dedicated prior-knowledge HTTP/2 channel initializer and adjusts HTTP/2 init flow to support eager init.
http-client-core/src/main/java/io/micronaut/http/client/HttpVersionSelection.java Adds H2C_PRIOR_KNOWLEDGE to the plaintext mode enum.

Comment thread http-client/src/main/java/io/micronaut/http/client/netty/ConnectionManager.java Outdated
Copilot AI review requested due to automatic review settings April 20, 2026 12:36

Copilot AI 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.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

Comment on lines +109 to +116
return Flux.create { sink ->
new Thread({
sink.next('f'.getBytes(StandardCharsets.UTF_8))
TimeUnit.SECONDS.sleep(1)
sink.next('oo'.getBytes(StandardCharsets.UTF_8))
sink.complete()
}).start()
}

Copilot AI Apr 20, 2026

Copy link

Choose a reason for hiding this comment

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

Spawning a raw new Thread per request and calling sleep without handling interruption/exception can leak threads and leave the Flux uncompleted if the sleep is interrupted (e.g., during shutdown), which can make the test flaky/hang. Prefer using Reactor operators (e.g., delayed emission via scheduler) or, at minimum, wrap the body in try/catch to call sink.error(...) (and restore interrupt flag) on failure.

Copilot uses AI. Check for mistakes.
Comment on lines +53 to +79
private String stream(String url) {
def composed = new StringBuilder()
def future = new CompletableFuture<Void>()
streamingHttpClient.dataStream(HttpRequest.GET(url)).subscribe(new Subscriber<ByteBuffer<?>>() {
@Override
void onSubscribe(Subscription s) {
s.request(Long.MAX_VALUE)
}

@Override
void onNext(ByteBuffer<?> byteBuffer) {
composed.append(new String(byteBuffer.toByteArray(), StandardCharsets.UTF_8))
}

@Override
void onError(Throwable t) {
future.completeExceptionally(t)
}

@Override
void onComplete() {
future.complete(null)
}
})
future.get(10, TimeUnit.SECONDS)
return composed.toString()
}

Copilot AI Apr 20, 2026

Copy link

Choose a reason for hiding this comment

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

This manual Subscriber approach makes timeouts/error handling harder: if future.get(...) times out (or the test fails early), the subscription is not cancelled, which can keep network activity running into teardown and increase flakiness. Consider expressing this with reactive operators (collect/join) and a bounded block/timeout so cancellation happens deterministically.

Copilot uses AI. Check for mistakes.
when:
conn.clientChannel.close()
conn.advance()
future.get()

Copilot AI Apr 20, 2026

Copy link

Choose a reason for hiding this comment

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

future.get() has no timeout and relies on the method-level @Timeout(30) to stop hangs. Using a bounded wait (e.g., get(… , TimeUnit.SECONDS)) here will fail faster and produce clearer diagnostics when the negative-path behavior regresses.

Suggested change
future.get()
future.get(5, TimeUnit.SECONDS)

Copilot uses AI. Check for mistakes.
return;
} else {
log.warn("Premature frame: {}", msg.getClass());
ctx.pipeline().remove(ctx.name());

Copilot AI Apr 20, 2026

Copy link

Choose a reason for hiding this comment

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

The handler removes itself via ctx.pipeline().remove(ctx.name()), which is more brittle than removing by instance. Prefer ctx.pipeline().remove(this) (or ctx.pipeline().remove(<handler instance>)) to avoid coupling to the handler name and to make the intent clearer.

Suggested change
ctx.pipeline().remove(ctx.name());
ctx.pipeline().remove(this);

Copilot uses AI. Check for mistakes.
@yawkat

yawkat commented Apr 22, 2026

Copy link
Copy Markdown
Member Author

Blocked on #12624

Copilot AI review requested due to automatic review settings April 22, 2026 09:37

Copilot AI 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.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

return Flux.create { sink ->
new Thread({
sink.next('f'.getBytes(StandardCharsets.UTF_8))
TimeUnit.SECONDS.sleep(1)

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

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

This introduces an unconditional 1s real-time sleep per /testStream call, which will slow down the overall test suite (this spec calls /testStream multiple times). Consider reducing the delay substantially (e.g., tens of ms) or using a deterministic mechanism that doesn't rely on wall-clock sleeping while still validating chunking/streaming semantics.

Suggested change
TimeUnit.SECONDS.sleep(1)
TimeUnit.MILLISECONDS.sleep(10)

Copilot uses AI. Check for mistakes.

then:
def e = thrown ExecutionException
e.cause != null

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

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

The assertion only checks that an exception has a cause, which makes the test relatively weak (it will pass for many unrelated failures). Tighten this by asserting the expected exception type and/or a stable part of the message (or a known Netty/Micronaut exception category) so the test better documents and validates the intended failure mode.

Suggested change
e.cause != null
e.cause instanceof io.micronaut.http.client.exceptions.HttpClientException
e.cause.message != null
e.cause.message.toLowerCase().contains('closed')

Copilot uses AI. Check for mistakes.
@sonarqubecloud

Copy link
Copy Markdown

@yawkat yawkat merged commit d1bed7f into 5.0.x May 8, 2026
18 checks passed
@yawkat yawkat deleted the feat/h2c-prior-knowledge-client branch May 8, 2026 08:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

h2c prior knowledge client support

3 participants