Skip to content

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

Merged
gunjansingh-msft merged 4 commits into
mainfrom
core/vertx-expect-continue
Sep 4, 2026
Merged

gunjansingh-msft merged 4 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

The fallback was cancelled only from continueHandler, so a service that
rejected the headers with a final status left the timer armed. It would
then write onto a finished exchange, and for a streaming body would
subscribe the source needlessly.

Cancel the fallback and claim the write when the response future reaches
any terminal state, so neither the timer nor a late interim response can
send the body afterwards. Adds a regression test for a service that
answers the headers with 401 and never reads the body.

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

Final responses with slowly consumed bodies can still trigger the fallback upload, and the rejection test cannot detect it.

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

Review details

Suppressed comments (1)

sdk/core/azure-core-http-vertx/src/main/java/com/azure/core/http/vertx/VertxHttpClient.java:223

  • This completion hook is too late for eagerly buffered responses. In sendInternal, responseFuture completes as soon as final response headers arrive, but promise is not completed until vertxHttpResponse.body() finishes (lines 153-169). If a service rejects the headers and slowly streams its final response body for more than one second, the fallback fires and uploads the request body despite the rejection. Cancel/claim the body write when the final responseFuture completes, rather than waiting for the public response promise.
            promise.future().onComplete(ignored -> {
                cancelFallback.run();
                bodyWritten.set(true);
            });
  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Balanced

@gunjansingh-msft
gunjansingh-msft merged commit 8bf370d into main Sep 4, 2026
35 checks passed
@gunjansingh-msft
gunjansingh-msft deleted the core/vertx-expect-continue branch September 4, 2026 10:39
Isabelle (ibrandes) added a commit that referenced this pull request Oct 28, 2026
* [Storage] Add Expect: 100-continue support

Applies the HTTP header Expect: 100-continue to requests that carry a
body, so that a body is not uploaded just to be rejected while the
service is under load. The service can reject the request before a
single byte of body is sent, which cuts the bandwidth wasted retrying
large uploads against a throttling service.

- Adds Request100ContinueOptions and Request100ContinueMode (AUTO,
  ALWAYS, NEVER) to azure-storage-common. AUTO, the default, applies the
  header only for a window after the service responds 429, 500, or 503,
  so the extra round trip is only paid when it earns its keep.
- The policies are added below the retry policy so a throttling response
  takes effect on the very next attempt rather than only on a later
  call, and above the credential policies since headers may affect the
  string to sign.
- Wired into the blob, data lake, file share, and queue pipelines, and
  exposed on the blob client builders via request100ContinueOptions.
- Can be turned off without a code change by setting the system property
  or environment variable AZURE_STORAGE_DISABLE_EXPECT_CONTINUE_HEADER.

Ported from azure-sdk-for-net #40611 and #52438.

* [Storage] Address PR feedback on Expect: 100-continue

- Rename Request100ContinueOptions/Request100ContinueMode to
  ExpectContinueOptions/ExpectContinueMode, the mode values to
  APPLY_ON_THROTTLE/ON/OFF, autoInterval to throttleInterval, and the
  builder method to expectContinueBehavior, matching the .NET names.
- Make ExpectContinueOnThrottlePolicy extend HttpPipelineSyncPolicy
  rather than ExpectContinuePolicy. Both policies are now final and
  share their logic through a package private helper, so neither
  advertises an extension point that callers outside the package cannot
  actually use.
- Allow the Expect header in the blob default log options, so it is
  visible when debugging whether a request negotiated the handshake.
- Replace the unrelated timeouts link in the options javadoc with RFC
  9110.
- Add ExpectContinueTransportTests, which drives each HTTP client against
  a real socket and asserts on what reached the wire and when.

The transport tests show that setting the header does not by itself make
a client wait. Only okhttp performs the handshake. netty and vertx send
the header and stream the body immediately, and jdk-httpclient drops the
header entirely since Expect is restricted by java.net.http.HttpClient.
That is recorded in the test matrix and documented on ExpectContinueOptions
and in the changelog rather than left implicit.

* [Storage] Test the allowed Expect log header, trim changelog

Cover the default log options change with an assertion, and keep the
changelog to the feature itself. The transport caveat stays on
ExpectContinueOptions, where the behavior it describes lives.

* [Storage] Match .NET exactly on enum names and log the Expect header everywhere

Use the .NET casing for the mode values, ApplyOnThrottle / On / Off,
rather than Java's upper snake case. Checkstyle accepts it.

Allow the Expect header in the default log options of the queue, file
share and data lake builders too, not only blob, as the review comment
was a general one.

* [Storage] Mirror the .NET expect-continue tests and cover the async path

Restructure ExpectContinueTests so the first section mirrors
ExpectContinueTests.cs test for test, with the same names and the same
case values, making the two suites directly comparable. Java specific
coverage, including the per retry behaviour that .NET has no test for,
moves to a clearly separated second section.

ThrottlePolicyRevertsAfterBackoff is disabled in .NET
(Azure/azure-sdk-for-net#41368); the Java mirror is enabled, since the
window is a monotonic nanoTime deadline and oversleeping it can only
close the window.

Extend the transport test to transport x sync/async, as those are
separate code paths in every client. All four transports behave the same
on both paths, so the earlier finding is unchanged.

* [Storage] Scope expect-continue to blob only and trim comments

Revert the queue, file share and data lake packages entirely, so the
header is applied only where a customer can configure it.

Drop the explanatory comments that were not carrying their weight,
including the cross references to the .NET implementation.

* [Storage] Remove an unrelated file that was committed by mistake

sdk/storage/TYPESPEC_MIGRATION_ESTIMATE.md is unrelated to this change
and was picked up accidentally.

* [Storage] Move the wire level tests into ExpectContinueTests

Keep them in the class the review comment was on rather than a separate
file. Each one opens and closes its own server, so the socket fixture
does not run for the tests that do not need it.

* [Storage] Address review feedback on Expect: 100-continue

- Drop the azure-storage-common changelog entry; the changes there are
  implementation level and the customer facing option is on the blob
  builders.
- Remove the okhttp, vertx and jdk test dependencies. They already come
  transitively from azure-core-test, so they were redundant.
- Move ExpectContinuePolicy and ExpectContinueOnThrottlePolicy into
  com.azure.storage.common.policy alongside the other storage policies,
  and rename the shared helper to ExpectContinuePolicyHelper.
- Use upper snake case for the mode constants.
- Reject a negative content length threshold or throttle interval rather
  than silently changing behaviour, with boundary tests for both.

* [Storage] Do not apply Expect: 100-continue unless it is configured

The handshake needs the HTTP client to withhold the request body until
the service responds, and the default transport does not do that, so a
default client would pay for the header and save nothing. The policy is
now added only when expectContinueBehavior is set.

* adding support for expect continue 100 to azure core netty

* Avoid the deprecated URL constructor in the netty observation test

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 and
took the whole build down with it.

* Expect the JDK and Vert.x clients to defer the request body

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.

* Let the JDK and Vert.x expectations follow the azure-core version

Those two clients honour the handshake from azure-core 1.2.0-beta.1
onwards, so the behaviour depends on whether the build resolves a
released artifact or the repository source. Asserting either one fails
half the pipeline, so assert what holds either way: the exchange
completes, the body arrives in full, and a withheld body implies the
header reached the wire.

Once Storage consumes a release carrying the fix these become
DEFERS_BODY. Netty and okhttp keep their exact expectations.

* Revert "Let the JDK and Vert.x expectations follow the azure-core version"

This reverts commit 644c196.

* Revert "Storage - STG105 NFS List V2 Files and Directories (#50036)"

This reverts commit 83dc6e2.

* add recordings for prev features that were missing

* addressing comments and fixing problems left by base merge

* addressing comment

* preventing expect continute jdk tests from running on java 11 or lower

* moving to internal.policy

* addressing comments

* removing dupe import

* fixing flaky test

* assets tag update

---------

Co-authored-by: Isabelle <ibrandes@microsoft.com>
Co-authored-by: browndav <browndav@microsoft.com>
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.

3 participants