Skip to content

Commit 6f7e2b6

Browse files
committed
Detect UTF8 encoding in text responses
1 parent 031b8de commit 6f7e2b6

File tree

5 files changed

+59
-8
lines changed

5 files changed

+59
-8
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@
3434
"main": "./dist/compile/lambda",
3535
"types": "./dist/compile/lambda.d.ts",
3636
"dependencies": {
37-
"in-process-request": "^0.1.0"
37+
"in-process-request": "^0.1.0",
38+
"isutf8": "^2.1.0"
3839
},
3940
"devDependencies": {
4041
"@types/compression": "^1.7.0",

src/@types/isutf8/index.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
declare module 'isutf8' {
2+
export default function isUtf8(b: Buffer): boolean
3+
}

src/response.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { InProcessResponse } from 'in-process-request';
22
import { LambdaResponse } from './types';
33
import fixResponseHeaders from './fixResponseHeaders'
4+
import isUtf8 from 'isutf8'
45

6+
type Encoding = 'base64' | 'utf8'
57
export const inProcessResponseToLambdaResponse = (response: InProcessResponse, supportMultiHeaders: boolean, supportCookies: boolean): LambdaResponse => {
6-
const encoding = response.isUTF8 ? 'utf8' : 'base64';
8+
const encoding = getEncoding(response);
79
return {
810
statusCode: response.statusCode,
911
body: response.body.toString(encoding),
@@ -12,6 +14,21 @@ export const inProcessResponseToLambdaResponse = (response: InProcessResponse, s
1214
};
1315
};
1416

17+
const getEncoding = (response: InProcessResponse): Encoding => {
18+
// APi Gateway REST API cannot handle html responses encoded as base64
19+
if (response.isUTF8) {
20+
return 'utf8';
21+
}
22+
const contentType = (response.headers['content-type'] as string || '').toLowerCase();
23+
const isJson = (): boolean => contentType.startsWith('application/json')
24+
const isText = (): boolean => contentType.startsWith('text/')
25+
const maybeUtf8 = isJson() || isText()
26+
if (maybeUtf8 && isUtf8(response.body)) {
27+
return 'utf8'
28+
}
29+
return 'base64'
30+
}
31+
1532
export const errorResponse = (): LambdaResponse => {
1633
return {
1734
statusCode: 500,

test/response.test.ts

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,19 @@ import { inProcessResponseToLambdaResponse, errorResponse} from '../src/response
22
import { InProcessResponse } from 'in-process-request';
33

44
describe('inProcessResponseToLambdaResponse', () => {
5-
const generateMockResponse = (isUTF8: boolean = true): InProcessResponse => ({
6-
body: Buffer.from('hello'),
5+
interface GenerateMockResponseOptions {
6+
body?: Buffer
7+
isUTF8?: boolean
8+
contentType?: string
9+
}
10+
const generateMockResponse = (opts: GenerateMockResponseOptions = {}): InProcessResponse => ({
11+
body: opts.body || Buffer.from('hello'),
712
headers: {
813
'set-cookie': ['chocolate=10; Path=/', 'peanut_butter=20; Path=/'],
9-
'content-type': 'text/plain',
14+
'content-type': opts.contentType || 'text/plain',
1015
'x-custom': '10',
1116
},
12-
isUTF8,
17+
isUTF8: opts.isUTF8 === true,
1318
statusCode: 200,
1419
statusMessage: 'OK',
1520
});
@@ -81,17 +86,37 @@ describe('inProcessResponseToLambdaResponse', () => {
8186

8287
describe('when the body is not utf8', () => {
8388
it('has base64 body', () => {
84-
const res = inProcessResponseToLambdaResponse(generateMockResponse(false), true, false);
89+
const res = inProcessResponseToLambdaResponse(generateMockResponse({isUTF8: false, contentType: 'not-text/plain'}), true, false);
8590
expect(res.body).toEqual('aGVsbG8=');
8691
})
8792

8893
it('is not base64 encoded', () => {
89-
const res = inProcessResponseToLambdaResponse(generateMockResponse(false), true, false);
94+
const res = inProcessResponseToLambdaResponse(generateMockResponse({isUTF8: false, contentType: 'not-text/plain'}), true, false);
9095
expect(res.isBase64Encoded).toEqual(true);
9196
})
9297

98+
it('has basee64 body if isUTF8 is set to false and content type starts with text/ and the content is not UTF8', () => {
99+
const res = inProcessResponseToLambdaResponse(generateMockResponse({body: Buffer.from([1,2,3,4,5,6]), isUTF8: false, contentType: 'binary/octet-stream'}), false, false);
100+
expect(res.isBase64Encoded).toEqual(true);
101+
expect(res.body).toEqual('AQIDBAUG');
102+
})
103+
104+
})
93105

106+
describe('UTF8 content', () => {
107+
it('has utf8 body if isUTF8 is set to true', () => {
108+
const res = inProcessResponseToLambdaResponse(generateMockResponse({isUTF8: true, contentType: 'not-text/plain'}), false, false);
109+
expect(res.isBase64Encoded).toEqual(false);
110+
expect(res.body).toEqual('hello');
111+
})
112+
113+
it('has utf8 body if isUTF8 is set to false but content type starts with text/ and the content is text', () => {
114+
const res = inProcessResponseToLambdaResponse(generateMockResponse({body: Buffer.from('text'), isUTF8: false, contentType: 'text/plain'}), false, false);
115+
expect(res.isBase64Encoded).toEqual(false);
116+
expect(res.body).toEqual('text');
117+
})
94118
})
119+
95120
})
96121

97122
describe('errorResponse', () => {

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2100,6 +2100,11 @@ istanbul-reports@^3.0.2:
21002100
html-escaper "^2.0.0"
21012101
istanbul-lib-report "^3.0.0"
21022102

2103+
isutf8@^2.1.0:
2104+
version "2.1.0"
2105+
resolved "https://registry.yarnpkg.com/isutf8/-/isutf8-2.1.0.tgz#b6d08a02d4ce43bf3b4be39b9b60231b88dfeb2b"
2106+
integrity sha512-rEMU6f82evtJNtYMrtVODUbf+C654mos4l+9noOueesUMipSWK6x3tpt8DiXhcZh/ZOBWYzJ9h9cNAlcQQnMiQ==
2107+
21032108
jake@^10.6.1:
21042109
version "10.6.1"
21052110
resolved "https://registry.yarnpkg.com/jake/-/jake-10.6.1.tgz#c9c476cfd6e726ef600ee9bb2b880d5425ff8c79"

0 commit comments

Comments
 (0)