Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I wrap every every express route handler with another function

Basically I want to instead of this...

app.get(routes.test, function(req, res, next){
  actualRouteHandler(req, res, next) // Always returns a promise or throws.
    .catch(function(err) {
      next(err);
    });
});

Have this: app.get(routes.test, catchWrap(actualRouteHandler));

Or something similar, I've tried messing around with fn.apply and things but I can't find a way to pass actualRouteHandler the correct parameters (req,res,next) and still have the function. Do I need to return a function or something similar?

Edit: I think there may be libraries that do this but we have no access to the actual express application in this bit of code.

like image 508
Mark McDermid Avatar asked Dec 14 '22 05:12

Mark McDermid


1 Answers

In your specific case, catchWrap would look like this:

function catchWrap(originalFunction) {
    return function(req, res, next) {
        try {
            return originalFunction.call(this, req, res, next);
        } catch (e) {
            next(e);
        }
    };
}

That returns a new function that, when called, will call the original with your catch wrapper around it. The key parts are that it creates and returns a function (return function(req, res, next) { ... };) and this line:

return originalFunction.call(this, req, res, next);

Function#call calls the given function saying what to use as this during the call (in the above we pass on the this we recieved) and the arguments to use in the call.

You'd use it as you showed:

app.get(routes.test, catchWrap(actualRouteHandler));

Or if you prefer to define the actual handler as an anonymous function:

app.get(routes.test, catchWrap(function(req, res, next) {
    // ...handler code here...
}));

That catchWrap is specific to your situation, because you want to call next(e) with the exception if thrown. The generic form of "wrap this function in another function" is like this:

function catchWrap(originalFunction) {
    return function() {
        try {
            // You can do stuff here before calling the original...
            // Now we call the original:
            var retVal = originalFunction.apply(this, arguments);
            // You can do stuff here after calling the original...
            // And we're done
            return retVal;
        } catch (e) {
            // you can do something here if you like, then:
            throw e; // Or, of course, handle it
        }
    };
}

arguments is a pseudo-array provided by JavaScript that includes all of the arguments that the current function was called with. Function#apply is just like Function#call, except you give the arguments to use as an array (or pseudo-array) rather than discretely.


Taking it a step further: You could create a function you passed a router to that returned an updated router that auto-wrapped the handlers when you called get, post, etc. Something along these lines (untested):

const routeCalls = ["all", "get", "post", "put", "delete", /*...*/];
function patchWithCatcher(router) {
    for (const call of routeCalls) {
        const original = router[call];
        // This seemingly-pointless object is to ensure that the new function
        // has a *name*; I don't know whether Express relies on the name of
        // the function you call (some libs with similar APIs do).
        const rep = {
            [call]: function (path, ...callbacks) {
                return router[call](path, ...callbacks.map(catchWrap));
            },
        };
        router[call] = rep[call];
    }
    return router; // Just for convenience
}

Using it:

const app = patchWithCatcher(express());
app.get(/*...*/);
app.post(/*...*/);
// ...

or

const router = patchWithCatcher(express.Router(/*...options...*/));
router.get(/*...*/);
router.post(/*...*/);
// ...

That monkey-patches the router you pass in. I considered using the passed-in router as the prototype of a new object in order to avoid modifying the router object passed in, but you'd have to test that really, really thoroughly, there are lots of edge cases around this during calls. Another viable alternative would be to use a Proxy, though at a runtime cost.

like image 175
T.J. Crowder Avatar answered Dec 31 '22 03:12

T.J. Crowder