Skip to content

Commit 9f10869

Browse files
authored
Merge pull request #33 from jkyberneees/support-http-lambda-proxy
adding http-lambda-proxy integration
2 parents 8478a3f + 68c5b59 commit 9f10869

File tree

8 files changed

+184
-94
lines changed

8 files changed

+184
-94
lines changed

.npmignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.env
2+
test/
3+
demos/
4+
.nyc_output
5+
.github/
6+
.travis.yml
7+
.benchmark/

README.md

Lines changed: 66 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ npm i fast-gateway
1212
```
1313

1414
## Usage
15-
### Gateway
15+
Next we describe two examples proxying HTTP and Lambda downstream services.
16+
> For simplicity of reading, both examples are separated, however a single gateway configuration supports all routes configurations.
17+
### HTTP Proxying
18+
#### Gateway
1619
```js
1720
const gateway = require('fast-gateway')
1821
const server = gateway({
@@ -24,14 +27,56 @@ const server = gateway({
2427

2528
server.start(8080)
2629
```
27-
### Remote Service
30+
#### Remote Service
2831
```js
2932
const service = require('restana')()
3033
service.get('/get', (req, res) => res.send('Hello World!'))
3134

3235
service.start(3000)
3336
```
3437

38+
### Lambda Proxying
39+
#### Gateway
40+
```bash
41+
npm i http-lambda-proxy
42+
```
43+
```js
44+
const gateway = require('fast-gateway')
45+
const server = gateway({
46+
routes: [{
47+
prefix: '/service',
48+
target: 'my-lambda-serverless-api',
49+
lambdaProxy: {
50+
region: 'eu-central-1'
51+
}
52+
}]
53+
})
54+
55+
server.start(8080)
56+
```
57+
#### Lambda Implementation
58+
```js
59+
const serverless = require('serverless-http')
60+
const json = require('serverless-json-parser')
61+
const query = require('connect-query')
62+
63+
const service = require('restana')()
64+
service.use(query())
65+
service.use(json())
66+
67+
// routes
68+
service.get('/get', (req, res) => {
69+
res.send({ msg: 'Go Serverless!' })
70+
})
71+
service.post('/post', (req, res) => {
72+
res.send(req.body)
73+
})
74+
75+
// export handler
76+
module.exports.handler = serverless(service)
77+
78+
```
79+
3580
## Configuration options explained
3681
```js
3782
{
@@ -42,7 +87,6 @@ service.start(3000)
4287
// If omitted, restana is used as default HTTP framework
4388
server,
4489
// Optional restana library configuration (https://www.npmjs.com/package/restana#configuration)
45-
//
4690
// Please note that if "server" is provided, this settings are ignored.
4791
restana: {},
4892
// Optional global middlewares in the format: (req, res, next) => next()
@@ -51,23 +95,36 @@ service.start(3000)
5195
// Optional global value for routes "pathRegex". Default value: '/*'
5296
pathRegex: '/*',
5397
// Optional global requests timeout value (given in milliseconds). Default value: '0' (DISABLED)
98+
// Ignored if proxyType = 'lambda'
5499
timeout: 0,
55100
// Optional "target" value that overrides the routes "target" config value. Feature intended for testing purposes.
56101
targetOverride: "https://yourdev.api-gateway.com",
57102

58103
// HTTP proxy
59104
routes: [{
105+
// Optional proxy type definition. Supported values: http, lambda
106+
// Default value: http
107+
proxyType: 'http'
60108
// Optional `fast-proxy` library configuration (https://www.npmjs.com/package/fast-proxy#options)
61109
// base parameter defined as the route target. Default value: {}
110+
// This settings apply only when proxyType = 'http'
62111
fastProxy: {},
112+
// Optional `http-lambda-proxy` library configuration (https://www.npmjs.com/package/http-lambda-proxy#options)
113+
// The 'target' parameter is extracted from route.target, default region = 'eu-central-1'
114+
// This settings apply only when proxyType = 'lambda'
115+
lambdaProxy: {
116+
region: 'eu-central-1'
117+
},
63118
// Optional proxy handler function. Default value: (req, res, url, proxy, proxyOpts) => proxy(req, res, url, proxyOpts)
64119
proxyHandler: () => {},
65120
// Optional flag to indicate if target uses the HTTP2 protocol. Default value: false
121+
// This setting apply only when proxyType = 'http'
66122
http2: false,
67123
// Optional path matching regex. Default value: '/*'
68124
// In order to disable the 'pathRegex' at all, you can use an empty string: ''
69125
pathRegex: '/*',
70126
// Optional service requests timeout value (given in milliseconds). Default value: '0' (DISABLED)
127+
// This setting apply only when proxyType = 'http'
71128
timeout: 0,
72129
// route prefix
73130
prefix: '/public',
@@ -79,7 +136,8 @@ service.start(3000)
79136
},
80137
// Optional "prefix rewrite" before request is forwarded. Default value: ''
81138
prefixRewrite: '',
82-
// Remote HTTP server URL to forward the request
139+
// Remote HTTP server URL to forward the request.
140+
// If proxyType = 'lambda', the value is the name of the Lambda function, version, or alias.
83141
target: 'http://localhost:3000',
84142
// Optional HTTP methods to limit the requests proxy to certain verbs only
85143
// Supported HTTP methods: ['GET', 'DELETE', 'PATCH', 'POST', 'PUT', 'HEAD', 'OPTIONS', 'TRACE']
@@ -99,54 +157,14 @@ service.start(3000)
99157
// ...
100158
}
101159

102-
// other options allowed https://www.npmjs.com/package/fast-proxy#opts
160+
// if proxyType= 'http', other options allowed https://www.npmjs.com/package/fast-proxy#opts
103161
}
104162
}]
105163
}
106164
```
107-
### onResponse Hook default implementation
108-
For developers reference, next we describe how the default `onResponse` hook looks like:
109-
```js
110-
const pump = require('pump')
111-
const toArray = require('stream-to-array')
112-
const TRANSFER_ENCODING_HEADER_NAME = 'transfer-encoding'
113-
114-
const onResponse = async (req, res, stream) => {
115-
const chunked = stream.headers[TRANSFER_ENCODING_HEADER_NAME]
116-
? stream.headers[TRANSFER_ENCODING_HEADER_NAME].endsWith('chunked')
117-
: false
118-
119-
if (req.headers.connection === 'close' && chunked) {
120-
try {
121-
// remove transfer-encoding header
122-
const transferEncoding = stream.headers[TRANSFER_ENCODING_HEADER_NAME].replace(/(,( )?)?chunked/, '')
123-
if (transferEncoding) {
124-
res.setHeader(TRANSFER_ENCODING_HEADER_NAME, transferEncoding)
125-
} else {
126-
res.removeHeader(TRANSFER_ENCODING_HEADER_NAME)
127-
}
128-
129-
if (!stream.headers['content-length']) {
130-
// pack all pieces into 1 buffer to calculate content length
131-
const resBuffer = Buffer.concat(await toArray(stream))
132-
133-
// add content-length header and send the merged response buffer
134-
res.setHeader('content-length', '' + Buffer.byteLength(resBuffer))
135-
res.statusCode = stream.statusCode
136-
res.end(resBuffer)
137-
138-
return
139-
}
140-
} catch (err) {
141-
res.statusCode = 500
142-
res.end(err.message)
143-
}
144-
}
165+
### onResponse hooks default implementation
166+
For developers reference, default hooks implementation are located in `lib/default-hooks.js` file.
145167

146-
res.statusCode = stream.statusCode
147-
pump(stream, res)
148-
}
149-
```
150168
## The "*GET /services.json*" endpoint
151169
Since version `1.3.5` the gateway exposes minimal documentation about registered services at: `GET /services.json`
152170

@@ -312,6 +330,7 @@ This is your repo ;)
312330
## Related projects
313331
- middleware-if-unless (https://www.npmjs.com/package/middleware-if-unless)
314332
- fast-proxy (https://www.npmjs.com/package/fast-proxy)
333+
- http-lambda-proxy (https://www.npmjs.com/package/http-lambda-proxy)
315334
- restana (https://www.npmjs.com/package/restana)
316335

317336
## Benchmarks

index.js

Lines changed: 12 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
/* eslint-disable no-useless-call */
22

3-
const fastProxy = require('fast-proxy')
3+
const proxyFactory = require('./lib/proxy-factory')
44
const restana = require('restana')
5-
const pump = require('pump')
6-
const toArray = require('stream-to-array')
75
const defaultProxyHandler = (req, res, url, proxy, proxyOpts) => proxy(req, res, url, proxyOpts)
86
const DEFAULT_METHODS = require('restana/libs/methods').filter(method => method !== 'all')
97
const send = require('@polka/send-type')
10-
const TRANSFER_ENCODING_HEADER_NAME = 'transfer-encoding'
8+
const PROXY_TYPES = ['http', 'lambda']
119

1210
const gateway = (opts) => {
1311
opts = Object.assign({
@@ -37,6 +35,15 @@ const gateway = (opts) => {
3735
route.prefixRewrite = ''
3836
}
3937

38+
// retrieve proxy type
39+
const { proxyType = 'http' } = route
40+
if (!PROXY_TYPES.includes(proxyType)) {
41+
throw new Error('Unsupported proxy type, expecting one of ' + PROXY_TYPES.toString())
42+
}
43+
44+
// retrieve default hooks for proxy
45+
const { onRequestNoOp, onResponse } = require('./lib/default-hooks')[proxyType]
46+
4047
// populating required NOOPS
4148
route.hooks = route.hooks || {}
4249
route.hooks.onRequest = route.hooks.onRequest || onRequestNoOp
@@ -49,11 +56,7 @@ const gateway = (opts) => {
4956
route.pathRegex = undefined === route.pathRegex ? opts.pathRegex : String(route.pathRegex)
5057

5158
// instantiate route proxy
52-
const { proxy } = fastProxy({
53-
base: opts.targetOverride || route.target,
54-
http2: !!route.http2,
55-
...(opts.fastProxy)
56-
})
59+
const proxy = proxyFactory({ opts, route, proxyType })
5760

5861
// route proxy handler function
5962
const proxyHandler = route.proxyHandler || defaultProxyHandler
@@ -102,41 +105,4 @@ const handler = (route, proxy, proxyHandler) => async (req, res, next) => {
102105
}
103106
}
104107

105-
const onRequestNoOp = (req, res) => { }
106-
const onResponse = async (req, res, stream) => {
107-
const chunked = stream.headers[TRANSFER_ENCODING_HEADER_NAME]
108-
? stream.headers[TRANSFER_ENCODING_HEADER_NAME].endsWith('chunked')
109-
: false
110-
111-
if (req.headers.connection === 'close' && chunked) {
112-
try {
113-
// remove transfer-encoding header
114-
const transferEncoding = stream.headers[TRANSFER_ENCODING_HEADER_NAME].replace(/(,( )?)?chunked/, '')
115-
if (transferEncoding) {
116-
res.setHeader(TRANSFER_ENCODING_HEADER_NAME, transferEncoding)
117-
} else {
118-
res.removeHeader(TRANSFER_ENCODING_HEADER_NAME)
119-
}
120-
121-
if (!stream.headers['content-length']) {
122-
// pack all pieces into 1 buffer to calculate content length
123-
const resBuffer = Buffer.concat(await toArray(stream))
124-
125-
// add content-length header and send the merged response buffer
126-
res.setHeader('content-length', '' + Buffer.byteLength(resBuffer))
127-
res.statusCode = stream.statusCode
128-
res.end(resBuffer)
129-
130-
return
131-
}
132-
} catch (err) {
133-
res.statusCode = 500
134-
res.end(err.message)
135-
}
136-
}
137-
138-
res.statusCode = stream.statusCode
139-
pump(stream, res)
140-
}
141-
142108
module.exports = gateway

lib/default-hooks.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
'use strict'
2+
3+
const pump = require('pump')
4+
const toArray = require('stream-to-array')
5+
const TRANSFER_ENCODING_HEADER_NAME = 'transfer-encoding'
6+
7+
module.exports = {
8+
lambda: {
9+
onRequestNoOp (req, res) { },
10+
onResponse (req, res, response) {
11+
const { statusCode, body } = JSON.parse(response.Payload)
12+
13+
res.statusCode = statusCode
14+
res.end(body)
15+
}
16+
},
17+
http: {
18+
onRequestNoOp (req, res) { },
19+
async onResponse (req, res, stream) {
20+
const chunked = stream.headers[TRANSFER_ENCODING_HEADER_NAME]
21+
? stream.headers[TRANSFER_ENCODING_HEADER_NAME].endsWith('chunked')
22+
: false
23+
24+
if (req.headers.connection === 'close' && chunked) {
25+
try {
26+
// remove transfer-encoding header
27+
const transferEncoding = stream.headers[TRANSFER_ENCODING_HEADER_NAME].replace(/(,( )?)?chunked/, '')
28+
if (transferEncoding) {
29+
// header format includes many encodings, example: gzip, chunked
30+
res.setHeader(TRANSFER_ENCODING_HEADER_NAME, transferEncoding)
31+
} else {
32+
res.removeHeader(TRANSFER_ENCODING_HEADER_NAME)
33+
}
34+
35+
if (!stream.headers['content-length']) {
36+
// pack all pieces into 1 buffer to calculate content length
37+
const resBuffer = Buffer.concat(await toArray(stream))
38+
39+
// add content-length header and send the merged response buffer
40+
res.setHeader('content-length', '' + Buffer.byteLength(resBuffer))
41+
res.statusCode = stream.statusCode
42+
res.end(resBuffer)
43+
44+
return
45+
}
46+
} catch (err) {
47+
res.statusCode = 500
48+
res.end(err.message)
49+
}
50+
}
51+
52+
res.statusCode = stream.statusCode
53+
pump(stream, res)
54+
}
55+
}
56+
}

lib/proxy-factory.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
'use strict'
2+
const fastProxy = require('fast-proxy')
3+
const lambdaProxy = require('http-lambda-proxy')
4+
5+
module.exports = ({ proxyType, opts, route }) => {
6+
let proxy
7+
if (proxyType === 'http') {
8+
proxy = fastProxy({
9+
base: opts.targetOverride || route.target,
10+
http2: !!route.http2,
11+
...(opts.fastProxy)
12+
}).proxy
13+
} else if (proxyType === 'lambda') {
14+
proxy = lambdaProxy({
15+
target: opts.targetOverride || route.target,
16+
region: 'eu-central-1',
17+
...(route.lambdaProxy || {})
18+
})
19+
}
20+
21+
return proxy
22+
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"express-jwt": "^5.3.1",
4141
"express-rate-limit": "^5.1.1",
4242
"helmet": "^3.21.3",
43+
"http-lambda-proxy": "^1.0.1",
4344
"mocha": "^7.1.0",
4445
"nyc": "^15.0.0",
4546
"opossum": "^4.2.4",

test/config.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,17 @@ module.exports = async () => {
7777
prefixRewrite: '/endpoint-proxy-methods-put',
7878
target: 'http://localhost:3000',
7979
methods: ['PUT']
80+
}, {
81+
prefix: '/lambda',
82+
proxyType: 'lambda',
83+
target: 'a-lambda-function-name',
84+
hooks: {
85+
async onRequest (req, res) {
86+
res.end('Go Serverless!')
87+
88+
return true
89+
}
90+
}
8091
}]
8192
}
8293
}

0 commit comments

Comments
 (0)