Sunday, December 23, 2012

Create FilterChain in node.js

I often try to learn something interesting (mostly programming related) whenever I get a long break from work.  Last Thanksgiving, I wrote an iPhone app that syncs photo among S3, Flickr and Facebook.  This Xmas, I took on writing my first node.js app.

I used http://www.nodebeginner.org/ as a starting point.  5 minutes into the tutorial, I encountered a strange problem.  For every request sent from chrome browser to my node.js server, my service recorded TWO requests.  A quick google search indicated that chrome ALWAYS sends an additional "/favicon.ico" request to an HTTP server if it cannot locate an icon for that server.

This was really annoying because it messed my global debugging counter.  The solution was simple: just ignore all requests in the format of "/favicon.ico".  But "SIMPLE" solutions are no fun, especially during learning process.  If I were to do this in java, I'd use ServletFilter to "preFilter" out all unqualified requests.  So I put my javascript and java skills to the test, and wrote this simple FilterChain function in node.js.  Enjoy!


var http = require("http");

/*
* Manage all filters.
*/
var filterChain = {
  filters: new Array(),
  add: function(filter) {
    this.filters.push(filter);
  },
  applyAll: function(request, response) {
    this.apply(request, response, 0);
  },
  apply: function(request, response, i) {
    if (i == this.filters.length) {
      return processRequest(request, response);
    }
    
    var filter = this.filters[i];

    // call preFilter and exits if fails
    console.log(filter.name + ".preFilter");
    success = filter.preFilter(request, response);  
    if (!success) {
      return false;
    }
    // call next filter and exits if fails
  success = this.apply(request, response, i+1);
  if (!success) {
      return false;
    }
    
    // call postFilter and exits
    console.log(filter.name + ".postFilter");
    success = filter.postFilter(request, response);
    return success;
  }
}

/*
* Filters
* All filters must implement 3 things:
* name - String unique name for this filter
* preFilter() - Executed before processing request
* postFilter() - Executed after processing request
*/
var faviconFilter = {
  name: "favicon",
  preFilter: function(request, response) {
  if (request.url === '/favicon.ico') {
    response.writeHead(200, {'Content-Type': 'image/x-icon'} );
    response.end();
    console.log('favicon request, filtered out!');
    return false;
  } else {
    return true;
  }
},
postFilter: function(request, response){return true;}
};
var latencyFilter = {
  name: "latency",
  timer: null,
  preFilter: function(request, response) {
    this.timer = process.hrtime();
    return true;
  },
  postFilter: function(request, response) {
    diff = process.hrtime(this.timer);
    console.log("<%s>%ds%dns", request.url, diff[0], diff[1]);
    return true;
  }
};

filterChain.add(latencyFilter);
filterChain.add(faviconFilter);

/*
* This is the actual function that processes the request
*/
function processRequest(request, response) {
  response.writeHead(200, {"Content-Type": "text/plain"});
  response.write("Hello World");
  console.log("Response send.");
  response.end();
  return true;
}

function onRequest(request, response) {
  filterChain.applyAll(request, response);
}

http.createServer(onRequest).listen(8888);

console.log("Server has started.");

No comments: