Extending EventEmitter In Order To Create A Response Proxy In Node.js

Posted April 27, 2011 at 5:18 PM by Ben Nadel

Tags: Javascript / DHTML

I've been doing a lot of experimenting with Node.js lately and one thing that I thought might be useful would be some additional hooks into the various parts of the HTTP response lifecycle. When you create an HTTP server, each request provides a response stream to which you can write data; but, other than hooking into the "finish" event on the response, Node.js doesn't appear to offer any other events - at least none that I could find (the Node.js documentation is decidedly sparse on the matter). In order to learn more about events in Node.js, I wanted to see if I could use the EventEmitter class in order to create a response Proxy object that would publish more events during the HTTP response lifecycle.

 
 
 
 
 
 
 
 
 
 

The ResponseProxy() class that I created extends the EventEmitter class using prototypal inheritance. It exposes a smaller API but a more robust event set. In particular, I chose to publish the following four events during the HTTP response lifecycle:

  • commitStart - Fires right before the headers are flushed to the client (which commits the response).
  • commitEnd - Fires right after the headers have been flushed to the client.
  • requestStart - Fires right before any content has been written to the response output.
  • requestEnd - Fires right before the response is closed.

In order to make these events possible, each ResponseProxy() instance has to manage the way in which the response stream is used. In particular, it has to be very explicit about when the headers are written and when the underlying stream is actually closed. To do this, the ResponseProxy() class exposes the following methods:

  • end( [ data [, encoding ]] ) - Closes the response with an optional output write.
  • getResponse() - Returns the underlying response object.
  • setHeader( name, value ) - Sets an HTTP header.
  • setStatus( code [, text ] ) - Sets the HTTP response code.
  • write( data [, encoding ] ) - Writes data to the output.

Ultimately, these methods just proxy the underlying HTTP response stream. But, in doing so, we can wrap event publication around the first "write" action and event publication around the "end" action.

Let's take a look at the code. In the following Node.js HTTP server, notice that every single request results in a new ResponseProxy() instance. Also notice that the response is altered both by direct calls as well as by event listeners.

Server.js - HTTP Server

  • // Include core libraries.
  • var http = require( "http" );
  • var events = require( "events" );
  •  
  •  
  • // ---------------------------------------------------------- //
  • // ---------------------------------------------------------- //
  • // ---------------------------------------------------------- //
  • // ---------------------------------------------------------- //
  •  
  •  
  • // I am the response proxy constructor.
  • function ResponseProxy( response ){
  •  
  • // Initialize super class properties. This will make sure that
  • // two instances of the response proxy don't accidentally share
  • // and event listeners.
  • events.EventEmitter.call( this );
  •  
  • // Store the original response object.
  • this._response = response;
  •  
  • // Set up the default status code.
  • this._status = {
  • code: 200,
  • text: "OK"
  • };
  •  
  • // Set up the default headers collection.
  • this._headers = {
  • "content-type": "text/plain"
  • };
  •  
  • // I determine whether or not the headers have been flushed.
  • this._isCommitted = false;
  •  
  • // Return this object reference.
  • return( this );
  •  
  • }
  •  
  • // Extend the EventEmitter class allowing us to use on() and emit()
  • // methods like the underlying response.
  • ResponseProxy.prototype = new events.EventEmitter();
  •  
  •  
  • // I commit the response to the client. This will flush the headers
  • // and flag the response as committed.
  • ResponseProxy.prototype._commitResponse = function(){
  •  
  • // Raise the commit-start event - this will give people one last
  • // hook for setting headers before they are flushed.
  • this.emit( "commitStart" );
  •  
  • // Flag the response as committed.
  • this._isCommitted = true;
  •  
  • // Write the status code.
  • this._response.writeHead(
  • this._status.code,
  • this._status.text,
  • this._headers
  • );
  •  
  • // Raise the commit-end event. At this point, no one can write
  • // anymore headers to the response.
  • this.emit( "commitEnd" );
  •  
  • // Raise the response-start event. This will be the first
  • // opporunity for someone to write to the output.
  • this.emit( "responseStart" );
  •  
  • };
  •  
  •  
  • // I end the current response, flushing any headers and additional
  • // content that have not yet been committed.
  • ResponseProxy.prototype.end = function( data, encoding ){
  •  
  • // Check to see if the current response has been committed.
  • // If not, then we have to commit before we end the response.
  • if (!this._isCommitted){
  •  
  • // Commit the response.
  • this._commitResponse();
  •  
  • }
  •  
  • // Check to see if any data was send with the end() request.
  • // If so, we want to write it manually - this way, we can still
  • // put a hook around the underlying end() request.
  • if (data){
  •  
  • // Write the last piece of data.
  • this._response.write( data, encoding );
  •  
  • }
  •  
  • // Raise the response-end event. This will provide one last
  • // hook for content to be written to the response.
  • this.emit( "responseEnd" );
  •  
  • // End the event.
  • this._response.end();
  •  
  • // Return this object reference for method chaining.
  • return( this );
  •  
  • };
  •  
  •  
  • // I provide access to the underlying response.
  • ResponseProxy.prototype.getResponse = function(){
  •  
  • // Return the underlying response.
  • return( this._response );
  •  
  • };
  •  
  •  
  • // I add the given header name/value pair to the header collection.
  • ResponseProxy.prototype.setHeader = function( name, value ){
  •  
  • // Store the header.
  • this._headers[ name ] = value;
  •  
  • // Return this object reference for method chaining.
  • return( this );
  •  
  • };
  •  
  •  
  • // I set the given status code and text.
  • ResponseProxy.prototype.setStatus = function( code, text ){
  •  
  • // Set the status code.
  • this._status.code = code;
  • this._status.text = (text || "");
  •  
  • // Return this object reference for method chaining.
  • return( this );
  •  
  • };
  •  
  •  
  • // I write data to the response, flushing any headers that have not
  • // yet been committed.
  • ResponseProxy.prototype.write = function( data, encoding ){
  •  
  • // Check to see if the current response has been committed.
  • // If not, then we have to commit before we write.
  • if (!this._isCommitted){
  •  
  • // Commit the response.
  • this._commitResponse();
  •  
  • }
  •  
  • // Write the data to the underlying response.
  • this._response.write( data, encoding );
  •  
  • // Return this object reference for method chaining.
  • return( this );
  •  
  • };
  •  
  •  
  • // ---------------------------------------------------------- //
  • // ---------------------------------------------------------- //
  • // ---------------------------------------------------------- //
  • // ---------------------------------------------------------- //
  •  
  •  
  • // Create the HTTP server.
  • var server = http.createServer(
  • function( request, response ){
  •  
  •  
  • // Create a new response proxy for our response.
  • var responseProxy = new ResponseProxy( response );
  •  
  •  
  • // Bind the commit-start event so we can add one more header
  • // before the content is flushed.
  • responseProxy.on( "commitStart", function(){
  •  
  • // Add one more header.
  • this.setHeader( "Commit-Start", "Test event hook" );
  •  
  • });
  •  
  • // Bind to the response-start event so we can be the first
  • // people to write to the output.
  • responseProxy.on( "responseStart", function(){
  •  
  • // Write the very first output data.
  • this.write( "I am the very first output!\n" );
  •  
  • });
  •  
  • // Bind to the response-end event so we can write more
  • // data to the output before the connection is closed.
  • responseProxy.on( "responseEnd", function(){
  •  
  • // Write one more bit of output.
  • this.write( "I am the last possible output!\n" );
  •  
  • });
  •  
  •  
  • // -- Start using proxy directly. -- //
  •  
  •  
  • // Set a header.
  • responseProxy.setHeader( "Explicit-Set", "Header value." );
  •  
  • // Write some output.
  • responseProxy.write( "This is the inline-write.\n" );
  •  
  • // End the response.
  • responseProxy.end( "Ending.\n" );
  •  
  •  
  • }
  • );
  •  
  • // Point the server to listen to the given port for incoming
  • // requests.
  • server.listen( 8080 );
  •  
  •  
  • // ---------------------------------------------------------- //
  • // ---------------------------------------------------------- //
  •  
  •  
  • // Output intialization confirmation message.
  • console.log( "HTTP sever is running on port 8080." );

Notice that our ResponseProxy() event listeners allow us to alter the headers right before the response is committed and to write output right before the response is closed. When we boot up the above HTTP server and make a request, we get the following response:

 
 
 
 
 
 
Writing to an HTTP response stream in Node.js using an EventEmitter-powered Response proxy object. 
 
 
 

As you can see, our event-based publication and subscription functionality provided by the inherited EventEmitter class allows us to augment the response based on the response lifecycle in addition to explicit writing, flushing, and closing. Since the EventEmitter class is being inherited, however, it is critical that the super constructor - EventEmitter - be invoked during the initialization of each ResponseProxy() instance. This will prevent two proxy objects from accidentally sharing the same set of event listeners (theoretically).

In a simple request / response context, something like this would probably be unnecessary. However, if you were building a Node.js HTTP framework, I can easily see some powerful functionality provided by a more robust set of response lifecycle hooks. If nothing else, this helped me get more comfortable with the core event model being used in Node.js.




Reader Comments

Apr 27, 2011 at 6:30 PM // reply »
2 Comments

Ben, I see you're starting to dive into Node these days. As much as I love CF, I honestly haven't used it since picking up Node a couple of months back. You should check out the Express and Geddy frameworks. Also check out Supervisor for node. Supervisor will monitor the node process and relaunch if it fails or reload if the source changes.


Apr 28, 2011 at 3:59 AM // reply »
27 Comments

requestStart, requestEnd, keep it up and you'll have an application.cfc on node in no time :)


Apr 28, 2011 at 8:42 AM // reply »
11,241 Comments

@Chris,

It's definitely a cool server technology. I don't see it replacing CF for me (at least not till I know a ton more about it). Supervisor sounds cool. At Nodejitsu (where I took my class over the weekend), they use a module they built called "Forever", which I think is the same concept - it monitors child processes and re-launches them if they die.

@Nelle,

I'd be lying if that wasn't *exactly* what I was thinking ;)


Apr 28, 2011 at 1:25 PM // reply »
2 Comments

If you haven't already give Socket.io (http://socket.io) a look see - combined with Express, the two make a formidable web tech stack.


Apr 28, 2011 at 2:57 PM // reply »
11,241 Comments

@Brian,

I haven't used Socket.io directly; but, I did play around with NowJS, which, I believe, uses Socket.io under the covers. I think it's all part of the socket-based realtime communication.... which is wicked awesome :D

I haven't looked at Express yet, either.


Post A Comment

Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.

Please review the following issues:

Author Name:


Author Email:

Author Website:

Comment:

Supported HTML tags for formatting: <strong>bold</strong>   <em>italic</em>   <code>code</code>







  • Help Wanted - Find Your Next ColdFusion Job
Ben Nadel's Company - Epicenter Consulting Recent Blog Comments
May 22, 2013 at 7:52 AM
Nested Views, Routing, And Deep Linking With AngularJS
Hi, Just a quick thank you. As it happens, for my own purposes, the pending ui-router work being done in native angular is likely the one I'll adopt, but your exploration, code and documentation of ... read »
May 22, 2013 at 4:43 AM
How Do You Use The ColdFusion CFParam Tag?
'<cfparam>' or 'isDefined()and <cfset>' performs the same task.Is there any difference? ... read »
May 21, 2013 at 7:46 PM
Using Plupload For Drag & Drop File Uploads In ColdFusion
No luck. At least I have uncovered the cause, URLScan 3.1. Here is what I see in the IIS log when a file is over 30mb. 2013-05-21 23:29:05 10.105.45.128 GET /plupload/assets/jquery/jquery-1.8. ... read »
May 21, 2013 at 6:12 PM
Using Plupload For Drag & Drop File Uploads In ColdFusion
Ben, I did not see you after Pete Freitag's Lockdown session at cfObjective but he said that IIS sets file size limits at 30MB by default which just happened to be the threshold for file size when ... read »
May 21, 2013 at 11:51 AM
Ask Ben: Parsing Very Large XML Documents In ColdFusion
Looking at my first ever XML document that I have to parse and put into MS SQL 2000 with CF8. I get it to list the desired Field name, many times over, and have a long list of this field name displa ... read »
May 21, 2013 at 9:25 AM
Turning Off and On Identity Column in SQL Server
you are awesome..i am lucky to get this blog between such a garbage one....Thanks, Prashant ... read »
May 20, 2013 at 4:38 PM
Using A Dynamic Column Name With ValueList() In ColdFusion
@Dana, Your confusion is well founded, since this is a very confusing features. In fact, it ONLY works if you use array notation. Meaning, that this: arrayToList( query[ "columnName" ] ) ... read »
May 20, 2013 at 4:34 PM
Using A Dynamic Column Name With ValueList() In ColdFusion
I was thinking chicken and the egg, I wouldn't have expected it to work in the valuelist going in I guess. Maybe I just need a beer, long day :) ... read »
InVision App - Prototyping Made Beautiful With Prototyping Tools