Skip to content

Commit 40b1457

Browse files
committed
open-source aws-lambda-fastify
0 parents  commit 40b1457

9 files changed

+357
-0
lines changed

.editorconfig

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# EditorConfig is awesome: http://EditorConfig.org
2+
root = true
3+
4+
[*.{js,jsx,json}]
5+
end_of_line = lf
6+
insert_final_newline = true
7+
charset = utf-8
8+
indent_style = space
9+
indent_size = 2
10+
trim_trailing_whitespace = true

.eslintignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/node_modules

.eslintrc

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"extends": "standard",
3+
"globals": {
4+
"describe": false,
5+
"it": false,
6+
"before": false,
7+
"after": false,
8+
"beforeEach": false,
9+
"afterEach": false
10+
},
11+
"rules": {
12+
"array-bracket-spacing": 0
13+
}
14+
}

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
node_modules
2+
.DS_Store
3+
npm-debug.log
4+
package-lock.json

.npmignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
.DS_Store
2+
setup
3+
test
4+
.eslintrc
5+
.gitignore
6+
.npmignore
7+
.editorconfig
8+
.vscode
9+
README.md
10+
.eslintignore

README.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Introduction
2+
3+
[![travis](https://img.shields.io/travis/adrai/aws-lambda-fastify.svg)](https://travis-ci.org/adrai/aws-lambda-fastify) [![npm](https://img.shields.io/npm/v/aws-lambda-fastify.svg)](https://npmjs.org/package/aws-lambda-fastify)
4+
5+
Inspired by the AWSLABS [aws-serverless-express](https://github.com/awslabs/aws-serverless-express) library tailor made for the [Fastify](https://www.fastify.io/) web framework.
6+
7+
No use of internal sockets, makes use of Fastify's [inject](https://www.fastify.io/docs/latest/Testing/#testing-with-http-injection) function.
8+
9+
## Installation
10+
11+
```bash
12+
$ npm install aws-lambda-fastify
13+
```
14+
15+
## Example
16+
17+
### app.js
18+
19+
```js
20+
const fastify = require('fastify');
21+
22+
const app = fastify();
23+
app.get('/', (request, reply) => reply.send({ hello: 'world' }));
24+
25+
if (require.main !== module) {
26+
// called directly i.e. "node app"
27+
app.listen(3000, (err) => {
28+
if (err) console.error(err);
29+
console.log('server listening on 3000');
30+
});
31+
} else {
32+
// required as a module => executed on aws lambda
33+
module.exports = app;
34+
}
35+
```
36+
37+
When executed in your lambda function we don't need to listen to a specific port,
38+
so we just export the `app` in this case.
39+
The [`lambda.js`](https://github.com/adrai/aws-lambda-fastify/blob/master/README.md#lambda.js) file will use this export.
40+
41+
When you execute your Fastify application like always,
42+
i.e. `node app.js` *(the detection for this could be `require.main === module`)*,
43+
you can normally listen to your port, so you can still run your Fastify function locally.
44+
45+
### lambda.js
46+
47+
```js
48+
const awsLambdaFastify = require('aws-lambda-fastify')
49+
const app = require('./app');
50+
51+
const proxy = awsLambdaFastify(app)
52+
// or
53+
// const proxy = awsLambdaFastify(app, { binaryMimeTypes: ['application/octet-stream'] })
54+
55+
exports.handler = proxy;
56+
// or
57+
// exports.handler = (event, context, callback) => proxy(event, context, callback);
58+
// or
59+
// exports.handler = (event, context) => proxy(event, context);
60+
// or
61+
// exports.handler = async (event, context) => proxy(event, context);
62+
```
63+
64+
### Hint
65+
66+
The original lambda event and context are passed via headers and can be used like this:
67+
68+
```js
69+
app.get('/', (request, reply) => {
70+
const event = JSON.parse(decodeURIComponent(request.headers['x-apigateway-event']))
71+
const context = JSON.parse(decodeURIComponent(request.headers['x-apigateway-context']))});
72+
// ...
73+
})
74+
```
75+
76+
#### Considerations
77+
78+
- For apps that may not see traffic for several minutes at a time, you could see [cold starts](https://aws.amazon.com/blogs/compute/container-reuse-in-lambda/)
79+
- Stateless only
80+
- API Gateway has a timeout of 29 seconds, and Lambda has a maximum execution time of 15 minutes.

index.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
module.exports = (app, options) => (event, context, callback) => {
2+
options = options || {}
3+
options.binaryMimeTypes = options.binaryMimeTypes || []
4+
event.body = event.body || ''
5+
6+
const method = event.httpMethod
7+
const url = event.path
8+
const query = event.queryStringParameters
9+
const headers = Object.assign({}, event.headers)
10+
const payload = Buffer.from(event.body, event.isBase64Encoded ? 'base64' : 'utf8')
11+
// NOTE: API Gateway is not setting Content-Length header on requests even when they have a body
12+
if (event.body && !headers['Content-Length'] && !headers['content-length']) headers['content-length'] = Buffer.byteLength(payload)
13+
14+
delete event.body
15+
headers['x-apigateway-event'] = encodeURIComponent(JSON.stringify(event))
16+
if (context) headers['x-apigateway-context'] = encodeURIComponent(JSON.stringify(context))
17+
18+
const prom = new Promise((resolve, reject) => {
19+
app.inject({ method, url, query, payload, headers }, (err, res) => {
20+
if (err) {
21+
console.error(err)
22+
return resolve({
23+
statusCode: 500,
24+
body: '',
25+
headers: {}
26+
})
27+
}
28+
// chunked transfer not currently supported by API Gateway
29+
if (headers['transfer-encoding'] === 'chunked') delete headers['transfer-encoding']
30+
if (headers['Transfer-Encoding'] === 'chunked') delete headers['Transfer-Encoding']
31+
32+
// HACK: modifies header casing to get around API Gateway's limitation of not allowing multiple
33+
// headers with the same name, as discussed on the AWS Forum https://forums.aws.amazon.com/message.jspa?messageID=725953#725953
34+
Object.keys(res.headers).forEach((h) => {
35+
if (Array.isArray(res.headers[h])) {
36+
if (h.toLowerCase() === 'set-cookie') {
37+
res.headers[h].forEach((value, i) => { res.headers[require('binary-case')(h, i + 1)] = value })
38+
delete res.headers[h]
39+
} else res.headers[h] = res.headers[h].join(',')
40+
}
41+
})
42+
43+
const contentType = (res.headers['content-type'] || res.headers['Content-Type'] || '').split(';')[0]
44+
const isBase64Encoded = options.binaryMimeTypes.indexOf(contentType) > -1
45+
46+
resolve({
47+
statusCode: res.statusCode,
48+
body: isBase64Encoded ? res.rawPayload.toString('base64') : res.payload,
49+
headers: res.headers,
50+
isBase64Encoded
51+
})
52+
})
53+
})
54+
if (!callback) return prom
55+
prom.then((ret) => callback(null, ret)).catch(callback)
56+
}

package.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "aws-lambda-fastify",
3+
"version": "1.0.0",
4+
"main": "index.js",
5+
"scripts": {
6+
"lint": "eslint .",
7+
"test": "npm run lint && mocha --colors --reporter spec --recursive test"
8+
},
9+
"dependencies": {
10+
"binary-case": "1.1.4"
11+
},
12+
"devDependencies": {
13+
"eslint": "6.0.1",
14+
"eslint-config-standard": "13.0.1",
15+
"eslint-plugin-import": "2.18.0",
16+
"eslint-plugin-node": "9.1.0",
17+
"eslint-plugin-promise": "4.2.1",
18+
"eslint-plugin-standard": "4.0.0",
19+
"fastify": "2.6.0",
20+
"mocha": "6.1.4",
21+
"should": "13.2.3"
22+
}
23+
}

test/basic.js

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
const should = require('should')
2+
const fastify = require('fastify')
3+
const fs = require('fs')
4+
const awsLambdaFastify = require('../index')
5+
6+
describe('basic', () => {
7+
describe('GET', () => {
8+
it('should work as expected', async () => {
9+
const app = fastify()
10+
app.get('/test', async (request, reply) => {
11+
should(request.headers).have.property('x-my-header', 'wuuusaaa')
12+
should(request.headers).have.property('x-apigateway-event', '%7B%22httpMethod%22%3A%22GET%22%2C%22path%22%3A%22%2Ftest%22%2C%22headers%22%3A%7B%22X-My-Header%22%3A%22wuuusaaa%22%7D%7D')
13+
should(request.headers).have.property('user-agent', 'lightMyRequest')
14+
should(request.headers).have.property('host', 'localhost:80')
15+
should(request.headers).have.property('content-length', '0')
16+
reply.header('Set-Cookie', 'qwerty=one')
17+
reply.header('Set-Cookie', 'qwerty=two')
18+
reply.send({ hello: 'world' })
19+
})
20+
const proxy = awsLambdaFastify(app)
21+
const ret = await proxy({
22+
httpMethod: 'GET',
23+
path: '/test',
24+
headers: {
25+
'X-My-Header': 'wuuusaaa'
26+
}
27+
})
28+
should(ret).have.property('statusCode', 200)
29+
should(ret).have.property('body', '{"hello":"world"}')
30+
should(ret).have.property('isBase64Encoded', false)
31+
should(ret).have.property('headers')
32+
should(ret.headers).have.property('content-type', 'application/json; charset=utf-8')
33+
should(ret.headers).have.property('content-length', '17')
34+
should(ret.headers).have.property('date')
35+
should(ret.headers).have.property('connection', 'keep-alive')
36+
should(ret.headers).have.property('Set-cookie', 'qwerty=one')
37+
should(ret.headers).have.property('sEt-cookie', 'qwerty=two')
38+
})
39+
})
40+
41+
describe('GET with base64 encoding response', () => {
42+
let fileBuffer
43+
before((done) => {
44+
fs.readFile(__filename, (err, fb) => {
45+
fileBuffer = fb
46+
done(err)
47+
})
48+
})
49+
it('should work as expected', async () => {
50+
const app = fastify()
51+
app.get('/test', async (request, reply) => {
52+
should(request.headers).have.property('x-my-header', 'wuuusaaa')
53+
should(request.headers).have.property('x-apigateway-event', '%7B%22httpMethod%22%3A%22GET%22%2C%22path%22%3A%22%2Ftest%22%2C%22headers%22%3A%7B%22X-My-Header%22%3A%22wuuusaaa%22%2C%22Content-Type%22%3A%22application%2Fjson%22%7D%7D')
54+
should(request.headers).have.property('user-agent', 'lightMyRequest')
55+
should(request.headers).have.property('host', 'localhost:80')
56+
should(request.headers).have.property('content-length', '0')
57+
reply.header('Set-Cookie', 'qwerty=one')
58+
reply.header('Set-Cookie', 'qwerty=two')
59+
reply.send(fileBuffer)
60+
})
61+
const proxy = awsLambdaFastify(app, { binaryMimeTypes: ['application/octet-stream'] })
62+
const ret = await proxy({
63+
httpMethod: 'GET',
64+
path: '/test',
65+
headers: {
66+
'X-My-Header': 'wuuusaaa',
67+
'Content-Type': 'application/json'
68+
}
69+
})
70+
should(ret).have.property('statusCode', 200)
71+
should(ret).have.property('body', fileBuffer.toString('base64'))
72+
should(ret).have.property('isBase64Encoded', true)
73+
should(ret).have.property('headers')
74+
should(ret.headers).have.property('content-type', 'application/octet-stream')
75+
should(ret.headers).have.property('content-length')
76+
should(ret.headers).have.property('date')
77+
should(ret.headers).have.property('connection', 'keep-alive')
78+
should(ret.headers).have.property('Set-cookie', 'qwerty=one')
79+
should(ret.headers).have.property('sEt-cookie', 'qwerty=two')
80+
})
81+
})
82+
83+
describe('POST', () => {
84+
it('should work as expected', async () => {
85+
const app = fastify()
86+
app.post('/test', async (request, reply) => {
87+
should(request.headers).have.property('content-type', 'application/json')
88+
should(request.headers).have.property('x-my-header', 'wuuusaaa')
89+
should(request.headers).have.property('x-apigateway-event', '%7B%22httpMethod%22%3A%22POST%22%2C%22path%22%3A%22%2Ftest%22%2C%22headers%22%3A%7B%22X-My-Header%22%3A%22wuuusaaa%22%2C%22Content-Type%22%3A%22application%2Fjson%22%7D%7D')
90+
should(request.headers).have.property('user-agent', 'lightMyRequest')
91+
should(request.headers).have.property('host', 'localhost:80')
92+
should(request.headers).have.property('content-length', 14)
93+
should(request.body).have.property('greet', 'hi')
94+
reply.header('Set-Cookie', 'qwerty=one')
95+
reply.header('Set-Cookie', 'qwerty=two')
96+
reply.send({ hello: 'world2' })
97+
})
98+
const proxy = awsLambdaFastify(app)
99+
const ret = await proxy({
100+
httpMethod: 'POST',
101+
path: '/test',
102+
headers: {
103+
'X-My-Header': 'wuuusaaa',
104+
'Content-Type': 'application/json'
105+
},
106+
body: '{"greet":"hi"}'
107+
})
108+
should(ret).have.property('statusCode', 200)
109+
should(ret).have.property('body', '{"hello":"world2"}')
110+
should(ret).have.property('isBase64Encoded', false)
111+
should(ret).have.property('headers')
112+
should(ret.headers).have.property('content-type', 'application/json; charset=utf-8')
113+
should(ret.headers).have.property('content-length', '18')
114+
should(ret.headers).have.property('date')
115+
should(ret.headers).have.property('connection', 'keep-alive')
116+
should(ret.headers).have.property('Set-cookie', 'qwerty=one')
117+
should(ret.headers).have.property('sEt-cookie', 'qwerty=two')
118+
})
119+
})
120+
121+
describe('POST with base64 encoding', () => {
122+
it('should work as expected', async () => {
123+
const app = fastify()
124+
app.post('/test', async (request, reply) => {
125+
should(request.headers).have.property('content-type', 'application/json')
126+
should(request.headers).have.property('x-my-header', 'wuuusaaa')
127+
should(request.headers).have.property('x-apigateway-event', '%7B%22httpMethod%22%3A%22POST%22%2C%22path%22%3A%22%2Ftest%22%2C%22headers%22%3A%7B%22X-My-Header%22%3A%22wuuusaaa%22%2C%22Content-Type%22%3A%22application%2Fjson%22%7D%2C%22isBase64Encoded%22%3Atrue%7D')
128+
should(request.headers).have.property('user-agent', 'lightMyRequest')
129+
should(request.headers).have.property('host', 'localhost:80')
130+
should(request.headers).have.property('content-length', 15)
131+
should(request.body).have.property('greet', 'hi')
132+
reply.header('Set-Cookie', 'qwerty=one')
133+
reply.header('Set-Cookie', 'qwerty=two')
134+
reply.send({ hello: 'world2' })
135+
})
136+
const proxy = awsLambdaFastify(app)
137+
const ret = await proxy({
138+
httpMethod: 'POST',
139+
path: '/test',
140+
headers: {
141+
'X-My-Header': 'wuuusaaa',
142+
'Content-Type': 'application/json'
143+
},
144+
body: 'eyJncmVldCI6ICJoaSJ9',
145+
isBase64Encoded: true
146+
})
147+
should(ret).have.property('statusCode', 200)
148+
should(ret).have.property('body', '{"hello":"world2"}')
149+
should(ret).have.property('isBase64Encoded', false)
150+
should(ret).have.property('headers')
151+
should(ret.headers).have.property('content-type', 'application/json; charset=utf-8')
152+
should(ret.headers).have.property('content-length', '18')
153+
should(ret.headers).have.property('date')
154+
should(ret.headers).have.property('connection', 'keep-alive')
155+
should(ret.headers).have.property('Set-cookie', 'qwerty=one')
156+
should(ret.headers).have.property('sEt-cookie', 'qwerty=two')
157+
})
158+
})
159+
})

0 commit comments

Comments
 (0)