Skip to content

[Storage] Add Expect: 100-continue support - #50094

Open
gunjansingh-msft wants to merge 14 commits into
feature/storage/stg105basefrom
feature/storage/expect-100-continue
Open

[Storage] Add Expect: 100-continue support#50094
gunjansingh-msft wants to merge 14 commits into
feature/storage/stg105basefrom
feature/storage/expect-100-continue

Conversation

@gunjansingh-msft

@gunjansingh-msft gunjansingh-msft commented Aug 11, 2026

Copy link
Copy Markdown
Member

Adds support for the HTTP header Expect: 100-continue on requests that carry a body. When the transport honours it, the service can reject a request before a single byte of the body is sent, which cuts the bandwidth wasted re-uploading against a throttling service.

  • Adds ExpectContinueOptions and ExpectContinueMode (APPLY_ON_THROTTLE, ON, OFF) to azure-storage-common, along with the two policies. APPLY_ON_THROTTLE 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.
  • Exposed on the blob client builders as expectContinueBehavior. Blob only, matching the Go SDK, which ships this in azblob and not in azqueue, azfile or azdatalake.
  • The policy is 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.
  • Not applied unless configured. The handshake needs the HTTP client to withhold the request body until the service responds, and the default transport does not do that (see below), so a default client would pay for the header and save nothing.
  • Can also be turned off without a code change via the system property or environment variable AZURE_STORAGE_DISABLE_EXPECT_CONTINUE_HEADER, matching .NET and Go.

Transport support

ExpectContinueTests drives each HTTP client against a real socket and asserts on what reached the wire and when:

Transport Behaviour
azure-core-http-okhttp Withholds the body until 100 Continue — the handshake works
azure-core-http-netty (default) Sends the header, streams the body immediately
azure-core-http-vertx Same as netty
azure-core-http-jdk-httpclient Drops the header; Expect is restricted by java.net.http.HttpClient

This is an azure-core gap rather than a Storage one. .NET and Go enable the feature by default because their transports perform the handshake — SocketsHttpHandler in .NET, and Go's net/http with azcore setting ExpectContinueTimeout: 1s. Once an equivalent lands for netty, flipping the default back to APPLY_ON_THROTTLE is a one-line change.

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

Description

Please add an informative description that covers that changes made by the pull request and link all relevant issues.

If an SDK is being regenerated based on a new swagger spec, a link to the pull request containing these swagger spec changes has been included above.

All SDK Contribution checklist:

  • The pull request does not introduce [breaking changes]
  • CHANGELOG is updated for new features, bug fixes or other significant changes.
  • I have read the contribution guidelines.

General Guidelines and Best Practices

  • Title of the pull request is clear and informative.
  • There are a small number of commits, each of which have an informative message. This means that previously merged commits do not appear in the history of the PR. For more information on cleaning up the commits in your PR, see this page.

Testing Guidelines

  • Pull request includes test coverage for the included changes.

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.
@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.

@ibrandes Isabelle (ibrandes) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

first pass :) let me know if you have any questions - thanks for picking this feature up!

- 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.
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.
…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.
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.
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.
sdk/storage/TYPESPEC_MIGRATION_ESTIMATE.md is unrelated to this change
and was picked up accidentally.
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.
Comment on lines +6 to +10
- Added `ExpectContinueOptions` and `ExpectContinueMode`, which configure when the HTTP header
`Expect: 100-continue` is applied to requests that carry a body. By default the header is applied only for a
period after the service responds 429, 500, or 503, so that a body is not uploaded just to be rejected again.
The header can also be turned off without a code change by setting the system property or environment variable
`AZURE_STORAGE_DISABLE_EXPECT_CONTINUE_HEADER` to `true`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

we don't need to add this changelog - all of the changes we made to the common package are implementation level changes

@gunjansingh-msft gunjansingh-msft Aug 19, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

yes this has been removed

Comment on lines +93 to +113

<!-- The remaining HTTP client implementations, so that ExpectContinueTransportTests can verify that each one
actually withholds the request body until the service answers 100 Continue. -->
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-core-http-okhttp</artifactId>
<version>1.13.5</version> <!-- {x-version-update;com.azure:azure-core-http-okhttp;dependency} -->
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-core-http-jdk-httpclient</artifactId>
<version>1.1.5</version> <!-- {x-version-update;com.azure:azure-core-http-jdk-httpclient;dependency} -->
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-core-http-vertx</artifactId>
<version>1.1.5</version> <!-- {x-version-update;com.azure:azure-core-http-vertx;dependency} -->
<scope>test</scope>
</dependency>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

do we need these imports? i think we have helper methods to test these client types - StorageCommonTestUtils.createVertxHttpClient and StorageCommonTestUtils.createJdkHttpClient could be useful, along with OK_HTTP_CLIENT from the same class.

@gunjansingh-msft gunjansingh-msft Aug 19, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

they were already coming in transitively from azure-core-test, so the pom is back to what it was.

* <p>
* RESERVED FOR INTERNAL USE.
*/
public final class ExpectContinueOnThrottlePolicy extends HttpPipelineSyncPolicy {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why is this and ExpectContinuePolicy in the implementation policy package (azure\storage\common\implementation\policy)? i noticed our other policies exist in azure\storage\common\policy, so I was wondering why these weren't in the same place.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

No good reason - moved both policies, and the helper, into com.azure.storage.common.policy next to the others.

* <p>
* RESERVED FOR INTERNAL USE.
*/
final class ExpectContinueSupport {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: rename this to ExpectContinuePolicyHelper to follow existing helper class patterns

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Renamed to ExpectContinuePolicyHelper.

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 configurable Expect: 100-continue policies to Storage Common and integrates them into Blob client pipelines.

Changes:

  • Adds always/on-throttle/off modes, thresholds, throttling windows, and global opt-out support.
  • Exposes configuration through Blob builders.
  • Adds policy, retry-ordering, configuration, and transport tests.

Reviewed changes

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

Show a summary per file
File Description
azure-storage-common/.../ExpectContinueTests.java Tests policy and transport behavior.
azure-storage-common/.../ExpectContinueOptions.java Defines public configuration options.
azure-storage-common/.../ExpectContinueMode.java Defines application modes.
azure-storage-common/.../ExpectContinueSupport.java Implements shared eligibility and header logic.
azure-storage-common/.../ExpectContinuePolicy.java Adds the header unconditionally when eligible.
azure-storage-common/.../ExpectContinueOnThrottlePolicy.java Activates the header following throttling responses.
azure-storage-common/.../Constants.java Adds the configuration opt-out key.
azure-storage-common/.../BuilderUtils.java Creates the selected policy.
azure-storage-common/pom.xml Adds transport test dependencies.
azure-storage-common/CHANGELOG.md Documents the common API.
azure-storage-blob/.../BuilderHelperTests.java Tests Blob pipeline integration.
azure-storage-blob/.../SpecializedBlobClientBuilder.java Exposes the option on specialized builders.
azure-storage-blob/.../BuilderHelper.java Installs the policy in Blob pipelines.
azure-storage-blob/.../BlobServiceClientBuilder.java Exposes service-level configuration.
azure-storage-blob/.../BlobContainerClientBuilder.java Exposes container-level configuration.
azure-storage-blob/.../BlobClientBuilder.java Exposes blob-level configuration.
azure-storage-blob/CHANGELOG.md Documents the Blob builder API.
Suppressed comments (1)

sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/policy/ExpectContinueOptions.java:90

  • A negative interval is accepted here and later converted to zero, silently making ApplyOnThrottle behave like Off. Reject negative durations at this public API boundary so configuration errors are diagnosable, and cover the validation in the options tests.
    public ExpectContinueOptions setThrottleInterval(Duration throttleInterval) {
        this.throttleInterval = throttleInterval == null ? DEFAULT_THROTTLE_INTERVAL : throttleInterval;

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

Comment on lines +342 to +345
{ "netty", "com.azure.core.http.netty.NettyAsyncHttpClientProvider", ContinueSupport.SENDS_HEADER_ONLY },
{ "okhttp", "com.azure.core.http.okhttp.OkHttpAsyncClientProvider", ContinueSupport.DEFERS_BODY },
{ "jdk", "com.azure.core.http.jdk.httpclient.JdkHttpClientProvider", ContinueSupport.DROPS_HEADER },
{ "vertx", "com.azure.core.http.vertx.VertxHttpClientProvider", ContinueSupport.SENDS_HEADER_ONLY } };

@gunjansingh-msft gunjansingh-msft Aug 19, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Isabelle (@ibrandes) this is the one I would like your call on.

I went with the second half of your suggestion: the header is no longer applied unless expectContinueBehavior is set, so a default client never pays for it. That avoids requiring DEFERS_BODY, which would fail 6 of the 8 transport cases and leave the build red.

Worth flagging that this diverges from the other languages - .NET and Go both default it on. They can, because their transports do the handshake: SocketsHttpHandler in .NET, and Go's net/http with azcore setting ExpectContinueTimeout: 1s.

Are you happy with opt-in, or would you rather we hold the feature until the transports support it?

gunjansingh-msft and others added 3 commits August 19, 2026 11:19
- 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.
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.
…-100-continue

The only conflict was in the blob BuilderHelper, where both sides added
policies at the same point. Both are kept. The expect-continue policy is
placed after the content validation policies, as the encoding policy
rewrites the body and resets Content-Length, and the threshold check
needs to read the encoded length that is actually sent.
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.
@gunjansingh-msft
gunjansingh-msft force-pushed the feature/storage/expect-100-continue branch from 1d64764 to 40f35a4 Compare September 1, 2026 18:45
@gunjansingh-msft

Copy link
Copy Markdown
Member Author

Note on merge order: this should land after the two azure-core transport fixes, #50311 (JDK) and #50312 (Vert.x).

The transport matrix in ExpectContinueTests records what each HTTP client does with Expect: 100-continue today. Two of those rows become wrong once those core changes ship and Storage picks up the new version:

{ "jdk",   "...JdkHttpClientProvider",   ContinueSupport.DROPS_HEADER },
{ "vertx", "...VertxHttpClientProvider", ContinueSupport.SENDS_HEADER_ONLY },

Both become DEFERS_BODY. The tests will fail deliberately at that point with "is recorded as dropping the header; update this table if that has changed", which is the intended behaviour - a transport gaining support should not pass silently - but it does mean the table needs updating in the same change that bumps the core dependency.

Merging the core PRs first means this PR can be updated once, with the matrix and the default both reflecting reality, rather than needing a follow-up.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Storage Storage Service (Queues, Blobs, Files)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants