I'm trying to set a timeout for incoming requests using a custom middleware, but no error is thrown, seems that the middleware is not called once setTimeout expires.
Here's the code I'm trying:
app.use(async (ctx, next) => {
setTimeout(() => {
if (!ctx.headersSent) {
ctx.throw(408, `processing time toke longer than ${config.PROCESSING_TIMEOUT}, request was timedout`);
}
}, config.PROCESSING_TIMEOUT);
await next();
});
Koa (or rather node's http server) has a built-in way to control the timeout of requests.
A global timeout can be set by setting server's timeout in ms
server.timeout = config.PROCESSING_TIMEOUT;
A timeout can be specified in a specific request for Koa as follows
ctx.request.socket.setTimeout(config.PROCESSING_TIMEOUT);
Related
I am working on a NODE JS project with Typescript started by other people and I need to trigger an action if the response from some endpoints is successful, so I need to intercept this response before it is sent to the entity that is making the request.
The request is made, first it goes through anothers middlewares, then it goes to the controller, when it finishes, I need to catch it.
The way the responses are returned from controllers is this
return response.status(200).json(data);
How can I solve this issue?
You can add some middleware BEFORE any of your request handlers that send a response that monitors the finish event on the response stream object to track when the request is done:
app.use((req, res, next) => {
// listen for when the request is done
res.on('finish', () => {
console.log(`request for ${req.url} finished with status ${res.statusCode}`);
});
next();
});
Here is my express code:
res.setHeader('Content-Type', 'application/json');
if (req.query.uuid) {
db.getPlayerByUUID(req.query.uuid, (data) => {
res.send(data)
})
} else if (req.query.name != null) {
db.getPlayerByName(req.query.name, (data) => {
res.send(data)
})
}
}).use(rateLimiter(req))
and here is my middleware:
const fs = require('fs')
const rateLimit = require('express-rate-limit')
const whitelist = JSON.parse(fs.readFileSync("./config/whitelist.json").toString())
const blacklist = JSON.parse(fs.readFileSync("./config/whitelist.json").toString())
const config = JSON.parse(fs.readFileSync("./config/config.json").toString())
const rateLimiter = (req) => {
const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
if (blacklist.indexOf(ip) <= -1) {
console.log(ip)
if (whitelist.indexOf(ip) <= -1) {
const rateLimiter = rateLimit({
windowMs: 60 * 1000,
max: config.webserver.limit,
message: {error: 'You have exceeded the requests in 1 min limit! Please try again soon.'},
headers: true,
})
}
} else {
res.status(403).send('You cannot use this service!');
}
}
module.exports = {
rateLimiter
}
So how could I either send my IP to my middleware or check the IP in my express code and use it only if the IP is not in my whitelist file? This issue is killing me :/.
Sorry if my question is dumb, I never really worked with express before.
It looks like your main problem is here:
}).use(rateLimiter(req))
You don't show exactly what comes before that, but if this is an app.use() or router.use(), then you need to fix how you pass your middleware to that to be like this:
app.use(rateLimiter)
This will pass the middleware function rateLimiter to app.use() so it can be called later. All middleware is called with three arguments (req, res, next) So, you need to modify your middleware to match that.
Then, you need to use those parameters appropriately in your middleware to accomplish your goal. Here's a possible implementation that should show you how this could work.
// create middleware from your rate limiting package to later conditionally apply
const limiter = rateLimit({
windowMs: 60 * 1000,
max: config.webserver.limit,
message: {error: 'You have exceeded the requests in 1 min limit! Please try again soon.'},
headers: true,
})
const rateLimiter = (req, res, next) => {
const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
// implement your ip address limiting here
// you should have one of three outcomes
// 1. You call next() to allow the routing to continue for this request
// 2. You call next(someError) to abort routing and fall-through to
// your Express error handler
// 3. You send a response using something like res.status(xxx).send(),
// thus ending the request
// block this request if the IP address is on the blacklist
if (blacklist.indexOf(ip) >= 0) {
res.status(403).send('You cannot use this service!');
return;
}
// if the IP address is on the whitelist, then let it go
if (whitelist.indexOf(ip) >= 0) {
// continue routing
next();
return;
}
// apply rate limiter middleware
limiter(req, res, next);
}
app.use(rateLimiter);
This creates a middleware called limiter here and a middleware rateLimiter. The rateLimiter middleware will be called on every request incoming request by virtue of the app.use(rateLimiter). Then, inside that it will check the blacklist and the whitelist. If it's on the blacklist, it will short-circuit the request and end it with a 403. If it's on the whitelist, it will continue routing and let subsequent route handlers process it. If it's not on either list, then it will conditionally apply rate limiting to it by manually calling the rate limiting middleware and passing it the (req, res, next) that were passed into your middleware.
Other comments on the questions you raise:
I need to apply middleware for everyone except a few IPs, but I can't get req and res outside of route, how could I apply it?
req and res only exist during the process of handling a given incoming request. So, once a request is over, those objects associated with that request are garbage collected and no longer available. Even if you managed to stuff them somewhere, that wouldn't do you much good because the socket associated with the res object has finished and is likely closed and you can't continue to send more data on it anyone.
So, you don't get access to req and res outside of processing a route or middleware. That's where they are available and useful.
So how could I either send my IP to my middleware or check the IP in my express code and use it only if the IP is not in my whitelist file? This issue is killing me
The IP address for the incoming request is available in the req object. Your middleware has access to that.
The usual way to do that is:
const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
More discussion of that here, but from your code, it looks like you already know that.
I have an Express server running for the backend of a website with Sentry (v5.15.5) successfully implemented. I'm now trying to improve the error handling on the backend, as at the moment if something goes wrong with the request, the request is not ended and the client sits there waiting for a response, which it never gets.
To tell the client if the request has failed I'm using the custom error handler in the documentation:
app.use(function onError(err, req, res, next) {
// The error id is attached to `res.sentry` to be returned
// and optionally displayed to the user for support.
res.statusCode = 500;
res.end(res.sentry + "\n");
});
However, when I use this on my server, only the custom error handler runs - Sentry never creates an event for the error, but if I just use another custom error function, they both get called fine, which makes me think this is a Sentry issue. Here's the relevant parts of the server code:
...
const Sentry = require('#sentry/node');
...
const app = express()
Sentry.init({ dsn: process.env.SENTRY });
...
// Middlewares
app.use(Sentry.Handlers.requestHandler());
app.use(express.json())
app.use(helmet())
app.use(cors())
app.use(morgan('tiny'))
const controllers = require('./controllers')
const wrap = fn => (...args) => Promise
.resolve(fn(...args))
.catch(args[2])
// Routes
...
app.post('/test', authUser, wrap(controllers.testController))
...
app.use(Sentry.Handlers.errorHandler());
app.use(function onError(err, req, res, next) {
res.statusCode = 500
res.end(res.sentry + "\n")
})
app.listen(PORT, () => console.log(`APP RUNNING ON PORT ${PORT}`))
The controllers on the server make database requests, etc. so they are async functions - that's why I use the wrap function to catch promise rejections and pass them to the error handler. If I unwrap the controller then Sentry works fine, but then the server never sends the error to the client.
I expect I'm probably going about this wrong as it should be pretty simple to do, but no matter what I do I cannot get Sentry + async controllers + custom error handler to work. Any help would be appreciated.
(This may be an Express issue, if so let me know and I'll take it over there)
Thanks
For some reason Sentry's 'Filter out localhost' option (which was turned off but somehow got toggled on) doesn't actually filter out all local errors. When I removed the custom error handler and wrap function, the errors managed to get past the localhost filter. After I turned it back off all the errors came through on Sentry fine.
I am using express and Connect Timeout Middleware to handle the timeouts.
It works great, but I the default node http server's timeout is set to two minutes.
Therefore if I want to set my timeout middleware to a value greater than two minutes, I also have to increase the http server timeout to be slightly bigger (otherwise my connect timeout handler is not called)
const app = express();
const http = new Http.Server(app);
http.setTimeout((4 * 60 * 1000) + 1); <-- Must set this
app.use(timeout('4m'));
How can I avoid this ? Am I missing something ?
If you want to use the connect-timeout middleware, you can't avoid it, since the middleware does not change the socket timeout, which defaults to 2 minutes.
There are two possible ways to avoid it, either using server.setTimeout() or request.setTimeout.
In case you only want to change the timeout to a few routes, and leave the default timeout to the rest, the recommended approach is to use: request.setTimeout
app.use('/some-routes', (req, res, next) => {
req.setTimeout((4 * 60 * 1000) + 1);
next();
}, timeout('4m'));
An alternative to setting the req.setTimeout to a value greater than the connect-timeout value, is dropping the connect-timeout middleware and using another work around, which is also not ideal.
You can check this old Node.js issue https://github.com/nodejs/node-v0.x-archive/issues/3460
function haltOnTimedout (req, res, next) {
if (!req.timedout) next()
}
app.use('/some-routes', (req, res, next) => {
req.setTimeout(4 * 60 * 1000); // No need to offset
req.socket.removeAllListeners('timeout'); // This is the work around
req.socket.once('timeout', () => {
req.timedout = true;
res.status(504).send('Timeout');
});
next();
});
app.use(haltOnTimedout);
// But if the timeout occurs in the middle of a route
// You will need to check if the headers were sent or if the request timedout
app.get('/some-routes', async(req, res, next) => {
// some async processing...
await asyncOperation();
if (!res.headersSent) // or !req.timedout
res.send('done');
});
PROBLEM
I've been looking for request/response timeouts for Express.js but everything seems to be related to the connection rather than the request/response itself.
If a request is taking a long time, it should be timed out. Obviously this shouldn't happen but even a simple mistake as having a route handler without a call to the callback or without res.send(), the browser will keep waiting for a reply forever.
An empty route handler is a perfect example of this.
app.get('/sessions/', function(req, res, callback){});
FIX
I added the following before app.use(app,router); and it seemed to add the timeout functionality. Does anyone have any experience/opinion on this?
app.use(function(req, res, next){
res.setTimeout(120000, function(){
console.log('Request has timed out.');
res.send(408);
});
next();
});
Note that I've set the timeout to 2 minutes.
There is already a Connect Middleware for Timeout support:
var timeout = express.timeout // express v3 and below
var timeout = require('connect-timeout'); //express v4
app.use(timeout(120000));
app.use(haltOnTimedout);
function haltOnTimedout(req, res, next){
if (!req.timedout) next();
}
If you plan on using the Timeout middleware as a top-level middleware like above, the haltOnTimedOut middleware needs to be the last middleware defined in the stack and is used for catching the timeout event. Thanks #Aichholzer for the update.
Side Note:
Keep in mind that if you roll your own timeout middleware, 4xx status codes are for client errors and 5xx are for server errors. 408s are reserved for when:
The client did not produce a request within the time that the server was prepared to wait. The client MAY repeat the request without modifications at any later time.
You don't need other npm modules to do this
var server = app.listen();
server.setTimeout(500000);
inspired by https://github.com/expressjs/express/issues/3330
or
app.use(function(req, res, next){
req.setTimeout(500000, function(){
// call back function is called when request timed out.
});
next();
});
An update if one is using Express 4.2 then the timeout middleware has been removed so need to manually add it with
npm install connect-timeout
and in the code it has to be (Edited as per comment, how to include it in the code)
var timeout = require('connect-timeout');
app.use(timeout('100s'));
In case you would like to use timeout middleware and exclude a specific route:
var timeout = require('connect-timeout');
app.use(timeout('5s')); //set 5s timeout for all requests
app.use('/my_route', function(req, res, next) {
req.clearTimeout(); // clear request timeout
req.setTimeout(20000); //set a 20s timeout for this request
next();
}).get('/my_route', function(req, res) {
//do something that takes a long time
});
If you need to test your api, this solotion can you help.
I used this in middleware to test my frontend.
For exmaple: if you need to test loader in frontend.
const router = require('express').Router();
const { data } = require('./data');
router.get('/api/data', (req, res, next) => {
setTimeout(() => {
res.set('Content-Type', 'application/json')
res.status(200).send(data)
next()
}, 2000)
})
module.exports = router;
request.setTimeout(< time in milliseconds >) does the job
https://nodejs.org/api/http.html#http_request_settimeout_timeout_callback
You can try:
return await new Promise((resolve) =>
setTimeout(() => {
resolve(resp);
}, 3000),
);
In above code, 3000 = 3 sec.
Change it according to your requirement.
I have not tried for very long scenarios though. Let me know the results in comments.
Before you set your routes, add the code:
app.all('*', function(req, res, next) {
setTimeout(function() {
next();
}, 120000); // 120 seconds
});