From a50122959dbb5c35afa3c25d49f69a5d3a89ac05 Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Thu, 20 Jul 2023 00:38:01 +0200 Subject: [PATCH] Support for API Gateway closes https://github.com/ashiina/lambda-local/issues/226 closes https://github.com/ashiina/lambda-local/issues/221 closes https://github.com/ashiina/lambda-local/issues/69 --- README.md | 19 +++++----- examples/handler_gateway2.js | 7 ++++ package.json | 4 +-- src/lambdalocal.ts | 67 +++++++++++++++++++++++++++++++----- src/lib/utils.ts | 17 +++++++++ 5 files changed, 94 insertions(+), 20 deletions(-) create mode 100644 examples/handler_gateway2.js diff --git a/README.md b/README.md index 14be48b..9bfe85b 100644 --- a/README.md +++ b/README.md @@ -160,22 +160,23 @@ lambda-local -l index.js -h handler -e examples/s3-put.js lambda-local -l index.js -h handler -e examples/s3-put.js -E '{"key":"value","key2":"value2"}' ``` -#### Running lambda functions as a HTTP Server -A simple way you can run lambda functions locally, without the need to create any special template files (like Serverless plugin and SAM requires), just adding the parameter `--watch`. It will raise a http server listening to the specified port (default is 8008), then you can pass the event payload to the handler via request body. +#### Running lambda functions as a HTTP Server (Amazon API Gateway payload format version 2.0.) + +A simple way you can run lambda functions locally, without the need to create any special template files (like Serverless plugin and SAM requires), just adding the parameter `--watch`. It will raise a http server listening to the specified port (default is 8008). You can then call the lambda as mentionned here: +https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html ```bash -lambda-local -l examples/handler_helloworld.js -h handler --watch 8008 +lambda-local -l examples/handler_gateway2.js -h handler --watch 8008 curl --request POST \ --url http://localhost:8008/ \ --header 'content-type: application/json' \ --data '{ - "event": { - "key1": "value1", - "key2": "value2", - "key3": "value3" - } + "key1": "value1", + "key2": "value2", + "key3": "value3" }' +{"message":"This is a response"} ``` ## About: Definitions @@ -187,7 +188,7 @@ Event data are just JSON objects exported: ```js // Sample event data module.exports = { - foo: "bar" + foo: "bar" }; ``` diff --git a/examples/handler_gateway2.js b/examples/handler_gateway2.js new file mode 100644 index 0000000..91826ba --- /dev/null +++ b/examples/handler_gateway2.js @@ -0,0 +1,7 @@ +/* + * Example Lambda function. + */ + +exports.handler = async function(event, context) { + return { message: "Hello ! Here's a full copy of the event:", event }; +}; \ No newline at end of file diff --git a/package.json b/package.json index 90773e2..c51b4b1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lambda-local", - "version": "2.0.3", + "version": "2.0.0", "description": "Commandline tool to run Lambda functions on your local machine.", "main": "build/lambdalocal.js", "types": "build/lambdalocal.d.ts", @@ -43,7 +43,7 @@ "devDependencies": { "@types/node": "^18.7.16", "chai": "^4.3.6", - "mocha": "^10.0.0", + "mocha": "^10.2.0", "sinon": "^14.0.0", "typescript": "^4.8.3" }, diff --git a/src/lambdalocal.ts b/src/lambdalocal.ts index fd53722..5d838e2 100644 --- a/src/lambdalocal.ts +++ b/src/lambdalocal.ts @@ -11,6 +11,7 @@ import fs = require('fs'); import path = require('path'); import os = require('os'); import { createServer, IncomingMessage, ServerResponse } from 'http'; + import utils = require('./lib/utils.js'); import Context = require('./lib/context.js'); require("./lib/streaming.js"); @@ -64,12 +65,11 @@ export function watch(opts) { return res.end(JSON.stringify({ error })); } try { - if(req.headers['content-type'] !== 'application/json') throw 'Invalid header Content-Type (Expected application/json)'; _getRequestPayload(req, async (error, result) => { try { if(error) throw error; - const data = await execute({ ...opts, event: () => result }); - const ans = JSON.stringify({ data }); + const data = await execute({ ...opts, event: result }); + const ans = _formatResponsePayload(res, data); logger.log('info', log_msg + ` -> OK (${ans.length * 2} bytes)`); return res.end(ans); } catch(error) { @@ -86,6 +86,9 @@ export function watch(opts) { } function _getRequestPayload(req, callback) { + /* + * Handle HTTP server functions. + */ let body = ''; req.on('data', chunk => { body += chunk.toString(); @@ -93,19 +96,65 @@ function _getRequestPayload(req, callback) { req.on('end', () => { let payload; try { - payload = JSON.parse(body); + payload = JSON.parse(body || '{}'); } catch(err) { callback(err); return; } - if(!payload.event) { - callback('Invalid body (Expected "event" property)'); - return; - } - callback(null, payload.event); + // Format: https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html#http-api-develop-integrations-lambda.proxy-format + const url = new URL(req.url, `http://${req.headers.host}`); + const event = { + version: "2.0", + routeKey: "$default", + rawPath: url.pathname, + rawQueryString: url.search, + cookies: utils.parseCookies(req), + headers: req.headers, + queryStringParameters: Object.fromEntries(url.searchParams), + requestContext: { + accountId: "123456789012", + apiId: "api-id", + authentication: {}, + authorizer: {}, + http: { + method: req.method, + path: url.pathname, + protocol: "HTTP/" + req.httpVersion, + sourceIp: req.socket.localAddress, + userAgent: req.headers['user-agent'], + }, + requestId: "id", + routeKey: "$default", + stage: "$default", + time: new Date().toISOString(), + timeEpoch: new Date().getTime(), + }, + body: payload, + isBase64Encoded: req.headers['content-type'] !== 'application/json', + }; + callback(null, event); }); } +function _formatResponsePayload(res, data) { + /* + * Handle HTTP server function output. + */ + // https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html#http-api-develop-integrations-lambda.response + if (!data.statusCode) { + data = { + isBase64Encoded: false, + statusCode: 200, + body: data, + headers: { + "content-type": "application/json", + } + } + } + res.writeHead(data.statusCode, data.headers); + return JSON.stringify(data.body); +} + function updateEnv(env) { /* * Update environment vars if not already in place diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 542829f..3ca74ca 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -96,6 +96,23 @@ export function processJSON(json) { } }; +export function parseCookies (request) { + const list = {}; + const cookieHeader = request.headers?.cookie; + if (!cookieHeader) return list; + + cookieHeader.split(`;`).forEach(function(cookie) { + let [ name, ...rest] = cookie.split(`=`); + name = name?.trim(); + if (!name) return; + const value = rest.join(`=`).trim(); + if (!value) return; + list[name] = decodeURIComponent(value); + }); + + return list; +} + export class TimeoutError extends Error { constructor(m: string) { super(m);