Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ service.start(3000)
middlewares: [],
// Optional global value for routes "pathRegex". Default value: '/*'
pathRegex: '/*',
// Optional global requests timeout value (given in milliseconds). Default value: '0' (DISABLED)
timeout: 0,
// Optional "target" value that overrides the routes "target" config value. Feature intended for testing purposes.
targetOverride: "https://yourdev.api-gateway.com",

Expand All @@ -50,11 +52,15 @@ service.start(3000)
// Optional `fast-proxy` library configuration (https://www.npmjs.com/package/fast-proxy#options)
// base parameter defined as the route target. Default value: {}
fastProxy: {},
// Optional proxy handler function. Default value: (req, res, url, proxy, proxyOpts) => proxy(req, res, url, proxyOpts)
proxyHandler: () => {}
// Optional flag to indicate if target uses the HTTP2 protocol. Default value: false
http2: false,
// Optional path matching regex. Default value: '/*'
// In order to disable the 'pathRegex' at all, you can use an empty string: ''
pathRegex: '/*',
// Optional service requests timeout value (given in milliseconds). Default value: '0' (DISABLED)
timeout: 0,
// route prefix
prefix: '/public',
// Optional documentation configuration (unrestricted schema)
Expand Down Expand Up @@ -133,6 +139,19 @@ Example output:
```
> NOTE: Please see `docs` configuration entry explained above.

## Timeouts and Unavailability
We can restrict requests timeouts globally, at service level using the `timeout` configuration.
To define an endpoint specific timeout, you can use the property `timeout` of the request object, normally inside a middleware:
```js
req.timeout = 500 // define a 500ms timeout on a custom request.
```
> NOTE: You might want to also check https://www.npmjs.com/package/middleware-if-unless

### Circuit Breaker
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.

Please see the `demos/circuitbreaker.js` example for more details using the `opossum` library.

## Gateway level caching
Caching support is provided by the `http-cache-middleware` module. https://www.npmjs.com/package/http-cache-middleware

Expand Down
59 changes: 59 additions & 0 deletions demos/circuitbreaker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
const gateway = require('../index')
const PORT = process.env.PORT || 8080
const onEnd = require('on-http-end')
const CircuitBreaker = require('opossum')

const REQUEST_TIMEOUT = 1.5 * 1000

const options = {
timeout: REQUEST_TIMEOUT - 200, // If our function takes longer than "timeout", trigger a failure
errorThresholdPercentage: 50, // When 50% of requests fail, trip the circuit
resetTimeout: 30 * 1000 // After 30 seconds, try again.
}
const breaker = new CircuitBreaker(([req, res, url, proxy, proxyOpts]) => {
return new Promise((resolve, reject) => {
proxy(req, res, url, proxyOpts)
onEnd(res, () => {
// you can optionally evaluate response codes here...
resolve()
})
})
}, options)

breaker.fallback(([req, res], err) => {
if (err.code === 'EOPENBREAKER') {
res.send({
message: 'Upps, looks like "public" service is down. Please try again in 30 seconds!'
}, 503)
}
})

gateway({
routes: [{
timeout: REQUEST_TIMEOUT,
proxyHandler: (...params) => breaker.fire(params),
prefix: '/public',
target: 'http://localhost:3000',
docs: {
name: 'Public Service',
endpoint: 'swagger.json',
type: 'swagger'
}
}]
}).start(PORT).then(() => {
console.log(`API Gateway listening on ${PORT} port!`)
})

const service = require('restana')({})
service.get('/longop', (req, res) => {
setTimeout(() => {
res.send('This operation will trigger the breaker failure counter...')
}, 2000)
})
service.get('/hi', (req, res) => {
res.send('Hello World!')
})

service.start(3000).then(() => {
console.log('Public service listening on 3000 port!')
})
19 changes: 16 additions & 3 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const fastProxy = require('fast-proxy')
const restana = require('restana')
const pump = require('pump')
const toArray = require('stream-to-array')
const defaultProxyHandler = (req, res, url, proxy, proxyOpts) => proxy(req, res, url, proxyOpts)

const gateway = (opts) => {
opts = Object.assign({
Expand Down Expand Up @@ -48,19 +49,31 @@ const gateway = (opts) => {
...(opts.fastProxy)
})

// route proxy handler function
const proxyHandler = route.proxyHandler || defaultProxyHandler

// populating timeout config
route.timeout = route.timeout || opts.timeout

// registering route handler
const methods = route.methods || ['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT', 'OPTIONS']
server.route(methods, route.prefix + route.pathRegex, handler(route, proxy), null, route.middlewares)
server.route(methods, route.prefix + route.pathRegex, handler(route, proxy, proxyHandler), null, route.middlewares)
})

return server
}

const handler = (route, proxy) => async (req, res) => {
const handler = (route, proxy, proxyHandler) => async (req, res) => {
req.url = req.url.replace(route.prefix, route.prefixRewrite)
const shouldAbortProxy = await route.hooks.onRequest(req, res)
if (!shouldAbortProxy) {
proxy(req, res, req.url, Object.assign({}, route.hooks))
const proxyOpts = Object.assign({
request: {
timeout: req.timeout || route.timeout
}
}, route.hooks)

proxyHandler(req, res, req.url, proxy, proxyOpts)
}
}

Expand Down
34 changes: 20 additions & 14 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "fast-gateway",
"version": "1.3.9",
"version": "1.4.0",
"description": "A Node.js API Gateway for the masses!",
"main": "index.js",
"scripts": {
Expand All @@ -26,7 +26,7 @@
},
"homepage": "https://github.com/jkyberneees/fast-gateway#readme",
"dependencies": {
"fast-proxy": "^1.2.0",
"fast-proxy": "^1.3.0",
"http-cache-middleware": "^1.2.3",
"restana": "^3.3.3",
"stream-to-array": "^2.3.0"
Expand All @@ -38,6 +38,7 @@
"helmet": "^3.21.2",
"mocha": "^6.2.2",
"nyc": "^14.1.1",
"opossum": "^4.2.1",
"response-time": "^2.3.2",
"standard": "^14.3.1",
"supertest": "^4.0.2"
Expand Down
2 changes: 2 additions & 0 deletions test/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ const pump = require('pump')

module.exports = async () => {
return {
timeout: 1.5 * 1000,

middlewares: [
require('cors')(),
require('http-cache-middleware')()
Expand Down
11 changes: 11 additions & 0 deletions test/smoke.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ describe('API Gateway', () => {
res.setHeader('x-cache-expire', 'GET/users/*')
res.send({})
})
remote.get('/longop', (req, res) => {
setTimeout(() => {
res.send({})
}, 2000)
})
remote.post('/204', (req, res) => res.send(204))
remote.get('/endpoint-proxy-methods', (req, res) => res.send({
name: 'endpoint-proxy-methods'
Expand Down Expand Up @@ -155,6 +160,12 @@ describe('API Gateway', () => {
})
})

it('Should timeout on GET /longop - 504', async () => {
return request(gateway)
.get('/users/longop')
.expect(504)
})

it('GET /users/info - 200', async () => {
await request(gateway)
.get('/users/info')
Expand Down