node.jsexpress

Without modifying every route in my app, how can I obtain a list of all the routes that have been matched so far?


Some tangential questions have been asked already. but this seems different - I am asking for a way to nicely instrument Express 5.0, not predict matched routes.

Related: How can I find the originally matched route at the time of a request in Express.Js

What I would like to do is instrument express in such a way that for every route handler that is matched, the matching string pattern is added to an array of handlers for the given request, so that a later handler can see the "path" a request took through the app. I really don't want to have to go modify every route in my app to instrument itself with req.route.

For example, what is an implementation of onRouteCalled where, given this code:

onRouteCalled(app, (req, route) => {
   req.myMatchedRoutes ??= [];
   req.myMatchedRoutes.push(route);
});

app.use(function logPath(req, res, next) {
    res.on('close', () => {
        // In the close event, req.route is available for the last route, but doesn't seem to include the information that would give me a full path. E.g. it only patches up to the last Router().
        console.log(req.myMatchedRoutes);
    });
    next();
});

const router = Router();
router.use('/bacon/:tastiness', (req, res, next) => next());
router.use('/:food/:tastiness', (req, res, next) => {
   res.sendStatus(204);
});

app.use(router);

A request like GET /bacon/delicious would log ['/bacon/:tastiness', '/:food/:tastiness'] to the console?


Solution

  • Express doesn't store this kind of data because it doesn't need it. This would require to monkey-patch express.Route.prototype methods to wrap route handler function with yours that does the extra job. This is the relevant implementation of express.Route in Express 5 for reference.

    Patching all request handlers would require more work, but for route handlers it would be something like:

    const { METHODS } = require('node:http');
    
    const httpMethods = METHODS.map((method) => method.toLowerCase());
    const routeMethods = [...methods, 'all'];
    const originalMethods = {};
    
    for (const method of routeMethods) {
        originalMethods[method] = express.Route.prototype[method];
        express.Route.prototype[method] = function (...args) {
            const { path } = this;
    
            for (let i = 0; i < args.length; i++) {
                const handler = args[i];
    
                if (typeof handler === 'function' && handler.name !== 'patchedHandler') {
                    args[i] = function patchedHandler(req, res, next) {
                        req.myMatchedRoutes ??= []; 
                        req.myMatchedRoutes.push(path); // also req.route.path
    
                        return handler.call(this, req, res, next);
                    }
                }
            }
    
            return originalMethods[method].apply(this, args);
        };
    }
    

    This currently doesn't support this kind of use, app.get('/example/c', [cb0, cb1, cb2]).

    Alternatively, this could be done by patching Route.prototype.dispatch.