Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Accept Provider in CompressionInputConfig #1132

Merged
merged 7 commits into from
Jan 4, 2024
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
5 changes: 5 additions & 0 deletions .changeset/bright-elephants-walk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@smithy/middleware-compression": patch
---

Accept Provider in CompressionInputConfig
1 change: 1 addition & 0 deletions packages/middleware-compression/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"@smithy/node-config-provider": "workspace:^",
"@smithy/types": "workspace:^",
"@smithy/util-config-provider": "workspace:^",
"@smithy/util-middleware": "workspace:^",
"fflate": "0.8.1",
"tslib": "^2.5.0"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ describe(compressionMiddleware.name, () => {
const mockBody = "body";
const mockConfig = {
bodyLengthChecker: jest.fn().mockReturnValue(mockBody.length),
disableRequestCompression: false,
requestMinCompressionSizeBytes: 0,
disableRequestCompression: async () => false,
requestMinCompressionSizeBytes: async () => 0,
};
const mockMiddlewareConfig = {
encodings: [CompressionAlgorithm.GZIP],
Expand All @@ -32,20 +32,20 @@ describe(compressionMiddleware.name, () => {

it("skips compression if it's not an HttpRequest", async () => {
const { isInstance } = HttpRequest;
(isInstance as unknown as jest.Mock).mockReturnValue(false);
((isInstance as unknown) as jest.Mock).mockReturnValue(false);
await compressionMiddleware(mockConfig, mockMiddlewareConfig)(mockNext, mockContext)({ ...mockArgs } as any);
expect(mockNext).toHaveBeenCalledWith(mockArgs);
});

describe("HttpRequest", () => {
beforeEach(() => {
const { isInstance } = HttpRequest;
(isInstance as unknown as jest.Mock).mockReturnValue(true);
((isInstance as unknown) as jest.Mock).mockReturnValue(true);
(isStreaming as jest.Mock).mockReturnValue(false);
});

it("skips compression if disabled", async () => {
await compressionMiddleware({ ...mockConfig, disableRequestCompression: true }, mockMiddlewareConfig)(
await compressionMiddleware({ ...mockConfig, disableRequestCompression: async () => true }, mockMiddlewareConfig)(
mockNext,
mockContext
)({ ...mockArgs } as any);
Expand Down Expand Up @@ -107,7 +107,7 @@ describe(compressionMiddleware.name, () => {
describe("not streaming", () => {
it("skips compression if body is smaller than min size", async () => {
await compressionMiddleware(
{ ...mockConfig, requestMinCompressionSizeBytes: mockBody.length + 1 },
{ ...mockConfig, requestMinCompressionSizeBytes: async () => mockBody.length + 1 },
mockMiddlewareConfig
)(
mockNext,
Expand Down
112 changes: 60 additions & 52 deletions packages/middleware-compression/src/compressionMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {

import { compressStream } from "./compressStream";
import { compressString } from "./compressString";
import { CompressionResolvedConfig } from "./configurations";
import { CompressionPreviouslyResolved, CompressionResolvedConfig } from "./configurations";
import { CLIENT_SUPPORTED_ALGORITHMS, CompressionAlgorithm } from "./constants";
import { isStreaming } from "./isStreaming";

Expand All @@ -34,67 +34,75 @@ export interface CompressionMiddlewareConfig {
/**
* @internal
*/
export const compressionMiddleware =
(config: CompressionResolvedConfig, middlewareConfig: CompressionMiddlewareConfig): BuildMiddleware<any, any> =>
<Output extends MetadataBearer>(next: BuildHandler<any, Output>): BuildHandler<any, Output> =>
async (args: BuildHandlerArguments<any>): Promise<BuildHandlerOutput<Output>> => {
if (!HttpRequest.isInstance(args.request) || config.disableRequestCompression) {
return next(args);
}
export const compressionMiddleware = (
config: CompressionResolvedConfig & CompressionPreviouslyResolved,
middlewareConfig: CompressionMiddlewareConfig
): BuildMiddleware<any, any> => <Output extends MetadataBearer>(
next: BuildHandler<any, Output>
): BuildHandler<any, Output> => async (args: BuildHandlerArguments<any>): Promise<BuildHandlerOutput<Output>> => {
if (!HttpRequest.isInstance(args.request)) {
return next(args);
}

const disableRequestCompression = await config.disableRequestCompression();
if (disableRequestCompression) {
return next(args);
}

const { request } = args;
const { body, headers } = request;
const { encodings, streamRequiresLength } = middlewareConfig;
const { request } = args;
const { body, headers } = request;
const { encodings, streamRequiresLength } = middlewareConfig;

let updatedBody = body;
let updatedHeaders = headers;
let updatedBody = body;
let updatedHeaders = headers;

for (const algorithm of encodings) {
if (CLIENT_SUPPORTED_ALGORITHMS.includes(algorithm as CompressionAlgorithm)) {
let isRequestCompressed = false;
if (isStreaming(body)) {
if (!streamRequiresLength) {
updatedBody = await compressStream(body);
isRequestCompressed = true;
} else {
// Invalid case. We should never get here.
throw new Error("Compression is not supported for streaming blobs that require a length.");
}
for (const algorithm of encodings) {
if (CLIENT_SUPPORTED_ALGORITHMS.includes(algorithm as CompressionAlgorithm)) {
let isRequestCompressed = false;
if (isStreaming(body)) {
if (!streamRequiresLength) {
updatedBody = await compressStream(body);
isRequestCompressed = true;
} else {
const bodyLength = config.bodyLengthChecker(body);
if (bodyLength && bodyLength >= config.requestMinCompressionSizeBytes) {
updatedBody = await compressString(body);
isRequestCompressed = true;
}
// Invalid case. We should never get here.
throw new Error("Compression is not supported for streaming blobs that require a length.");
}
} else {
const bodyLength = config.bodyLengthChecker(body);
const requestMinCompressionSizeBytes = await config.requestMinCompressionSizeBytes();
if (bodyLength && bodyLength >= requestMinCompressionSizeBytes) {
updatedBody = await compressString(body);
isRequestCompressed = true;
}
}

if (isRequestCompressed) {
// Either append to the header if it already exists, else set it
if (headers["Content-Encoding"]) {
updatedHeaders = {
...headers,
"Content-Encoding": `${headers["Content-Encoding"]},${algorithm}`,
};
} else {
updatedHeaders = { ...headers, "Content-Encoding": algorithm };
}

// We've matched on one supported algorithm in the
// priority-ordered list, so we're finished.
break;
if (isRequestCompressed) {
// Either append to the header if it already exists, else set it
if (headers["Content-Encoding"]) {
updatedHeaders = {
...headers,
"Content-Encoding": `${headers["Content-Encoding"]},${algorithm}`,
};
} else {
updatedHeaders = { ...headers, "Content-Encoding": algorithm };
}

// We've matched on one supported algorithm in the
// priority-ordered list, so we're finished.
break;
}
}
}

return next({
...args,
request: {
...request,
body: updatedBody,
headers: updatedHeaders,
},
});
};
return next({
...args,
request: {
...request,
body: updatedBody,
headers: updatedHeaders,
},
});
};

export const compressionMiddlewareOptions: BuildHandlerOptions & AbsoluteLocation = {
name: "compressionMiddleware",
Expand Down
33 changes: 24 additions & 9 deletions packages/middleware-compression/src/configurations.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,42 @@
import { BodyLengthCalculator } from "@smithy/types";
import { BodyLengthCalculator, Provider } from "@smithy/types";

/**
* @public
*/
export interface CompressionInputConfig {
/**
* A function that can calculate the length of a body.
*/
bodyLengthChecker: BodyLengthCalculator;

/**
* Whether to disable request compression.
*/
disableRequestCompression: boolean;
disableRequestCompression: boolean | Provider<boolean>;

/**
* The minimum size in bytes that a request body should be to trigger compression.
* The value must be a non-negative integer value between 0 and 10485760 bytes inclusive.
*/
requestMinCompressionSizeBytes: number;
requestMinCompressionSizeBytes: number | Provider<number>;
}

/**
* @internal
*/
export interface CompressionPreviouslyResolved {
/**
* A function that can calculate the length of a body.
*/
bodyLengthChecker: BodyLengthCalculator;
}

/**
* @internal
*/
export interface CompressionResolvedConfig extends CompressionInputConfig {}
export interface CompressionResolvedConfig {
/**
* Resolved value for input config {@link CompressionInputConfig.disableRequestCompression}
*/
disableRequestCompression: Provider<boolean>;

/**
* Resolved value for input config {@link CompressionInputConfig.requestMinCompressionSizeBytes}
*/
requestMinCompressionSizeBytes: Provider<number>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ jest.mock("./compressionMiddleware");
describe(getCompressionPlugin.name, () => {
const config = {
bodyLengthChecker: jest.fn(),
disableRequestCompression: false,
requestMinCompressionSizeBytes: 0,
disableRequestCompression: async () => false,
requestMinCompressionSizeBytes: async () => 0,
};
const middlewareConfig = { encodings: [] };

Expand Down
4 changes: 2 additions & 2 deletions packages/middleware-compression/src/getCompressionPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import {
CompressionMiddlewareConfig,
compressionMiddlewareOptions,
} from "./compressionMiddleware";
import { CompressionResolvedConfig } from "./configurations";
import { CompressionPreviouslyResolved, CompressionResolvedConfig } from "./configurations";

/**
* @internal
*/
export const getCompressionPlugin = (
config: CompressionResolvedConfig,
config: CompressionResolvedConfig & CompressionPreviouslyResolved,
middlewareConfig: CompressionMiddlewareConfig
): Pluggable<any, any> => ({
applyToStack: (clientStack) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,39 +6,41 @@ describe(resolveCompressionConfig.name, () => {
disableRequestCompression: false,
requestMinCompressionSizeBytes: 0,
};
it("should throw an error if requestMinCompressionSizeBytes is less than 0", () => {

it("should throw an error if requestMinCompressionSizeBytes is less than 0", async () => {
const requestMinCompressionSizeBytes = -1;
expect(() => {
resolveCompressionConfig({ ...mockConfig, requestMinCompressionSizeBytes });
}).toThrow(
const resolvedConfig = resolveCompressionConfig({ ...mockConfig, requestMinCompressionSizeBytes });
await expect(resolvedConfig.requestMinCompressionSizeBytes()).rejects.toThrow(
new RangeError(
"The value for requestMinCompressionSizeBytes must be between 0 and 10485760 inclusive. " +
`The provided value ${requestMinCompressionSizeBytes} is outside this range."`
)
);
});

it("should throw an error if requestMinCompressionSizeBytes is greater than 10485760", () => {
it("should throw an error if requestMinCompressionSizeBytes is greater than 10485760", async () => {
const requestMinCompressionSizeBytes = 10485761;
expect(() => {
resolveCompressionConfig({ ...mockConfig, requestMinCompressionSizeBytes });
}).toThrow(
const resolvedConfig = resolveCompressionConfig({ ...mockConfig, requestMinCompressionSizeBytes });
await expect(resolvedConfig.requestMinCompressionSizeBytes()).rejects.toThrow(
new RangeError(
"The value for requestMinCompressionSizeBytes must be between 0 and 10485760 inclusive. " +
`The provided value ${requestMinCompressionSizeBytes} is outside this range."`
)
);
});

it.each([0, 10240, 10485760])("returns requestMinCompressionSizeBytes value %s", (requestMinCompressionSizeBytes) => {
const inputConfig = { ...mockConfig, requestMinCompressionSizeBytes };
const resolvedConfig = resolveCompressionConfig(inputConfig);
expect(inputConfig).toEqual(resolvedConfig);
});
it.each([0, 10240, 10485760])(
"returns requestMinCompressionSizeBytes value %s",
async (requestMinCompressionSizeBytes) => {
const inputConfig = { ...mockConfig, requestMinCompressionSizeBytes };
const resolvedConfig = resolveCompressionConfig(inputConfig);
await expect(resolvedConfig.requestMinCompressionSizeBytes()).resolves.toEqual(requestMinCompressionSizeBytes);
}
);

it.each([false, true])("returns disableRequestCompression value %s", (disableRequestCompression) => {
it.each([false, true])("returns disableRequestCompression value %s", async (disableRequestCompression) => {
const inputConfig = { ...mockConfig, disableRequestCompression };
const resolvedConfig = resolveCompressionConfig(inputConfig);
expect(inputConfig).toEqual(resolvedConfig);
await expect(resolvedConfig.disableRequestCompression()).resolves.toEqual(disableRequestCompression);
});
});
30 changes: 18 additions & 12 deletions packages/middleware-compression/src/resolveCompressionConfig.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
import { normalizeProvider } from "@smithy/util-middleware";
syall marked this conversation as resolved.
Show resolved Hide resolved

import { CompressionInputConfig, CompressionResolvedConfig } from "./configurations";

/**
* @internal
*/
export const resolveCompressionConfig = <T>(input: T & CompressionInputConfig): T & CompressionResolvedConfig => {
const { requestMinCompressionSizeBytes } = input;
export const resolveCompressionConfig = <T>(input: T & CompressionInputConfig): T & CompressionResolvedConfig => ({
...input,
disableRequestCompression: normalizeProvider(input.disableRequestCompression),
requestMinCompressionSizeBytes: async () => {
const requestMinCompressionSizeBytes = await normalizeProvider(input.requestMinCompressionSizeBytes)();

// The requestMinCompressionSizeBytes should be less than the upper limit for API Gateway
// https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-openapi-minimum-compression-size.html
if (requestMinCompressionSizeBytes < 0 || requestMinCompressionSizeBytes > 10485760) {
throw new RangeError(
"The value for requestMinCompressionSizeBytes must be between 0 and 10485760 inclusive. " +
`The provided value ${requestMinCompressionSizeBytes} is outside this range."`
);
}
// The requestMinCompressionSizeBytes should be less than the upper limit for API Gateway
// https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-openapi-minimum-compression-size.html
if (requestMinCompressionSizeBytes < 0 || requestMinCompressionSizeBytes > 10485760) {
throw new RangeError(
"The value for requestMinCompressionSizeBytes must be between 0 and 10485760 inclusive. " +
`The provided value ${requestMinCompressionSizeBytes} is outside this range."`
);
}

return input;
};
return requestMinCompressionSizeBytes;
},
});
1 change: 1 addition & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2085,6 +2085,7 @@ __metadata:
"@smithy/node-config-provider": "workspace:^"
"@smithy/types": "workspace:^"
"@smithy/util-config-provider": "workspace:^"
"@smithy/util-middleware": "workspace:^"
"@tsconfig/recommended": 1.0.1
concurrently: 7.0.0
downlevel-dts: 0.10.1
Expand Down
Loading