Losing track of the request context
Languages like Java, C++ are multi-threaded which can have access to request contexts in any part of the thread execution flow or request lifecycle. Whereas, Javascript follows a single-threaded functional flow by default and our required arguments need to be passed through async functions until the request-response lifecycle has been completed. But there could be instances where we lose track of data from the request context that is hitting the application.
We pass headers, query params and body on a request which can be highly crucial. Say we introduce a header tracking-token
which tracks the HTTP calls in a microservice architecture. This being sent with a request header hitting from a service, we have access to the header in the route file as well as multiple middleware. But there are service layer functions handling heavy logic to get the desired result. Under this scenario, we may have multiple service layer functions being called. Also, we would've introduced more number of routes and more number of functions to handle the routes. At this point of time, it would be cumbersome to pass the request
as arguments to each callback function in the entire asynchronous cycle to make use of the header named tracking-token
.
// routes.js
app.get('/get-users, () => {
const userIds = req.users.map(user => user.id);
await getUsers(userIds);
});
//users.js
const getUsers = async (userIds) => {
await db.findAllByIds(userIds);
}
In the code above we have lost track of the req
in the getUsers
function. Now, the only way to pass req.get('tracking-token')
is as a parameter to getUsers
function. Imagine we have tens and hundreds of such functions without access to request context and we may need to access it everywhere. Got into a mess right?!
AsyncLocalStorage
has got us covered!!
AsyncLocalStorage to the rescue
There are two ways to approach this. There are projects which use the latest stable version of Node.js and can embrace this and have a go ahead with AsyncLocalStorage
. But there are applications where upgrading to a newer node version is not an easy step. We'll figure out the former first.
AsyncLocalStorage
is a class which uses node:async-hooks
which is stable after Node version 16. We can add a middleware with AsyncLocalStorage
store which would hold the request context.
//storeRequestMiddleWare.js
const { AsyncLocalStorage } = require('async_hooks');
const asyncLocalStorage = new AsyncLocalStorage();
const storeRequestMiddleware = (req, res, next) => {
asyncLocalStorage.run(new Map(), () => {
asyncLocalStorage.getStore().set('context',
req.get('tracing-token'));
next();
});
};
module.exports = {
storeRequestMiddleware,
asyncLocalStorage,
};
//app.js
app.use(storeRequestMiddleware)
With the storeRequestMiddleware
, the tracing-token
from the request header is now held in the store initialised by AsyncLocalStorage
.
Now, we can fetch the tracing-token
from the store in any part of the application.
//random.js
const traceId = asyncLocalStorage.getStore().get('traceId');
Note:
AsyncLocalStorage
usesasync-hooks
which is still in experimental state. Make sure to use the dependency with caution for production-ready applications.
For older node versions
As for projects using older versions of node, we have the node-continuation-local-storage
package which solves the same problem but has it's own way of implementation as written below.
import { createNamespace } from 'continuation-local-storage';
import { NextFunction, Request, Response} from "express";
export const requestStore = createNamespace('token-store');
export const tracingTokenMiddleware = (req: Request, res: Response, next: NextFunction) => {
requestStore.run(() => {
requestStore.set('tracing-token', req.get('tracing-
token'));
next();
});
};
Drawbacks of continuation-local-storage
The package makes use of async-listeners
which backfires in case of concurrent HTTP calls. We may lose the request context when multiple HTTP calls are made to another service.
It depends on the version of node and the requirement we need to meet when implementing these dependencies. Hope we don't lose track of the required parameters from the request context anymore!
Top comments (0)