Node.js / Express.js - How to override/intercept res.render function?
Asked Answered
S

4

25

I'm building a Node.js app with Connect/Express.js and I want to intercept the res.render(view, option) function to run some code before forwarding it on to the original render function.

app.get('/someUrl', function(req, res) {
    
    res.render = function(view, options, callback) {
        view = 'testViews/' + view;
        res.prototype.render(view, options, callback);
    };

    res.render('index', { title: 'Hello world' });
});

It looks like a contrived example, but it does fit in an overall framework I'm building.

My knowledge of OOP and Prototypal inheritance on JavaScript is a bit weak. How would I do something like this?

After some experimentation I came up with the following:

app.get('/someUrl', function(req, res) {

    var response = {};
    
    response.prototype = res;

    response.render = function(view, opts, fn, parent, sub){
        view = 'testViews/' + view;
        this.prototype.render(view, opts, fn, parent, sub);
    };

    response.render('index', { title: 'Hello world' });
});

It seems to work. Not sure if it's the best solution as I'm creating a new response wrapper object for each request, would that be a problem?

Sordello answered 14/2, 2012 at 23:38 Comment(2)
That is normally how you do it. If you put that in a middleware that is used in before the router you can set it up in a single place.Mandolin
I thought of using middleware. Not sure if I want to override the response behaviour for the entire application, only for requests that are routed through my framework.Sordello
T
30

Old question, but found myself asking the same thing. How to intercept res render? Using express 4.0x something now.

You can use/write middleware. The concept was a bit daunting to me at first, but after some reading it made a little more sense. And just for some context for anyone else reading this, the motivation for overriding res.render was to provide global view variables. I want session to be available in all my templates without me having to type it in every res object.

The basic middleware format is.

app.use( function( req, res, next ) {
    //....
    next();
} );

The next param and function call are crucial to execution. next is the callback function, to allow multiple middleware to do their thing without blocking. For a better explanation read here

This can then be used to override render logic

app.use( function( req, res, next ) {
    // grab reference of render
    var _render = res.render;
    // override logic
    res.render = function( view, options, fn ) {
        // do some custom logic
        _.extend( options, {session: true} );
        // continue with original render
        _render.call( this, view, options, fn );
    }
    next();
} );

I've tested this code, using express 3.0.6. It should work with 4.x without issue. You can also override specific URLs combos with

app.use( '/myspcificurl', function( req, res, next ) {...} );
Tavis answered 5/6, 2014 at 3:50 Comment(5)
Does this override the function on every request, putting pressure on the GC? Would it make more sense to override the prototype?Spelldown
This overrides on every request. It will override the prototype via property shadowing. Can override the prototype if you want to, but generally you want properties on the main object not deep in the prototype chain. The property look up time is faster this way.Tavis
I didn't get this answer at first, but after fiddling around myself a lot, now I do get it. You can checkout a variation of Lex his code on github.com/mettamage/renderExtMiddleware -- I don't use _.extend(args). My code renders a normal view except when the client is not a browser, then it returns json.Footwear
Yeah could describe it better. Your example is using the default render function, I didn't know it would return json when different headers. The above example is defining a new function to handle rendering. And _.extend is totally optional, I was using the underscore library to extend an object.Tavis
+1 for mentioning underscore, that made the answer a bit more understandable -- I'm new(ish) to node.js.Footwear
K
16

It's not a good idea to use a middleware to override a response or request method for each instance of them because the middleware is get executed for each and every request and each time it get called you use cpu as well as memory because you are creating a new function.

As you may know javascript is a Prototype-based language and every object has a prototype, like response and request objects. By looking at code (express 4.13.4) you can find their prototypes:

req => express.request
res => express.response

So when you want to override a method for every single instance of a response it's much much better to override it in it's prototype as it's done once is available in every instance of response:

var app = (global.express = require('express'))();
var render = express.response.render;
express.response.render = function(view, options, callback) {
    // desired code
    /** here this refer to the current res instance and you can even access req for this res: **/
    console.log(this.req);
    render.apply(this, arguments);
};
Kayceekaye answered 30/9, 2015 at 19:23 Comment(2)
I don't know why this comment is not the accepted one! it is the absolute right answer!.Weldon
overriding functions this way is possible but considered code pollution. regardless if the logic is called from a middleware or by overriding the render function they are almost identical performance-wise, 1 extra function call in the middleware should never be taken into consideration when evaluation implementations for a web server. there are way more bottlenecks on the network layer than an extra function call. though this has worst performance than using middleware due to the render variable and the .apply function call at the end...Homology
D
8

The response object doesn't have a prototype. This should work (taking Ryan's idea of putting it in a middleware):

var wrapRender = function(req, res, next) {
  var _render = res.render;
  res.render = function(view, options, callback) {
    _render.call(res, "testViews/" + view, options, callback);
  };
};

However, it might be better to hack the ServerResponse.prototype:

var express = require("express")
  , http = require("http")
  , response = http.ServerResponse.prototype
  , _render = response.render;

response.render = function(view, options, callback) {
  _render.call(this, "testViews/" + view, options, callback);
};
Dunston answered 15/2, 2012 at 13:47 Comment(3)
Thanks for that. Although this seems to break how it looks for the layout/master view as well, it tries to look for 'testViews/layout.ejs'. I did look at the source and saw that a _render method is already define on the response object, could this cause problems?Sordello
Yes, that's correct - it will prepend "testViews/" to all views. It has nothing to do with ServerResponse._render. What are you trying to accomplish?Dunston
Tried to implement the ServerRespone.prototype hack and failed. Guess this worked in node 0.6/express 2? Do you have any idea how to make this work in node 0.8 and express 3?Defeat
P
4

I recently found myself needing to do the same thing, to provide a configuration-specific Google Analytics property id and cookie domain to each of my templates.

There are a number of great solutions here.

I chose to go with something very close to the solution proposed by Lex, but ran into problems where calls to res.render() did not already include existing options. For instance, the following code was causing an exception in the call to extend(), because options was undefined:

return res.render('admin/refreshes');

I added the following, which accounts for the various combinations of arguments that are possible, including the callback. A similar approach can be used with the solutions proposed by others.

app.use(function(req, res, next) {
  var _render = res.render;
  res.render = function(view, options, callback) {
    if (typeof options === 'function') {
      callback = options;
      options = {};
    } else if (!options) {
      options = {};
    }
    extend(options, {
      gaPropertyID: config.googleAnalytics.propertyID,
      gaCookieDomain: config.googleAnalytics.cookieDomain
    });
    _render.call(this, view, options, callback);
  }
  next();
});

edit: Turns out that while this all might be handy when I need to actually run some code, there's a tremendously simpler way to accomplish what I was trying to do. I looked again at the source and docs for Express, and it turns out that the app.locals are used to render every template. So in my case, I ultimately replaced all of the middleware code above with the following assignments:

app.locals.gaPropertyID = config.googleAnalytics.propertyID;
app.locals.gaCookieDomain = config.googleAnalytics.cookieDomain;
Pralltriller answered 2/10, 2015 at 16:28 Comment(1)
Two really good points. The locals property would fit most cases. And if no options are passed to render the callback becomes undefined, and all hell breaks loose.Tavis

© 2022 - 2024 — McMap. All rights reserved.