diff --git a/README.md b/README.md index af2b092..4b1e31d 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ $ npm i @fastify/aws-lambda | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------- | | binaryMimeTypes | Array of binary MimeTypes to handle | `[]` | | enforceBase64 | Function that receives the response and returns a boolean indicating if the response content is binary or not and should be base64-encoded | `undefined` | +| disableBase64Encoding | Disable base64 encoding of responses and omit the `isBase64Encoded` property. When `undefined`, it is automatically enabled when `payloadAsStream` is `true` and the request does not come from an ALB. | `undefined` | | serializeLambdaArguments | Activate the serialization of lambda Event and Context in http header `x-apigateway-event` `x-apigateway-context` | `false` *(was `true` for { return enforceBase64(res) === true } +const disableBase64EncodingDefault = (options) => { + if (options.payloadAsStream) { + return (event) => !event.requestContext?.elb + } + return (_event) => false +} + module.exports = (app, options) => { options = options || {} options.binaryMimeTypes = options.binaryMimeTypes || [] @@ -20,6 +27,12 @@ module.exports = (app, options) => { options.parseCommaSeparatedQueryParams = options.parseCommaSeparatedQueryParams !== undefined ? options.parseCommaSeparatedQueryParams : true options.payloadAsStream = options.payloadAsStream !== undefined ? options.payloadAsStream : false options.albMultiValueHeaders = options.albMultiValueHeaders !== undefined ? options.albMultiValueHeaders : false + if (options.disableBase64Encoding === undefined) { + options.disableBase64Encoding = disableBase64EncodingDefault(options) + } else { + const opt = options.disableBase64Encoding + options.disableBase64Encoding = (_event) => opt + } let currentAwsArguments = {} if (options.decorateRequest) { options.decorationPropertyName = options.decorationPropertyName || 'awsLambda' @@ -160,15 +173,17 @@ module.exports = (app, options) => { } }) + const isBase64Disabled = options.disableBase64Encoding(event) const contentType = (res.headers['content-type'] || res.headers['Content-Type'] || '').split(';', 1)[0] - const isBase64Encoded = options.binaryMimeTypes.indexOf(contentType) > -1 || customBinaryCheck(options, res) + const shouldBase64Encode = !isBase64Disabled && (options.binaryMimeTypes.indexOf(contentType) > -1 || customBinaryCheck(options, res)) const ret = { statusCode: res.statusCode, - headers: res.headers, - isBase64Encoded + headers: res.headers } + if (!isBase64Disabled) ret.isBase64Encoded = shouldBase64Encode + if (cookies && event.version === '2.0') ret.cookies = cookies if (multiValueHeaders && (!event.version || event.version === '1.0')) ret.multiValueHeaders = multiValueHeaders if (options.albMultiValueHeaders) { @@ -179,7 +194,7 @@ module.exports = (app, options) => { } if (!options.payloadAsStream) { - ret.body = isBase64Encoded ? res.rawPayload.toString('base64') : res.payload + ret.body = shouldBase64Encode ? res.rawPayload.toString('base64') : res.payload return resolve(ret) } diff --git a/test/alb.test.js b/test/alb.test.js index 4cda869..b359a69 100644 --- a/test/alb.test.js +++ b/test/alb.test.js @@ -2,8 +2,10 @@ const { describe, it } = require('node:test') const assert = require('node:assert') +const { Readable } = require('node:stream') const fastify = require('fastify') const awsLambdaFastify = require('../index') +const { accumulate } = require('./utils') describe('ALB Tests', async () => { it('GET request', async () => { @@ -57,6 +59,33 @@ describe('ALB Tests', async () => { assert.deepStrictEqual(ret.cookies, ['qwerty=one', 'qwerty=two']) }) + it('GET streamed response with payloadAsStream', async () => { + const app = fastify() + app.get('/stream', async (_request, reply) => { + reply.header('content-type', 'application/json; charset=utf-8') + return reply.send(Readable.from(JSON.stringify({ hello: 'world' }))) + }) + + const proxy = awsLambdaFastify(app, { + payloadAsStream: true + }) + + const { meta, stream } = await proxy({ + requestContext: { elb: { targetGroupArn: 'xxx' } }, + httpMethod: 'GET', + path: '/stream', + headers: { + 'X-My-Header': 'wuuusaaa' + } + }) + + assert.equal(meta.statusCode, 200) + assert.equal(meta.isBase64Encoded, false) + + const data = await accumulate(stream) + assert.equal(data.toString(), '{"hello":"world"}') + }) + it('GET Broken', async () => { const event = { requestContext: { http: { method: 'GET' } }, diff --git a/test/basic.test.js b/test/basic.test.js index 1385d43..59b947a 100644 --- a/test/basic.test.js +++ b/test/basic.test.js @@ -133,6 +133,39 @@ describe('Basic Tests', () => { assert.equal(ret.headers['set-cookie'], 'qwerty=one') }) + it('GET with base64 encoding disabled', async () => { + const fileBuffer = await readFileAsync(__filename) + const app = fastify() + + app.get('/test', async (_request, reply) => { + reply.send(fileBuffer) + }) + + const proxy = awsLambdaFastify(app, { + binaryMimeTypes: ['application/octet-stream'], + serializeLambdaArequestrguments: true, + disableBase64Encoding: true + }) + + const ret = await proxy({ + httpMethod: 'GET', + path: '/test', + headers: { + 'Content-Type': 'application/json' + } + }) + + assert.equal(ret.statusCode, 200) + assert.equal(ret.body, fileBuffer.toString()) + assert.equal(ret.isBase64Encoded, undefined) + assert.ok(ret.headers) + assert.equal(ret.headers['content-type'], 'application/octet-stream') + assert.ok(ret.headers['content-length']) + assert.ok(ret.headers.date) + assert.equal(ret.headers.connection, 'keep-alive') + assert.equal(ret.multiValueHeaders, undefined) + }) + it('GET with content-encoding response', async () => { const app1 = fastify() const fileBuffer = await readFileAsync(__filename) diff --git a/test/stream.test.js b/test/stream.test.js index 6b83b94..1857b4d 100644 --- a/test/stream.test.js +++ b/test/stream.test.js @@ -2,21 +2,10 @@ const { describe, it } = require('node:test') const assert = require('node:assert') -const { promisify } = require('node:util') const { Readable } = require('node:stream') const fastify = require('fastify') const awsLambdaFastify = require('../index') - -const accumulate = promisify(function (stream, cb) { - const chunks = [] - stream.on('error', cb) - stream.on('data', (chunk) => { - chunks.push(chunk) - }) - stream.on('end', () => { - cb(null, Buffer.concat(chunks)) - }) -}) +const { accumulate } = require('./utils') describe('Basic Stream Tests', () => { it('GET a normal response as stream', async () => { @@ -70,7 +59,7 @@ describe('Basic Stream Tests', () => { const data = await accumulate(stream) assert.equal(data.toString(), '{"hello":"world"}', 'Response body should match') - assert.equal(meta.isBase64Encoded, false, 'isBase64Encoded should be false') + assert.equal(meta.isBase64Encoded, undefined, 'isBase64Encoded should not be set') assert.ok(meta.headers, 'Headers should be defined') assert.equal( meta.headers['content-type'], @@ -147,7 +136,7 @@ describe('Basic Stream Tests', () => { const data = await accumulate(stream) assert.equal(data.toString(), '{"hello":"world"}', 'Response body should match') - assert.equal(meta.isBase64Encoded, false, 'isBase64Encoded should be false') + assert.equal(meta.isBase64Encoded, undefined, 'isBase64Encoded should not be set') assert.ok(meta.headers, 'Headers should be defined') assert.equal( meta.headers['content-type'], @@ -171,4 +160,30 @@ describe('Basic Stream Tests', () => { 'Cookies should match' ) }) + + it('GET a streamed response as stream with disableBase64Encoding false', async () => { + const app = fastify() + + const evt = { + version: '2.0', + httpMethod: 'GET', + path: '/test', + headers: { + 'X-My-Header': 'wuuusaaa' + }, + cookies: ['foo=bar'], + queryStringParameters: '' + } + + app.get('/test', async (_request, reply) => { + reply.header('content-type', 'application/json; charset=utf-8') + return reply.send(Readable.from(JSON.stringify({ hello: 'world' }))) + }) + + const proxy = awsLambdaFastify(app, { payloadAsStream: true, disableBase64Encoding: false }) + + const { meta } = await proxy(evt) + + assert.equal(meta.isBase64Encoded, false) + }) }) diff --git a/test/utils.js b/test/utils.js new file mode 100644 index 0000000..2552b60 --- /dev/null +++ b/test/utils.js @@ -0,0 +1,18 @@ +'use strict' + +const accumulate = function (stream) { + return new Promise((resolve, reject) => { + const chunks = [] + stream.on('error', reject) + stream.on('data', (chunk) => { + chunks.push(chunk) + }) + stream.on('end', () => { + resolve(Buffer.concat(chunks)) + }) + }) +} + +module.exports = { + accumulate +} diff --git a/types/index.d.ts b/types/index.d.ts index 174256a..e6733eb 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -12,6 +12,7 @@ declare namespace awsLambdaFastify { decorateRequest?: boolean; decorationPropertyName?: string; enforceBase64?: (response: LightMyRequestResponse) => boolean; + disableBase64Encoding?: boolean; retainStage?: boolean; /** * usually set to 'proxy', if used @@ -38,7 +39,7 @@ declare namespace awsLambdaFastify { export interface LambdaResponseBase { statusCode: number; headers: Record; - isBase64Encoded: boolean; + isBase64Encoded?: boolean; cookies?: string[]; } diff --git a/types/index.test-d.ts b/types/index.test-d.ts index db6cb60..84329c1 100644 --- a/types/index.test-d.ts +++ b/types/index.test-d.ts @@ -92,6 +92,9 @@ expectAssignable({ }, retainStage: true, }); +expectAssignable({ + disableBase64Encoding: true, +}); expectError(awsLambdaFastify()); expectError(awsLambdaFastify(app, { neh: "definition" }));