Skip to main content

Hacking express middleware to automatically capture request timeouts

With any server, you need to ensure that you don’t have run-away requests that end up getting stuck in an infinite process, held in memory for long periods of time. Typically, the requester will abandon the request if it is taking too long, perceptually, to respond.

Express in a node.js server doesn’t handle this by itself. Instead you need to add something like the connect-timeout middleware. However, the default example for this leaves you open to forgetting to add the timeout check within your middleware chain.

import express from 'express';
import timeout from 'connect-timeout';
 
function haltOnTimedout(req, res, next) {
	if (!req.timedout) next();
}
 
const app = express()
	.use(timeout(5_000))
	.use(bodyParser())
	.use(haltOnTimedout)
	.use(cookieParser())
	.use(haltOnTimedout)
	.use(myMiddleware())
	.use(haltOnTimedout)
	.use(handleRequest());

Notice that you need to manually add the haltOnTimedout middleware after each other middleware in the chain. This may be fine and dandy for a small personal project that you, as the sole developer, know all about. However, as you start distributing knowledge to a larger team, it’s imperative that this is easy and automatic. There are few, if any, cases where you would want to opt-out of the behavior of timing out a request if it’s taking too long.

So, here’s a pattern that I’ve used a number of times for various purposes – proxying the Express application and wrapping the use() function for some automatic behavior. It’s as simple as wrapping the initial express application (or the application after any setup):

const app = proxyTimeoutMiddleware(express().use(timeout(5_000)))
	.use(bodyParser())
	.use(cookieParser())
	.use(myMiddleware())
	.use(handleRequest());

How does it work?

We create a Proxy that wraps the use() function. This is fairly basic setup to pull out the middleware that is added to express and wrap it with another function:

function proxyTimeoutMiddleware<T extends Application | Router>(app: T) {
	return new Proxy(app, {
		get(this: T, target, property, receiver) {
			// If the target property is not express's `.use()` function, pass through to the default behavior
			if (property !== 'use') {
				return Reflect.get(target, property, receiver);
			}
 
			// Otherwise, return a new function that wraps the first argument, given that it is a function
			// with our timeout wrapper
			return function useWithTimeout(...args) {
				const wrappedArgs = args.map((arg) => {
					if (typeof arg === 'function') {
						return wrapMiddlewareWithTimeout(arg);
					}
					return arg;
				});
				return Reflect.get(target, property, receiver).apply(this, wrappedArgs);
			};
		},
	});
}

Then, our wrapping function returns a new middleware function that checks first if the request is timed out (req.timedout is added via the connect-timeout middleware that we previously added) and returns. Upon early return, not calling req.end(), req.send() or next(), express will return the default 500 response.

Then as well, we wrap the next() function passed to our actual middleware and check if the request timedout after running the middleware and again, if so, early return. Otherwise we call the next() function and continue on.

function wrapMiddlewareWithTimeout(middleware: (req: Request, res: Response, next: NextFunction) => void) {
	return function timeoutWrappedMiddleware(req: Request, res: Response, next: NextFunction) {
		const { name } = middleware;
		if (req.timedout) {
			// logger.error(`Request timed out before middleware "${name}".`);
			return;
		}
 
		middleware(req, res, function wrappedNext(...args) {
			if (req.timedout) {
				// logger.error(`Request timed out after middleware "${name}".`);
				return;
			}
 
			next.apply(this, args);
		});
	};
}

That’s it! Once this is set up, no one will ever have to think about adding the timeout halting middleware in the future.

Other uses

This same pattern can be used for similar before/after actions on middleware. You may have noticed a couple commented out logging lines in the timeoutWrappedMiddleware example previously. We could also do something similar to record the amount of time each middleware is taking in order to help us track down problem code that could be optimized:

const NANOSECONDS_PER_MILLISECOND = 1e6;
const MILLISECONDS_PER_SECOND = 1e3;
 
function wrapMiddlewareWithMetrics(middleware: (req: Request, res: Response, next: NextFunction) => void) {
	return function metricsWrappedMiddleware(req: Request, res: Response, next: NextFunction) {
		const { name } = middleware;
		const [seconds, nseconds] = hrtime();
		// Get the high resolution time in milliseconds
		const start = seconds * MILLISECONDS_PER_SECOND + nseconds / NANOSECONDS_PER_MILLISECOND;
 
		middleware(req, res, function wrappedNext(...args) {
			const [seconds, nseconds] = hrtime();
			const end = seconds * MILLISECONDS_PER_SECOND + nseconds / NANOSECONDS_PER_MILLISECOND;
			myMetrics.float({ metric: `middleware/${name || 'unknown'}`, value: end - start });
 
			next.apply(this, args);
		});
	};
}