Skip to content

Commit

Permalink
Support for API Gateway
Browse files Browse the repository at this point in the history
closes #226
closes #221
closes #69
  • Loading branch information
gpotter2 committed Jul 19, 2023
1 parent 3710aa2 commit a501229
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 20 deletions.
19 changes: 10 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -187,7 +188,7 @@ Event data are just JSON objects exported:
```js
// Sample event data
module.exports = {
foo: "bar"
foo: "bar"
};
```

Expand Down
7 changes: 7 additions & 0 deletions examples/handler_gateway2.js
Original file line number Diff line number Diff line change
@@ -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 };
};
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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"
},
Expand Down
67 changes: 58 additions & 9 deletions src/lambdalocal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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) {
Expand All @@ -86,26 +86,75 @@ export function watch(opts) {
}

function _getRequestPayload(req, callback) {
/*
* Handle HTTP server functions.
*/
let body = '';
req.on('data', chunk => {
body += chunk.toString();
});
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
Expand Down
17 changes: 17 additions & 0 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down

0 comments on commit a501229

Please sign in to comment.