Skip to content

Commit 0b12d4f

Browse files
authored
Merge pull request #15 from jkyberneees/adding-timeout-and-proxyHandler-configs
Adding timeout and proxy handler configs
2 parents 31d9bef + 1447b70 commit 0b12d4f

File tree

7 files changed

+130
-19
lines changed

7 files changed

+130
-19
lines changed

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ service.start(3000)
4242
middlewares: [],
4343
// Optional global value for routes "pathRegex". Default value: '/*'
4444
pathRegex: '/*',
45+
// Optional global requests timeout value (given in milliseconds). Default value: '0' (DISABLED)
46+
timeout: 0,
4547
// Optional "target" value that overrides the routes "target" config value. Feature intended for testing purposes.
4648
targetOverride: "https://yourdev.api-gateway.com",
4749

@@ -50,11 +52,15 @@ service.start(3000)
5052
// Optional `fast-proxy` library configuration (https://www.npmjs.com/package/fast-proxy#options)
5153
// base parameter defined as the route target. Default value: {}
5254
fastProxy: {},
55+
// Optional proxy handler function. Default value: (req, res, url, proxy, proxyOpts) => proxy(req, res, url, proxyOpts)
56+
proxyHandler: () => {}
5357
// Optional flag to indicate if target uses the HTTP2 protocol. Default value: false
5458
http2: false,
5559
// Optional path matching regex. Default value: '/*'
5660
// In order to disable the 'pathRegex' at all, you can use an empty string: ''
5761
pathRegex: '/*',
62+
// Optional service requests timeout value (given in milliseconds). Default value: '0' (DISABLED)
63+
timeout: 0,
5864
// route prefix
5965
prefix: '/public',
6066
// Optional documentation configuration (unrestricted schema)
@@ -133,6 +139,19 @@ Example output:
133139
```
134140
> NOTE: Please see `docs` configuration entry explained above.
135141
142+
## Timeouts and Unavailability
143+
We can restrict requests timeouts globally, at service level using the `timeout` configuration.
144+
To define an endpoint specific timeout, you can use the property `timeout` of the request object, normally inside a middleware:
145+
```js
146+
req.timeout = 500 // define a 500ms timeout on a custom request.
147+
```
148+
> NOTE: You might want to also check https://www.npmjs.com/package/middleware-if-unless
149+
150+
### Circuit Breaker
151+
By using the `proxyHandler` hook, developers can optionally intercept and modify the default gateway routing behavior right before the origin request is proxied to the remote service. Therefore, connecting advanced monitoring mechanisms like [Circuit Breakers](https://martinfowler.com/bliki/CircuitBreaker.html) is rather simple.
152+
153+
Please see the `demos/circuitbreaker.js` example for more details using the `opossum` library.
154+
136155
## Gateway level caching
137156
Caching support is provided by the `http-cache-middleware` module. https://www.npmjs.com/package/http-cache-middleware
138157

demos/circuitbreaker.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
const gateway = require('../index')
2+
const PORT = process.env.PORT || 8080
3+
const onEnd = require('on-http-end')
4+
const CircuitBreaker = require('opossum')
5+
6+
const REQUEST_TIMEOUT = 1.5 * 1000
7+
8+
const options = {
9+
timeout: REQUEST_TIMEOUT - 200, // If our function takes longer than "timeout", trigger a failure
10+
errorThresholdPercentage: 50, // When 50% of requests fail, trip the circuit
11+
resetTimeout: 30 * 1000 // After 30 seconds, try again.
12+
}
13+
const breaker = new CircuitBreaker(([req, res, url, proxy, proxyOpts]) => {
14+
return new Promise((resolve, reject) => {
15+
proxy(req, res, url, proxyOpts)
16+
onEnd(res, () => {
17+
// you can optionally evaluate response codes here...
18+
resolve()
19+
})
20+
})
21+
}, options)
22+
23+
breaker.fallback(([req, res], err) => {
24+
if (err.code === 'EOPENBREAKER') {
25+
res.send({
26+
message: 'Upps, looks like "public" service is down. Please try again in 30 seconds!'
27+
}, 503)
28+
}
29+
})
30+
31+
gateway({
32+
routes: [{
33+
timeout: REQUEST_TIMEOUT,
34+
proxyHandler: (...params) => breaker.fire(params),
35+
prefix: '/public',
36+
target: 'http://localhost:3000',
37+
docs: {
38+
name: 'Public Service',
39+
endpoint: 'swagger.json',
40+
type: 'swagger'
41+
}
42+
}]
43+
}).start(PORT).then(() => {
44+
console.log(`API Gateway listening on ${PORT} port!`)
45+
})
46+
47+
const service = require('restana')({})
48+
service.get('/longop', (req, res) => {
49+
setTimeout(() => {
50+
res.send('This operation will trigger the breaker failure counter...')
51+
}, 2000)
52+
})
53+
service.get('/hi', (req, res) => {
54+
res.send('Hello World!')
55+
})
56+
57+
service.start(3000).then(() => {
58+
console.log('Public service listening on 3000 port!')
59+
})

index.js

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const fastProxy = require('fast-proxy')
22
const restana = require('restana')
33
const pump = require('pump')
44
const toArray = require('stream-to-array')
5+
const defaultProxyHandler = (req, res, url, proxy, proxyOpts) => proxy(req, res, url, proxyOpts)
56

67
const gateway = (opts) => {
78
opts = Object.assign({
@@ -48,19 +49,31 @@ const gateway = (opts) => {
4849
...(opts.fastProxy)
4950
})
5051

52+
// route proxy handler function
53+
const proxyHandler = route.proxyHandler || defaultProxyHandler
54+
55+
// populating timeout config
56+
route.timeout = route.timeout || opts.timeout
57+
5158
// registering route handler
5259
const methods = route.methods || ['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT', 'OPTIONS']
53-
server.route(methods, route.prefix + route.pathRegex, handler(route, proxy), null, route.middlewares)
60+
server.route(methods, route.prefix + route.pathRegex, handler(route, proxy, proxyHandler), null, route.middlewares)
5461
})
5562

5663
return server
5764
}
5865

59-
const handler = (route, proxy) => async (req, res) => {
66+
const handler = (route, proxy, proxyHandler) => async (req, res) => {
6067
req.url = req.url.replace(route.prefix, route.prefixRewrite)
6168
const shouldAbortProxy = await route.hooks.onRequest(req, res)
6269
if (!shouldAbortProxy) {
63-
proxy(req, res, req.url, Object.assign({}, route.hooks))
70+
const proxyOpts = Object.assign({
71+
request: {
72+
timeout: req.timeout || route.timeout
73+
}
74+
}, route.hooks)
75+
76+
proxyHandler(req, res, req.url, proxy, proxyOpts)
6477
}
6578
}
6679

package-lock.json

Lines changed: 20 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "fast-gateway",
3-
"version": "1.3.9",
3+
"version": "1.4.0",
44
"description": "A Node.js API Gateway for the masses!",
55
"main": "index.js",
66
"scripts": {
@@ -26,7 +26,7 @@
2626
},
2727
"homepage": "https://github.com/jkyberneees/fast-gateway#readme",
2828
"dependencies": {
29-
"fast-proxy": "^1.2.0",
29+
"fast-proxy": "^1.3.0",
3030
"http-cache-middleware": "^1.2.3",
3131
"restana": "^3.3.3",
3232
"stream-to-array": "^2.3.0"
@@ -38,6 +38,7 @@
3838
"helmet": "^3.21.2",
3939
"mocha": "^6.2.2",
4040
"nyc": "^14.1.1",
41+
"opossum": "^4.2.1",
4142
"response-time": "^2.3.2",
4243
"standard": "^14.3.1",
4344
"supertest": "^4.0.2"

test/config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ const pump = require('pump')
22

33
module.exports = async () => {
44
return {
5+
timeout: 1.5 * 1000,
6+
57
middlewares: [
68
require('cors')(),
79
require('http-cache-middleware')()

test/smoke.test.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ describe('API Gateway', () => {
3333
res.setHeader('x-cache-expire', 'GET/users/*')
3434
res.send({})
3535
})
36+
remote.get('/longop', (req, res) => {
37+
setTimeout(() => {
38+
res.send({})
39+
}, 2000)
40+
})
3641
remote.post('/204', (req, res) => res.send(204))
3742
remote.get('/endpoint-proxy-methods', (req, res) => res.send({
3843
name: 'endpoint-proxy-methods'
@@ -155,6 +160,12 @@ describe('API Gateway', () => {
155160
})
156161
})
157162

163+
it('Should timeout on GET /longop - 504', async () => {
164+
return request(gateway)
165+
.get('/users/longop')
166+
.expect(504)
167+
})
168+
158169
it('GET /users/info - 200', async () => {
159170
await request(gateway)
160171
.get('/users/info')

0 commit comments

Comments
 (0)