Skip to content

[Core] Honour Expect: 100-continue in the Vert.x HTTP client - #50312

Open
gunjansingh-msft wants to merge 3 commits into
mainfrom
core/vertx-expect-continue
Open

[Core] Honour Expect: 100-continue in the Vert.x HTTP client#50312
gunjansingh-msft wants to merge 3 commits into
mainfrom
core/vertx-expect-continue

Conversation

@gunjansingh-msft

@gunjansingh-msft gunjansingh-msft commented Sep 1, 2026

Copy link
Copy Markdown
Member

VertxHttpClient.sendBody always wrote the headers and the body together, so a request carrying Expect: 100-continue behaved exactly like one without it: the body went out immediately and the service had no opportunity to reject it first.

Vert.x already exposes what is needed - sendHead() on the request and continueHandler() on the client stream. When the header is present, this sends the head and writes the body from continueHandler; otherwise the existing path is unchanged.

The body writing has been extracted so both branches share it, which means this covers both the in-memory bodies and the reactive Flux<ByteBuffer> / InputStream path. The in-memory branch switches between send(buffer) and end(buffer) depending on whether the head has already gone out; the streaming path uses write/end and works unchanged in both cases.

Verification

VertxHttpClientExpectContinueTests drives the client against a raw socket that deliberately delays its 100 Continue, and asserts on what reached the wire and when:

Before After
Expect header on the wire present present
Body bytes before 100 Continue 1024 (sent immediately) 0
Body delivered after 100 Continue n/a 1024

A second test asserts a request without the header still sends its body immediately, so the behaviour is scoped to requests that opt in.

The 32 pre-existing errors in this module's suite are unrelated - they reproduce identically on main without this change (47 tests / 32 errors on main, 49 tests / 32 errors here, the two extra being the new passing tests).

Expect-continue fallback

RFC 9110 says a client "SHOULD NOT wait for an indefinite period before sending the content", and a service may ignore the expectation entirely and simply wait for the body. The body is therefore sent after a one second fallback if no interim response arrives, matching the defaults used by .NET and Go. The write is guarded so the body goes out once whether the interim response or the fallback wins, and the timer is cancelled when continueHandler fires. sendsBodyWhenTheServiceIgnoresTheExpectation covers this against a server that never answers.

The interval is a constant rather than a configurable option; happy to expose it if you would prefer, and it would be worth being consistent across the transports.

Context

This came out of adding Expect: 100-continue support to the Storage blob clients (#50094). Of the four transports, only azure-core-http-okhttp performs the handshake today. A companion change for the JDK client is in #50311. azure-core-http-netty is a larger problem: neither netty nor reactor-netty has client side support, and the reactor-netty maintainers consider sending the body immediately to be RFC compliant (reactor/reactor-netty#4353).

sendBody always wrote the headers and the body together, so a request
carrying Expect: 100-continue behaved exactly like one without it and the
service had no opportunity to reject it before receiving the body.

Vert.x already exposes the two pieces needed. When the header is present,
send the head and write the body from continueHandler; otherwise keep the
existing behaviour. The body writing is now shared by both paths, so this
covers in-memory and streaming bodies alike.

Adds wire level tests that drive the client against a socket which delays
its 100 Continue, asserting that no body bytes arrive before the response
and that the full body arrives after it.
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).
35 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

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

Updates the Vert.x transport in azure-core to properly honor Expect: 100-continue by sending request headers first and deferring request-body transmission until the server signals 100 Continue, aligning the transport’s on-the-wire behavior with HTTP expectations.

Changes:

  • Teach VertxHttpClient to use sendHead() + continueHandler() when Expect: 100-continue is present, and reuse a shared body-writing path.
  • Add a socket-level unit test validating that the body is withheld until 100 Continue, and that requests without the header remain unchanged.
  • Document the behavior change in the module changelog.

Reviewed changes

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

File Description
sdk/core/azure-core-http-vertx/src/main/java/com/azure/core/http/vertx/VertxHttpClient.java Defers body write on Expect: 100-continue using sendHead()/continueHandler() and factors body writing into a helper.
sdk/core/azure-core-http-vertx/src/test/java/com/azure/core/http/vertx/VertxHttpClientExpectContinueTests.java Adds raw-socket tests that assert on wire timing and presence/absence of Expect behavior.
sdk/core/azure-core-http-vertx/CHANGELOG.md Notes new Expect: 100-continue handshake behavior in Vert.x transport.

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

The core modules compile with -Werror, and URL(String) is deprecated as
of Java 20, so the test failed to compile on the JDK 25 build agents.
@gunjansingh-msft

Copy link
Copy Markdown
Member Author

#50094 (Storage blob Expect: 100-continue) is waiting on this one. Its transport matrix currently records this client as not performing the handshake, and those expectations flip once this ships, so the intent is to merge this first and update Storage in the same change that bumps the core dependency.

gunjansingh-msft added a commit that referenced this pull request Sep 1, 2026
Records the behaviour those transports have once the azure-core fixes in
#50311 and #50312 ship. Until Storage picks up those versions the four
cases fail, which is the intended signal rather than a silent mismatch.

Netty stays as it is: the change there observes the interim response and
does not defer the body.
Send the body after a one second fallback if the service never answers
the expectation, so a server that ignores it cannot stall the request
until the response timeout. The body is guarded so it is written once
whether the interim response or the fallback comes first, and the timer
is cancelled on continue.

Expect is a comma separated list and values may carry whitespace, so
compare each expectation rather than the header as a whole.

The test server now reads for the whole settle window rather than taking
an available() snapshot after a sleep. Adds cases for the header spelling
variants and for a service that ignores the expectation entirely.

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.

🟡 Changes recommended

The fallback timer can write the body after the request has already received a final response or failed.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment on lines +207 to +210
io.vertx.core.Context vertxContext = Vertx.currentContext();
Long fallbackTimerId = vertxContext == null
? null
: vertxContext.owner().setTimer(EXPECT_CONTINUE_TIMEOUT.toMillis(), ignored -> writeOnce.run());
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Azure.Core azure-core

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants