Skip to content

Commit 4f071ce

Browse files
committed
Refactored the logic to remove checks for content types
1 parent 87058a2 commit 4f071ce

File tree

5 files changed

+81
-102
lines changed

5 files changed

+81
-102
lines changed

packages/event-handler/src/rest/constants.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -88,20 +88,14 @@ export const SAFE_CHARS = "-._~()'!*:@,;=+&$";
8888

8989
export const UNSAFE_CHARS = '%<> \\[\\]{}|^';
9090

91-
/**
92-
* Match for compressible content type.
93-
*/
94-
export const COMPRESSIBLE_CONTENT_TYPE_REGEX = {
95-
COMMON: /^\s*application\/json(?:[;\s]|$)/i,
96-
OCCASIONAL:
97-
/^\s*(?:text\/(?!event-stream(?:[;\s]|$))[^;\s]+|application\/(?:xml|javascript)|[^;\s]+?\+(?:json|text|xml|yaml))(?:[;\s]|$)/i,
98-
RARE: /^\s*(?:application\/(?:xml-dtd|ecmascript|dart|postscript|rtf|tar|toml|vnd\.dart|vnd\.ms-fontobject|vnd\.ms-opentype|wasm|x-httpd-php|x-javascript|x-ns-proxy-autoconfig|x-sh|x-tar|x-virtualbox-hdd|x-virtualbox-ova|x-virtualbox-ovf|x-virtualbox-vbox|x-virtualbox-vdi|x-virtualbox-vhd|x-virtualbox-vmdk|x-www-form-urlencoded)|font\/(?:otf|ttf)|image\/(?:bmp|vnd\.adobe\.photoshop|vnd\.microsoft\.icon|vnd\.ms-dds|x-icon|x-ms-bmp)|message\/rfc822|model\/gltf-binary|x-shader\/x-fragment|x-shader\/x-vertex)(?:[;\s]|$)/i,
99-
};
91+
export const DEFAULT_COMPRESSION_RESPONSE_THRESHOLD = 1024;
10092

10193
export const CACHE_CONTROL_NO_TRANSFORM_REGEX =
10294
/(?:^|,)\s*?no-transform\s*?(?:,|$)/i;
10395

10496
export const COMPRESSION_ENCODING_TYPES = {
10597
GZIP: 'gzip',
10698
DEFLATE: 'deflate',
99+
IDENTITY: 'identity',
100+
ANY: '*',
107101
} as const;

packages/event-handler/src/rest/converters.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
2-
import type { HandlerResponse } from '../types/rest.js';
2+
import type { CompressionOptions, HandlerResponse } from '../types/rest.js';
3+
import { COMPRESSION_ENCODING_TYPES } from './constants.js';
34
import { isAPIGatewayProxyResult } from './utils.js';
45

56
/**
@@ -89,11 +90,35 @@ export const webResponseToProxyResult = async (
8990
}
9091
}
9192

93+
// Check if response contains compressed/binary content
94+
const contentEncoding = response.headers.get(
95+
'content-encoding'
96+
) as CompressionOptions['encoding'];
97+
let body: string;
98+
let isBase64Encoded = false;
99+
100+
if (
101+
contentEncoding &&
102+
[
103+
COMPRESSION_ENCODING_TYPES.GZIP,
104+
COMPRESSION_ENCODING_TYPES.DEFLATE,
105+
].includes(contentEncoding)
106+
) {
107+
// For compressed content, get as buffer and encode to base64
108+
const buffer = await response.arrayBuffer();
109+
body = Buffer.from(buffer).toString('base64');
110+
isBase64Encoded = true;
111+
} else {
112+
// For text content, use text()
113+
body = await response.text();
114+
isBase64Encoded = false;
115+
}
116+
92117
const result: APIGatewayProxyResult = {
93118
statusCode: response.status,
94119
headers,
95-
body: await response.text(),
96-
isBase64Encoded: false,
120+
body,
121+
isBase64Encoded,
97122
};
98123

99124
if (Object.keys(multiValueHeaders).length > 0) {

packages/event-handler/src/rest/middleware/compress.ts

Lines changed: 42 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import type { Middleware } from '../../types/index.js';
22
import type { CompressionOptions } from '../../types/rest.js';
33
import {
44
CACHE_CONTROL_NO_TRANSFORM_REGEX,
5-
COMPRESSIBLE_CONTENT_TYPE_REGEX,
65
COMPRESSION_ENCODING_TYPES,
6+
DEFAULT_COMPRESSION_RESPONSE_THRESHOLD,
77
} from '../constants.js';
88

99
/**
@@ -25,8 +25,8 @@ import {
2525
*
2626
* @example
2727
* ```typescript
28-
* import { Router } from '@aws-lambda-powertools/event-handler';
29-
* import { compress } from '@aws-lambda-powertools/event-handler/rest/middleware';
28+
* import { Router } from '@aws-lambda-powertools/event-handler/experimental-rest';
29+
* import { compress } from '@aws-lambda-powertools/event-handler/experimental-rest/middleware';
3030
*
3131
* const app = new Router();
3232
*
@@ -41,8 +41,8 @@ import {
4141
*
4242
* @example
4343
* ```typescript
44-
* import { Router } from '@aws-lambda-powertools/event-handler';
45-
* import { compress } from '@aws-lambda-powertools/event-handler/rest/middleware';
44+
* import { Router } from '@aws-lambda-powertools/event-handler/experimental-rest';
45+
* import { compress } from '@aws-lambda-powertools/event-handler/experimental-rest/middleware';
4646
*
4747
* const app = new Router();
4848
*
@@ -62,59 +62,59 @@ import {
6262
*/
6363

6464
const compress = (options?: CompressionOptions): Middleware => {
65-
const threshold = options?.threshold ?? 1024;
65+
const preferredEncoding =
66+
options?.encoding ?? COMPRESSION_ENCODING_TYPES.GZIP;
67+
const threshold =
68+
options?.threshold ?? DEFAULT_COMPRESSION_RESPONSE_THRESHOLD;
6669

6770
return async (_, reqCtx, next) => {
6871
await next();
6972

70-
const contentLength = reqCtx.res.headers.get('content-length');
71-
const isEncodedOrChunked =
72-
reqCtx.res.headers.has('content-encoding') ||
73-
reqCtx.res.headers.has('transfer-encoding');
74-
75-
// Check if response should be compressed
7673
if (
77-
isEncodedOrChunked ||
78-
reqCtx.request.method === 'HEAD' ||
79-
(contentLength && Number(contentLength) < threshold) ||
80-
!shouldCompress(reqCtx.res) ||
81-
!shouldTransform(reqCtx.res) ||
82-
!reqCtx.res.body
74+
!shouldCompress(reqCtx.request, reqCtx.res, preferredEncoding, threshold)
8375
) {
8476
return;
8577
}
8678

87-
const acceptedEncoding = reqCtx.request.headers.get('accept-encoding');
88-
const encoding =
89-
options?.encoding ??
90-
Object.values(COMPRESSION_ENCODING_TYPES).find((encoding) =>
91-
acceptedEncoding?.includes(encoding)
92-
) ??
93-
COMPRESSION_ENCODING_TYPES.GZIP;
94-
9579
// Compress the response
96-
const stream = new CompressionStream(encoding);
80+
const stream = new CompressionStream(preferredEncoding);
9781
reqCtx.res = new Response(reqCtx.res.body.pipeThrough(stream), reqCtx.res);
9882
reqCtx.res.headers.delete('content-length');
99-
reqCtx.res.headers.set('content-encoding', encoding);
83+
reqCtx.res.headers.set('content-encoding', preferredEncoding);
10084
};
10185
};
10286

103-
const shouldCompress = (res: Response) => {
104-
const type = res.headers.get('content-type');
105-
return (
106-
type &&
107-
(COMPRESSIBLE_CONTENT_TYPE_REGEX.COMMON.test(type) ||
108-
COMPRESSIBLE_CONTENT_TYPE_REGEX.OCCASIONAL.test(type) ||
109-
COMPRESSIBLE_CONTENT_TYPE_REGEX.RARE.test(type))
110-
);
111-
};
87+
const shouldCompress = (
88+
request: Request,
89+
response: Response,
90+
preferredEncoding: NonNullable<CompressionOptions['encoding']>,
91+
threshold: NonNullable<CompressionOptions['threshold']>
92+
): response is Response & { body: NonNullable<Response['body']> } => {
93+
const acceptedEncoding =
94+
request.headers.get('accept-encoding') ?? COMPRESSION_ENCODING_TYPES.ANY;
95+
const contentLength = response.headers.get('content-length');
96+
const cacheControl = response.headers.get('cache-control');
97+
98+
const isEncodedOrChunked =
99+
response.headers.has('content-encoding') ||
100+
response.headers.has('transfer-encoding');
101+
102+
const shouldEncode =
103+
!acceptedEncoding.includes(COMPRESSION_ENCODING_TYPES.IDENTITY) &&
104+
(acceptedEncoding.includes(preferredEncoding) ||
105+
acceptedEncoding.includes(COMPRESSION_ENCODING_TYPES.ANY));
112106

113-
const shouldTransform = (res: Response) => {
114-
const cacheControl = res.headers.get('cache-control');
115-
// Don't compress for Cache-Control: no-transform
116-
// https://tools.ietf.org/html/rfc7234#section-5.2.2.4
117-
return !cacheControl || !CACHE_CONTROL_NO_TRANSFORM_REGEX.test(cacheControl);
107+
if (
108+
!shouldEncode ||
109+
isEncodedOrChunked ||
110+
request.method === 'HEAD' ||
111+
(contentLength && Number(contentLength) < threshold) ||
112+
(cacheControl && CACHE_CONTROL_NO_TRANSFORM_REGEX.test(cacheControl)) ||
113+
!response.body
114+
) {
115+
return false;
116+
}
117+
return true;
118118
};
119119

120120
export { compress };

packages/event-handler/src/types/rest.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ type ValidationResult = {
116116
};
117117

118118
type CompressionOptions = {
119-
encoding?: (typeof COMPRESSION_ENCODING_TYPES)[keyof typeof COMPRESSION_ENCODING_TYPES];
119+
encoding?: 'gzip' | 'deflate';
120120
threshold?: number;
121121
};
122122

packages/event-handler/tests/unit/rest/middleware/compress.test.ts

Lines changed: 7 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { gzipSync } from 'node:zlib';
12
import context from '@aws-lambda-powertools/testing-utils/context';
23
import { Router } from 'src/rest/Router.js';
34
import { beforeEach, describe, expect, it } from 'vitest';
@@ -31,6 +32,9 @@ describe('Compress Middleware', () => {
3132
// Assess
3233
expect(result.headers?.['content-encoding']).toBe('gzip');
3334
expect(result.headers?.['content-length']).toBeUndefined();
35+
expect(result.body).toEqual(
36+
gzipSync(JSON.stringify(body)).toString('base64')
37+
);
3438
});
3539

3640
it('skips compression when content is below threshold', async () => {
@@ -98,50 +102,6 @@ describe('Compress Middleware', () => {
98102
expect(result.headers?.['content-encoding']).toEqual('gzip');
99103
});
100104

101-
it.each([
102-
'image/jpeg',
103-
'image/png',
104-
'image/gif',
105-
'audio/mpeg',
106-
'audio/mp4',
107-
'audio/ogg',
108-
'video/mp4',
109-
'video/mpeg',
110-
'video/webm',
111-
'application/zip',
112-
'application/gzip',
113-
'application/x-gzip',
114-
'application/octet-stream',
115-
'application/pdf',
116-
'application/msword',
117-
'text/event-stream',
118-
])(
119-
'skips compression for non-compressible content types',
120-
async (contentType) => {
121-
// Prepare
122-
const application = new Router();
123-
application.get(
124-
'/test',
125-
[
126-
compress(),
127-
createSettingHeadersMiddleware({
128-
'content-length': '2000',
129-
'content-type': contentType,
130-
}),
131-
],
132-
async () => {
133-
return body;
134-
}
135-
);
136-
137-
// Act
138-
const result = await application.resolve(event, context);
139-
140-
// Assess
141-
expect(result.headers?.['content-encoding']).toBeUndefined();
142-
}
143-
);
144-
145105
it('skips compression when cache-control no-transform is set', async () => {
146106
// Prepare
147107
const application = new Router();
@@ -191,10 +151,10 @@ describe('Compress Middleware', () => {
191151
expect(result.headers?.['content-encoding']).toBe('deflate');
192152
});
193153

194-
it('infers encoding from Accept-Encoding header', async () => {
154+
it('does not compress if Accept-Encoding is set to identity', async () => {
195155
// Prepare
196156
const deflateCompressionEvent = createTestEvent('/test', 'GET', {
197-
'Accept-Encoding': 'deflate',
157+
'Accept-Encoding': 'identity',
198158
});
199159
app.get('/test', async () => {
200160
return body;
@@ -204,6 +164,6 @@ describe('Compress Middleware', () => {
204164
const result = await app.resolve(deflateCompressionEvent, context);
205165

206166
// Assess
207-
expect(result.headers?.['content-encoding']).toBe('deflate');
167+
expect(result.headers?.['content-encoding']).not.toBeDefined;
208168
});
209169
});

0 commit comments

Comments
 (0)