UNPKG

@fastify/aws-lambda

Version:

Inspired by aws-serverless-express to work with Fastify with inject functionality.

191 lines (175 loc) 9.22 kB
'use strict' const isCompressedDefault = (res) => { const contentEncoding = res.headers['content-encoding'] || res.headers['Content-Encoding'] return contentEncoding && contentEncoding !== 'identity' } const customBinaryCheck = (options, res) => { const enforceBase64 = typeof options.enforceBase64 === 'function' ? options.enforceBase64 : isCompressedDefault return enforceBase64(res) === true } module.exports = (app, options) => { options = options || {} options.binaryMimeTypes = options.binaryMimeTypes || [] options.serializeLambdaArguments = options.serializeLambdaArguments !== undefined ? options.serializeLambdaArguments : false options.decorateRequest = options.decorateRequest !== undefined ? options.decorateRequest : true options.retainStage = options.retainStage !== undefined ? options.retainStage : false options.pathParameterUsedAsPath = options.pathParameterUsedAsPath !== undefined ? options.pathParameterUsedAsPath : false options.parseCommaSeparatedQueryParams = options.parseCommaSeparatedQueryParams !== undefined ? options.parseCommaSeparatedQueryParams : true options.payloadAsStream = options.payloadAsStream !== undefined ? options.payloadAsStream : false let currentAwsArguments = {} if (options.decorateRequest) { options.decorationPropertyName = options.decorationPropertyName || 'awsLambda' app.decorateRequest(options.decorationPropertyName, { getter: () => ({ get event () { return currentAwsArguments.event }, get context () { return currentAwsArguments.context } }) }) } return (event, context, callback) => { currentAwsArguments.event = event currentAwsArguments.context = context if (options.callbackWaitsForEmptyEventLoop !== undefined) { context.callbackWaitsForEmptyEventLoop = options.callbackWaitsForEmptyEventLoop } // event.body = event.body || '' // do not magically default body to '' const method = event.httpMethod || (event.requestContext && event.requestContext.http ? event.requestContext.http.method : undefined) let url = (options.pathParameterUsedAsPath && event.pathParameters && event.pathParameters[options.pathParameterUsedAsPath] && `/${event.pathParameters[options.pathParameterUsedAsPath]}`) || event.path || event.rawPath || '/' // seen rawPath for HTTP-API // NOTE: if used directly via API Gateway domain and /stage if (!options.retainStage && event.requestContext && event.requestContext.stage && event.requestContext.resourcePath && (url).indexOf(`/${event.requestContext.stage}/`) === 0 && event.requestContext.resourcePath.indexOf(`/${event.requestContext.stage}/`) !== 0) { url = url.substring(event.requestContext.stage.length + 1) } const query = {} const parsedCommaSeparatedQuery = {} if (event.requestContext && event.requestContext.elb) { if (event.multiValueQueryStringParameters) { Object.keys(event.multiValueQueryStringParameters).forEach((q) => { query[decodeURIComponent(q)] = event.multiValueQueryStringParameters[q].map((val) => decodeURIComponent(val)) }) } else if (event.queryStringParameters) { Object.keys(event.queryStringParameters).forEach((q) => { query[decodeURIComponent(q)] = decodeURIComponent(event.queryStringParameters[q]) if (options.parseCommaSeparatedQueryParams && event.version === '2.0' && typeof query[decodeURIComponent(q)] === 'string' && query[decodeURIComponent(q)].indexOf(',') > 0) { parsedCommaSeparatedQuery[decodeURIComponent(q)] = query[decodeURIComponent(q)].split(',') } }) } } else { if (event.queryStringParameters && options.parseCommaSeparatedQueryParams && event.version === '2.0') { Object.keys(event.queryStringParameters).forEach((k) => { if (typeof event.queryStringParameters[k] === 'string' && event.queryStringParameters[k].indexOf(',') > 0) { parsedCommaSeparatedQuery[decodeURIComponent(k)] = event.queryStringParameters[k].split(',') } }) } Object.assign(query, event.multiValueQueryStringParameters || event.queryStringParameters, parsedCommaSeparatedQuery) } const headers = Object.assign({}, event.headers) if (event.multiValueHeaders) { Object.keys(event.multiValueHeaders).forEach((h) => { if (event.multiValueHeaders[h].length > 1) { headers[h] = event.multiValueHeaders[h] } }) } const payload = event.body !== null && event.body !== undefined ? Buffer.from(event.body, event.isBase64Encoded ? 'base64' : 'utf8') : event.body // NOTE: API Gateway is not setting Content-Length header on requests even when they have a body if (event.body && !headers['Content-Length'] && !headers['content-length']) headers['content-length'] = Buffer.byteLength(payload) if (options.serializeLambdaArguments) { event.body = undefined // remove body from event only when setting request headers headers['x-apigateway-event'] = encodeURIComponent(JSON.stringify(event)) if (context) headers['x-apigateway-context'] = encodeURIComponent(JSON.stringify(context)) } if (event.requestContext && event.requestContext.requestId) { headers['x-request-id'] = headers['x-request-id'] || event.requestContext.requestId } // API gateway v2 cookies: https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html if (event.cookies && event.cookies.length) { headers.cookie = event.cookies.join(';') } let remoteAddress if (event.requestContext) { if (event.requestContext.http && event.requestContext.http.sourceIp) { remoteAddress = event.requestContext.http.sourceIp } else if (event.requestContext.identity && event.requestContext.identity.sourceIp) { remoteAddress = event.requestContext.identity.sourceIp } } const prom = new Promise((resolve) => { app.inject({ method, url, query, payload, headers, remoteAddress, payloadAsStream: options.payloadAsStream }, (err, res) => { currentAwsArguments = {} if (err) { console.error(err) if (!options.payloadAsStream) { return resolve({ statusCode: 500, body: '', headers: {} }) } const stream = res && res.stream() return resolve({ meta: { statusCode: 500, headers: {} }, // fix issue with Lambda where streaming repsonses always require a body to be present stream: stream && stream.readableLength > 0 ? stream : require('node:stream').Readable.from('') }) } // chunked transfer not currently supported by API Gateway if (headers['transfer-encoding'] === 'chunked') delete headers['transfer-encoding'] if (headers['Transfer-Encoding'] === 'chunked') delete headers['Transfer-Encoding'] let multiValueHeaders let cookies Object.keys(res.headers).forEach((h) => { const isSetCookie = h.toLowerCase() === 'set-cookie' const isArraycookie = Array.isArray(res.headers[h]) if (isArraycookie) { if (isSetCookie) { multiValueHeaders = multiValueHeaders || {} multiValueHeaders[h] = res.headers[h] } else res.headers[h] = res.headers[h].join(',') } else if (typeof res.headers[h] !== 'undefined' && typeof res.headers[h] !== 'string') { // NOTE: API Gateway (i.e. HttpApi) validates all headers to be a string res.headers[h] = res.headers[h].toString() } if (isSetCookie) { cookies = isArraycookie ? res.headers[h] : [res.headers[h]] if (event.version === '2.0' || isArraycookie) delete res.headers[h] } }) const contentType = (res.headers['content-type'] || res.headers['Content-Type'] || '').split(';', 1)[0] const isBase64Encoded = options.binaryMimeTypes.indexOf(contentType) > -1 || customBinaryCheck(options, res) const ret = { statusCode: res.statusCode, headers: res.headers, isBase64Encoded } if (cookies && event.version === '2.0') ret.cookies = cookies if (multiValueHeaders && (!event.version || event.version === '1.0')) ret.multiValueHeaders = multiValueHeaders if (!options.payloadAsStream) { ret.body = isBase64Encoded ? res.rawPayload.toString('base64') : res.payload return resolve(ret) } const stream = res.stream() resolve({ meta: ret, // fix issue with Lambda where streaming repsonses always require a body to be present stream: stream && stream.readableLength > 0 ? stream : require('node:stream').Readable.from('') }) }) }) if (!callback) return prom prom.then((ret) => callback(null, ret)).catch(callback) return prom } } module.exports.default = module.exports module.exports.awsLambdaFastify = module.exports