Thursday, April 29, 2010

Fab.js: Binary Apps (and Solving a Minor Mystery with fab.nodejs.http)

‹prev | My Chain | next›

Before moving on with fab.js, I need to take a break to investigate binary apps. Specifically, I need to ensure that I understand listeners.

I start with my usual fab.js skeleton:
var puts = require( "sys" ).puts;

with ( require( ".." ) )

( fab )

( listen, 0xFAB )

( 404 );
I replace the default 404 response with two binaries and a very simple unary that responds with a body containing only "foo":
var puts = require( "sys" ).puts;

with ( require( ".." ) )

( fab )

( listen, 0xFAB )

// binary app
(
function (app) {
return function () {
return app.call(this);
};
}
)

// binary app
(
function (app) {
return function () {
return app.call(this);
};
}
)

// unary app
(
function () {
this({body: "foo"})();
}
);
I start up the fab.js server:
cstrom@whitefall:~/repos/fab$ node ./play/binary_binary.js
Now, when I access any resource on my fab.js server, I get this response:
cstrom@whitefall:~/repos/fab$ curl http://localhost:4011/bar
foo
Easy enough.

But I was accessing the "/bar" resource on my server, so I really want the server to reply with "bar". For that to happen, the (fab) unary app needs to return a listener to the downstream app requesting the headers:
(
function () {
var out = this;
return function (head) {
out({body: head.url})();
};
}
);
Now, when I access the same resource, I get an empty response from the server because it crashed:
cstrom@whitefall:~/repos/fab$ node ./play/binary_binary.js
TypeError: Cannot call method 'toString' of undefined
at ServerResponse.write (http:345:31)
at listener (/home/cstrom/repos/fab/apps/fab.nodejs.js:55:20)
at /home/cstrom/repos/fab/play/binary_binary.js:30:22
at Server.<anonymous> (/home/cstrom/repos/fab/apps/fab.nodejs.js:18:17)
at HTTPParser.onIncoming (http:562:10)
at HTTPParser.onHeadersComplete (http:84:14)
at Stream.ondata (http:524:30)
at IOWatcher.callback (net:307:31)
at node.js:748:9
This turns out to be a problem with my use of head.url rather than head.url.pathname. The backtrace pointed to the correct line (18), but the error was less-than-helpful. Anyhow, my new unary function looks like:
(
function () {
var out = this;
return function (head) {
out({body: head.url.pathname.substring(1)})();
};
}
);
Now when I access the "/bar" resource I get my desired response:
cstrom@whitefall:~/repos/fab$ curl http://localhost:4011/bar
bar
Nice!

That is especially nice because I did not have to set up anything special in the downstream apps to call that listener—fab.js just took care of it for me. But what if I want to access the response in my binary middleware? For that, I do need a listener:
(
function (app) {
return function () {
var out = this;
return app.call(
function listener(obj) {
if (obj) arguments[0] = {body: obj.body + '+foo'};
out.apply(this, arguments);

return listener;
}

);
};
}
)
Here, I give the upstream listener, the out() call in my unary app something to call directly in my binary/middleware app. I take the object with the path body and append '+foo' to the body, I then send the response on its merry way back downstream with another apply. Now, if I access the '/bar' resource, I should get a response of 'bar+foo':
cstrom@whitefall:~/repos/fab$ curl http://localhost:4011/bar
bar+foo
Perfect!

So far all of this jibes with my understanding of binary apps in fab.js. So why did I bother with all of this? Well, it is because fab.nodejs.http does not seem to behave like a binary app. Specifically, if my downstream app replies with a resource that fab.nodejs.http should proxy, say a CouchDB resource:
( fab.nodejs.http )

(
function () {
var out = this({body:'http://localhost:5984/seed/test'});
if (out) out();
}
)
...then everything is hunky-dory:
cstrom@whitefall:~/repos/fab$ curl http://localhost:4011/test
{"_id":"test","_rev":"2-4b4f7289fd8b290dfcffe7bdfc23cf8d","change":"bar"}
The problem occurs if I try to infer the CouchDB resource from the header information:
( fab.nodejs.http )

(
function () {
var out = this;
return function (head) {
out = out({body: 'http://localhost:5984/seed' + head.url.pathname});
if (out) out();
};
}
)
My unary is identical to my previous unary (I'm just pre-pending the CouchDB database URL), so I know that it is a valid (fab) unary app. Sadly however, when I access any resource, it just hangs.

Armed with my knowledge of how a binary / middleware app ought to respond to an upstream listener, I root around in fab.nodejs.http and find that is does not, in fact, return an upstream listener. It works just fine if the upstream app returns without inspecting headers, but, if it needs to listen for those headers... nothing.

So I add a listener:
exports.app = function( app ) {
var url = require( "url" )
, http = require( "http" );

return function() {
var out = this;
return app.call( function listener( obj ) {

if (obj) {
// proxy the request as before with node.js
}
return listener;
});
}
Nothing special there—I am just repeating what I already verified was working in my simplistic binary app. Now, when I access the "test" resource on my fab.js server, the CouchDB "test" document is returned:
cstrom@whitefall:~/repos/fab$ curl http://localhost:4011/test
{"_id":"test","_rev":"2-4b4f7289fd8b290dfcffe7bdfc23cf8d","change":"bar"}
More importantly, when I access the "foo" resource on my fab.js server, the "foo" document is retrieved from CouchDB:
cstrom@whitefall:~/repos/fab$ curl http://localhost:4011/foo
{"_id":"foo","_rev":"1-967a00dff5e02add41819138abb3284d"}
I will submit a pull request tomorrow. For now, I am thrilled to have finally gotten to the bottom of this pesky little mystery. In retrospect, maybe I should have suspected fab.nodejs.http since it is undergoing major refactoring, but my inexperience with fab.js led me astray. As with anything new, breaking the problem down into small, well understood chunks until I could recreate the problem served me well in solving this little problem.

Day #88

No comments:

Post a Comment