Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions packages/middleware-expect-continue/src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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({
Expand All @@ -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({
Expand Down
38 changes: 31 additions & 7 deletions packages/middleware-expect-continue/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { HttpHandler, HttpRequest } from "@smithy/protocol-http";
import {
import type {
BodyLengthCalculator,
BuildHandler,
BuildHandlerArguments,
BuildHandlerOptions,
Expand All @@ -13,20 +14,43 @@ import {
interface PreviouslyResolved {
runtime: string;
requestHandler?: RequestHandler<any, any, any> | HttpHandler<any>;
bodyLengthChecker?: BodyLengthCalculator;
expectContinueHeader?: boolean | number;
}

export function addExpectContinueMiddleware(options: PreviouslyResolved): BuildMiddleware<any, any> {
return <Output extends MetadataBearer>(next: BuildHandler<any, Output>): BuildHandler<any, Output> =>
async (args: BuildHandlerArguments<any>): Promise<BuildHandlerOutput<Output>> => {
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ describe("middleware-flexible-checksums", () => {
...(body.length
? {
"content-length": body.length.toString(),
Expect: "100-continue",
}
: {}),
...(requestChecksumCalculation === RequestChecksumCalculation.WHEN_REQUIRED &&
Expand Down
33 changes: 33 additions & 0 deletions packages/middleware-sdk-s3/src/s3Configuration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
19 changes: 19 additions & 0 deletions packages/middleware-sdk-s3/src/s3Configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand All @@ -58,6 +74,7 @@ export interface S3ResolvedConfig {
followRegionRedirects: boolean;
s3ExpressIdentityProvider: S3ExpressIdentityProvider;
bucketEndpoint: boolean;
expectContinueHeader: boolean | number;
}

export const resolveS3Config = <T>(
Expand All @@ -76,6 +93,7 @@ export const resolveS3Config = <T>(
followRegionRedirects,
s3ExpressIdentityProvider,
bucketEndpoint,
expectContinueHeader,
} = input;

return Object.assign(input, {
Expand All @@ -93,5 +111,6 @@ export const resolveS3Config = <T>(
)
),
bucketEndpoint: bucketEndpoint ?? false,
expectContinueHeader: expectContinueHeader ?? 2_097_152,
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 0 additions & 1 deletion private/aws-middleware-test/src/middleware-serde.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading