From a82eaab9506c5591edf83e1050223d348c7cf00d Mon Sep 17 00:00:00 2001 From: George Fu Date: Wed, 22 Oct 2025 13:00:19 -0400 Subject: [PATCH 1/2] feat(client-s3): make expect continue header configurable --- .../src/index.spec.ts | 15 +++- .../middleware-expect-continue/src/index.ts | 38 ++++++++-- .../middleware-expect-continue.integ.spec.ts | 69 ++++++++++++++++--- .../src/s3Configuration.spec.ts | 33 +++++++++ .../middleware-sdk-s3/src/s3Configuration.ts | 19 +++++ .../initializeWithMaximalConfiguration.ts | 1 + 6 files changed, 157 insertions(+), 18 deletions(-) diff --git a/packages/middleware-expect-continue/src/index.spec.ts b/packages/middleware-expect-continue/src/index.spec.ts index 3baa0f63a62ce..4c497346c60f0 100644 --- a/packages/middleware-expect-continue/src/index.spec.ts +++ b/packages/middleware-expect-continue/src/index.spec.ts @@ -11,7 +11,10 @@ describe("addExpectContinueMiddleware", () => { }); it("sets the Expect header to 100-continue if there is a request body in node runtime", async () => { - const handler = addExpectContinueMiddleware({ runtime: "node" })(mockNextHandler, {} as any); + const handler = addExpectContinueMiddleware({ runtime: "node", expectContinueHeader: true })( + mockNextHandler, + {} as any + ); await handler({ input: {}, request: new HttpRequest({ @@ -27,7 +30,10 @@ describe("addExpectContinueMiddleware", () => { }); it("does not set the Expect header to 100-continue if there is no request body in node runtime", async () => { - const handler = addExpectContinueMiddleware({ runtime: "node" })(mockNextHandler, {} as any); + const handler = addExpectContinueMiddleware({ runtime: "node", expectContinueHeader: true })( + mockNextHandler, + {} as any + ); await handler({ input: {}, request: new HttpRequest({ @@ -42,7 +48,10 @@ describe("addExpectContinueMiddleware", () => { }); it("does not set the Expect header to 100-continue for browser runtime", async () => { - const handler = addExpectContinueMiddleware({ runtime: "browser" })(mockNextHandler, {} as any); + const handler = addExpectContinueMiddleware({ runtime: "browser", expectContinueHeader: true })( + mockNextHandler, + {} as any + ); await handler({ input: {}, request: new HttpRequest({ diff --git a/packages/middleware-expect-continue/src/index.ts b/packages/middleware-expect-continue/src/index.ts index 33c930cf4dbf2..38a39f46b9e71 100644 --- a/packages/middleware-expect-continue/src/index.ts +++ b/packages/middleware-expect-continue/src/index.ts @@ -1,5 +1,6 @@ import { HttpHandler, HttpRequest } from "@smithy/protocol-http"; -import { +import type { + BodyLengthCalculator, BuildHandler, BuildHandlerArguments, BuildHandlerOptions, @@ -13,20 +14,43 @@ import { interface PreviouslyResolved { runtime: string; requestHandler?: RequestHandler | HttpHandler; + bodyLengthChecker?: BodyLengthCalculator; + expectContinueHeader?: boolean | number; } export function addExpectContinueMiddleware(options: PreviouslyResolved): BuildMiddleware { return (next: BuildHandler): BuildHandler => async (args: BuildHandlerArguments): Promise> => { const { request } = args; - if (HttpRequest.isInstance(request) && request.body && options.runtime === "node") { - if (options.requestHandler?.constructor?.name !== "FetchHttpHandler") { - request.headers = { - ...request.headers, - Expect: "100-continue", - }; + + if ( + options.expectContinueHeader !== false && + HttpRequest.isInstance(request) && + request.body && + options.runtime === "node" && + options.requestHandler?.constructor?.name !== "FetchHttpHandler" + ) { + let sendHeader = true; + if (typeof options.expectContinueHeader === "number") { + try { + const bodyLength = + Number(request.headers?.["content-length"]) ?? options.bodyLengthChecker?.(request.body) ?? Infinity; + sendHeader = bodyLength >= options.expectContinueHeader; + console.log({ + sendHeader, + bodyLength, + threshold: options.expectContinueHeader, + }); + } catch (e) {} + } else { + sendHeader = !!options.expectContinueHeader; + } + + if (sendHeader) { + request.headers.Expect = "100-continue"; } } + return next({ ...args, request, diff --git a/packages/middleware-expect-continue/src/middleware-expect-continue.integ.spec.ts b/packages/middleware-expect-continue/src/middleware-expect-continue.integ.spec.ts index cd0931c60e64d..449447fd1eb71 100644 --- a/packages/middleware-expect-continue/src/middleware-expect-continue.integ.spec.ts +++ b/packages/middleware-expect-continue/src/middleware-expect-continue.integ.spec.ts @@ -5,35 +5,88 @@ import { describe, expect, test as it } from "vitest"; describe("middleware-expect-continue", () => { describe(S3.name, () => { it("should not set expect header if there is no body", async () => { - const client = new S3({ region: "us-west-2" }); - + const client = new S3({ region: "us-west-2", expectContinueHeader: true }); requireRequestsFrom(client).toMatch({ headers: { Expect: /undefined/, }, }); - await client.listBuckets({}); - expect.assertions(1); }); it("should set expect header if there is a body", async () => { - const client = new S3({ region: "us-west-2" }); - + const client = new S3({ region: "us-west-2", expectContinueHeader: 4 }); requireRequestsFrom(client).toMatch({ headers: { Expect: /100-continue/, }, }); - await client.putObject({ Bucket: "b", Key: "k", Body: Buffer.from("abcd"), }); - expect.assertions(1); }); + + describe("should set or omit expect header based on configurations", () => { + it("false", async () => { + const client = new S3({ region: "us-west-2", expectContinueHeader: false }); + requireRequestsFrom(client).toMatch({ + headers: { + Expect: /undefined/, + }, + }); + await client.putObject({ + Bucket: "b", + Key: "k", + Body: Buffer.from("abcd"), + }); + expect.assertions(1); + }); + it("5", async () => { + const client = new S3({ region: "us-west-2", expectContinueHeader: 5 }); + requireRequestsFrom(client).toMatch({ + headers: { + Expect: /undefined/, + }, + }); + await client.putObject({ + Bucket: "b", + Key: "k", + Body: Buffer.from("abcd"), + }); + expect.assertions(1); + }); + it("true", async () => { + const client = new S3({ region: "us-west-2", expectContinueHeader: true }); + requireRequestsFrom(client).toMatch({ + headers: { + Expect: /100-continue/, + }, + }); + await client.putObject({ + Bucket: "b", + Key: "k", + Body: Buffer.from("abcd"), + }); + expect.assertions(1); + }); + it("4", async () => { + const client = new S3({ region: "us-west-2", expectContinueHeader: 4 }); + requireRequestsFrom(client).toMatch({ + headers: { + Expect: /100-continue/, + }, + }); + await client.putObject({ + Bucket: "b", + Key: "k", + Body: Buffer.from("abcd"), + }); + expect.assertions(1); + }); + }); }); }); diff --git a/packages/middleware-sdk-s3/src/s3Configuration.spec.ts b/packages/middleware-sdk-s3/src/s3Configuration.spec.ts index 4465a63fd9629..76e5601c49754 100644 --- a/packages/middleware-sdk-s3/src/s3Configuration.spec.ts +++ b/packages/middleware-sdk-s3/src/s3Configuration.spec.ts @@ -11,4 +11,37 @@ describe(resolveS3Config.name, () => { }) ).toBe(input); }); + + it("accepts bool/num for expectContinueHeader and defaults to 2mb", () => { + expect( + resolveS3Config( + { + expectContinueHeader: 1, + }, + { + session: [() => null, vi.fn()], + } + ).expectContinueHeader + ).toEqual(1); + + expect( + resolveS3Config( + { + expectContinueHeader: false, + }, + { + session: [() => null, vi.fn()], + } + ).expectContinueHeader + ).toEqual(false); + + expect( + resolveS3Config( + {}, + { + session: [() => null, vi.fn()], + } + ).expectContinueHeader + ).toEqual(2 * 1024 * 1024); + }); }); diff --git a/packages/middleware-sdk-s3/src/s3Configuration.ts b/packages/middleware-sdk-s3/src/s3Configuration.ts index 3b3ed7f3b48cf..5759e517a7a8b 100644 --- a/packages/middleware-sdk-s3/src/s3Configuration.ts +++ b/packages/middleware-sdk-s3/src/s3Configuration.ts @@ -35,6 +35,22 @@ export interface S3InputConfig { * Whether to use the bucket name as the endpoint for this client. */ bucketEndpoint?: boolean; + /** + * This field configures the SDK's behavior around setting the `expect: 100-continue` header. + * + * Default: 2_097_152 (2 MB) + * + * When given as a boolean - always send or omit the header. + * When given as a number - minimum byte threshold of the payload before setting the header. + * Unmeasurable payload sizes (streams) will set the header too. + * + * The `expect: 100-continue` header is used to allow the server a chance to validate the PUT request + * headers before the client begins to send the object payload. This avoids wasteful data transmission for a + * request that is rejected. + * + * However, there is a trade-off where the request will take longer to complete. + */ + expectContinueHeader?: boolean | number; } /** @@ -58,6 +74,7 @@ export interface S3ResolvedConfig { followRegionRedirects: boolean; s3ExpressIdentityProvider: S3ExpressIdentityProvider; bucketEndpoint: boolean; + expectContinueHeader: boolean | number; } export const resolveS3Config = ( @@ -76,6 +93,7 @@ export const resolveS3Config = ( followRegionRedirects, s3ExpressIdentityProvider, bucketEndpoint, + expectContinueHeader, } = input; return Object.assign(input, { @@ -93,5 +111,6 @@ export const resolveS3Config = ( ) ), bucketEndpoint: bucketEndpoint ?? false, + expectContinueHeader: expectContinueHeader ?? 2_097_152, }); }; diff --git a/private/aws-client-api-test/src/client-interface-tests/client-s3/impl/initializeWithMaximalConfiguration.ts b/private/aws-client-api-test/src/client-interface-tests/client-s3/impl/initializeWithMaximalConfiguration.ts index 495acb2ef6f7d..432022ab97c30 100644 --- a/private/aws-client-api-test/src/client-interface-tests/client-s3/impl/initializeWithMaximalConfiguration.ts +++ b/private/aws-client-api-test/src/client-interface-tests/client-s3/impl/initializeWithMaximalConfiguration.ts @@ -132,6 +132,7 @@ export const initializeWithMaximalConfiguration = () => { responseChecksumValidation: DEFAULT_RESPONSE_CHECKSUM_VALIDATION, userAgentAppId: "testApp", requestStreamBufferSize: 8 * 1024, + expectContinueHeader: 8 * 1024 * 1024, }; const s3 = new S3Client(config); From 6683e09c6ae9cd528972e6ff036501b27021d034 Mon Sep 17 00:00:00 2001 From: George Fu Date: Wed, 22 Oct 2025 13:31:11 -0400 Subject: [PATCH 2/2] test: update integ tests for expect 100-continue --- .../src/middleware-flexible-checksums.integ.spec.ts | 1 - private/aws-middleware-test/src/middleware-serde.spec.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/middleware-flexible-checksums/src/middleware-flexible-checksums.integ.spec.ts b/packages/middleware-flexible-checksums/src/middleware-flexible-checksums.integ.spec.ts index f27b720a1df43..50d5f73226a83 100644 --- a/packages/middleware-flexible-checksums/src/middleware-flexible-checksums.integ.spec.ts +++ b/packages/middleware-flexible-checksums/src/middleware-flexible-checksums.integ.spec.ts @@ -65,7 +65,6 @@ describe("middleware-flexible-checksums", () => { ...(body.length ? { "content-length": body.length.toString(), - Expect: "100-continue", } : {}), ...(requestChecksumCalculation === RequestChecksumCalculation.WHEN_REQUIRED && diff --git a/private/aws-middleware-test/src/middleware-serde.spec.ts b/private/aws-middleware-test/src/middleware-serde.spec.ts index 58ec7b3777abd..354b48703f006 100644 --- a/private/aws-middleware-test/src/middleware-serde.spec.ts +++ b/private/aws-middleware-test/src/middleware-serde.spec.ts @@ -25,7 +25,6 @@ describe("middleware-serde", () => { "content-type": "application/xml", "x-amz-acl": "private", "content-length": "509", - Expect: "100-continue", "x-amz-checksum-crc32": "XnKFaw==", host: "s3.us-west-2.amazonaws.com", "x-amz-content-sha256": "c0a89780e1aac5dfa17604e9e25616e7babba0b655db189be49b4c352543bb22",