Skip to content

feat(middleware-compression): add middleware for request compression #5617

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

Closed
wants to merge 1 commit into from
Closed
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
24 changes: 24 additions & 0 deletions packages/middleware-compression/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "middleware-compression",
"version": "1.0.0",
"description": "Package that implements the compression middleware for request bodies. ",
"main": "index.js",
"scripts": {
"test": "jest"
},
"repository": {
"type": "git",
"url": "git+https://github.com/aws/aws-sdk-js-v3.git"
},
"keywords": [
"middleware",
"compression",
"gzip"
],
"author": "AWS SDK for JavaScript Team",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/aws/aws-sdk-js-v3/issues"
},
"homepage": "https://github.com/aws/aws-sdk-js-v3#readme"
}
54 changes: 54 additions & 0 deletions packages/middleware-compression/src/compress.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { compress } from './compress';
import * as zlib from 'zlib';
import { promisify } from 'util';

jest.mock('zlib', () => ({
gzip: jest.fn((buffer, options, callback) => callback(null, 'compressed data')),
}));

jest.mock('util', () => ({
promisify: jest.fn(),
}));

describe('compress', () => {
const mockZlibGzip = zlib.gzip as jest.MockedFunction<typeof zlib.gzip>;
const mockZlibDeflate = zlib.deflate as jest.MockedFunction<typeof zlib.deflate>;
const mockPromisify = promisify as jest.MockedFunction<typeof promisify>;

beforeEach(() => {
jest.clearAllMocks();
});

it('should compress data with gzip', async () => {
const data = 'test data';
const compressedData = Buffer.from('compressed data');
mockZlibGzip.mockImplementation((buffer, callback) => callback(null, compressedData));
mockPromisify.mockImplementation(() => Promise.resolve(compressedData));

const result = await compress(data, 'gzip');

expect(mockZlibGzip).toHaveBeenCalledWith(Buffer.from(data), expect.any(Function));
expect(result).toEqual(compressedData);
});

it('should throw an error for unsupported compression algorithm', async () => {
const data = 'test data';

await expect(compress(data, 'unsupported' as any)).rejects.toThrow('Unsupported compression algorithm');
});

it('should throw an error if body cannot be buffered', async () => {
const data = {};

await expect(compress(data as any, 'gzip')).rejects.toThrow('Body cannot be buffered');
});

it('should throw an error if compression fails', async () => {
const data = 'test data';
const error = new Error('Compression failed');
mockZlibGzip.mockImplementation((buffer, callback) => callback(error));
mockPromisify.mockImplementation(() => Promise.reject(error));

await expect(compress(data, 'gzip')).rejects.toThrow('Compression failed');
});
});
23 changes: 23 additions & 0 deletions packages/middleware-compression/src/compress.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import zlib from 'zlib';
import { promisify } from 'util';
import { CompressionAlgorithm } from './constants';

// zlib.gzip( buffer, options, callback )

// reject stream if it goes into compress function
export const compress = async (body: Uint8Array, algorithm: CompressionAlgorithm): Promise<Uint8Array> => {
// switch-case for algorithms: default -> unsupported
if (algorithm !== "gzip") {
throw new Error('Unsupported compression algorithm');
}
try {
const compressedData = zlib.gzipSync(body);
return compressedData as Uint8Array; // should it be returned as buffer?
} catch (err) {
throw new Error('Compression failed: ' + err.message);
}
};

// type should be --> import type readable from stream and use readable as the type
// export const compressStream = (body: NodeJS.ReadableStream, algorithm: string): NodeJS.ReadableStream => {

54 changes: 54 additions & 0 deletions packages/middleware-compression/src/compressionMiddleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { HttpRequest } from "@smithy/protocol-http";
import { BuildHandler, BuildHandlerArguments, BuildHandlerOptions, BuildHandlerOutput, BuildMiddleware, HandlerExecutionContext, MetadataBearer, } from "@smithy/types";
import { CompressionResolvedConfig } from "./configuration"
import { isStreaming } from "./utils";
import { CLIENT_SUPPORTED_ALGORITHMS as supportedAlgorithms } from "./types";
import { compress, compressStream } from "./compress";

/**
* @internal
*/
export const compressionMiddleware = (config: CompressionResolvedConfig):
BuildMiddleware<any, any> => {
return <Output extends MetadataBearer>(next: BuildHandler<any, Output>,
context: HandlerExecutionContext):
BuildHandler<any, Output> =>
async (args: BuildHandlerArguments<any>):
Promise<BuildHandlerOutput<Output>> => {

if (!HttpRequest.isInstance(args.request) || config.disableRequestCompression) {
return next(args);
}

const { request } = args;
const { body, headers } = request;

for (const algorithm of supportedAlgorithms) {
// have to check for streaming length trait and @requiredLength trait; probably to be done in codegen part and not check in middleware
if (isStreaming(body) && !isStreamingLengthRequired(body)) {
// if isStreaming results in Transfer-Encoding: Chunked
request.body = compressStream(body, algorithm);
// body.length checks --> check for util body length in smithy pkg
} else if (!isStreaming(body) && body.length >= config.minCompressionSizeInBytes) {
request.body = await compress(body, algorithm);
}
if (headers["Content-Encoding"]) {
headers["Content-Encoding"] += `,${algorithm}`;
} else {
headers["Content-Encoding"] = algorithm;
}
break;
}

return next({ ...args, request });
};

}


export const compressionMiddlewareOptions: BuildHandlerOptions = {
name: "compressionMiddleware",
step: "build",
tags: ["REQUEST_BODY_COMPRESSION", "GZIP"],
override: true,
};
35 changes: 35 additions & 0 deletions packages/middleware-compression/src/configuration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@

/**
* @public
*
*/
export interface CompressionInputConfig {
/**
* Whether to disable request compression.
*/
disableRequestCompression?: boolean;
/**
* Minimum size in bytes for compression. The value must be a non-negative integer value between 0 and 10485760 bytes inclusive.
*/
minCompressionSizeInBytes?: number;
}

export interface CompressionResolvedConfig {
disableRequestCompression: boolean;
minCompressionSizeInBytes: number;
}


export const resolveCompressionConfig = <T>(input: T & CompressionInputConfig): T & CompressionResolvedConfig => {
const minCompressionSizeInBytes = input.minCompressionSizeInBytes ?? 10240;
const maxCompressionSizeInBytes = 10485760;
// minCompressionSizeInBytes explanation
if (minCompressionSizeInBytes < 0 || minCompressionSizeInBytes > maxCompressionSizeInBytes) {
throw new Error('minCompressionSizeInBytes must be between 0 and 10485760 bytes inclusive');
}
return {
...input,
disableRequestCompression: input.disableRequestCompression ?? false,
minCompressionSizeInBytes,
};
};
4 changes: 4 additions & 0 deletions packages/middleware-compression/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/**
* Compression algorithms supported by the SDK.
*/
export type CompressionAlgorithm = "gzip"
1 change: 1 addition & 0 deletions packages/middleware-compression/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./compressionMiddleware";
11 changes: 11 additions & 0 deletions packages/middleware-compression/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { CompressionAlgorithm } from "./constants";

export const CLIENT_SUPPORTED_ALGORITHMS: CompressionAlgorithm[] = [
'gzip',
// add more supported compression algorithms here
];

export const PRIORITY_ORDER_ALGORITHMS: CompressionAlgorithm[] = [
'gzip',
// add more supported compression algorithms here in the order of their priority
];
7 changes: 7 additions & 0 deletions packages/middleware-compression/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { isArrayBuffer } from "@smithy/is-array-buffer";

/**
* Returns true if the given value is a streaming response.
*/
export const isStreaming = (body: unknown) =>
body !== undefined && typeof body !== "string" && !ArrayBuffer.isView(body) && !isArrayBuffer(body);
9 changes: 9 additions & 0 deletions packages/middleware-compression/tsconfig.cjs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"compilerOptions": {
"baseUrl": ".",
"outDir": "dist-cjs",
"rootDir": "src"
},
"extends": "../../tsconfig.cjs.json",
"include": ["src/"]
}