request.js | |
---|---|
The request object encapsulates a request, creating a Node.js HTTP request and then handling the response. | var HTTP = require("http")
, HTTPS = require("https")
, URL = require("url")
, QueryString = require("querystring")
, Emitter = require('events').EventEmitter
, sprintf = require("sprintf").sprintf
, _ = require("underscore")
, Response = require("./response")
, HeaderMixins = require("./mixins/headers")
, Content = require("./content")
; |
The Shred object itself constructs the | var Request = function(options) {
this.log = options.logger;
processOptions(this,options||{});
createRequest(this);
}; |
A | Object.defineProperties(Request.prototype, { |
| url: {
get: function() {
if (!this.scheme) { return null; }
return sprintf("%s://%s:%s%s",
this.scheme, this.host, this.port,
(this.proxy ? "/" : this.path) +
(this.query ? ("?" + this.query) : ""));
},
set: function(_url) {
_url = URL.parse(_url);
this.scheme = _url.protocol.slice(0,-1);
this.host = _url.hostname;
this.port = _url.port;
this.path = _url.pathname;
this.query = _url.query;
return this;
},
enumerable: true
}, |
|
headers: {
get: function() {
return this.getHeaders();
},
enumerable: true
}, |
| port: {
get: function() {
if (!this._port) {
switch(this.scheme) {
case "https": return this._port = 443;
case "http":
default: return this._port = 80;
}
}
return this._port;
},
set: function(value) { this._port = value; return this; },
enumerable: true
}, |
| method: {
get: function() {
return this._method = (this._method||"GET");
},
set: function(value) {
this._method = value; return this;
},
enumerable: true
}, |
| query: {
get: function() {return this._query;},
set: function(value) {
if (value) {
if (typeof value === 'object') {
value = QueryString.stringify(value);
} else {
value = QueryString.stringify(QueryString.parse(value.toString()))
}
}
if (value) { this._query = "?" + value; }
else this._query = "";
return this;
},
enumerable: true
}, |
| parameters: {
get: function() { return QueryString.parse(this._query||""); },
enumerable: true
}, |
| body: {
get: function() { return this._body; },
set: function(value) {
this._body = new Content({
data: value,
type: this.getHeader("Content-Type")
});
this.setHeader("Content-Type",this.content.type);
this.setHeader("Content-Length",this.content.length);
return this;
},
enumerable: true
}, |
| timeout: {
get: function() { return this._timeout; }, // in milliseconds
set: function(timeout) {
var request = this
, milliseconds = 0;
;
if (!timeout) return this;
if (typeof options=="number") { milliseconds = options; }
else {
milliseconds = (options.milliseconds||0) +
(1000 * ((options.seconds||0) +
(60 * ((options.minutes||0) +
(60 * (options.hours||0))))));
}
this._timeout = milliseconds;
return this;
},
enumerable: true
}
}); |
Alias | Object.defineProperty(Request.prototype,"content",
Object.getOwnPropertyDescriptor(Request.prototype, "body")); |
The | _.extend(Request.prototype,{
inspect: function() {
var request = this;
var headers = _(request.headers).reduce(function(array,value,key){
array.push("\t" + key + ": " + value); return array;
},[]).join("\n");
var summary = ["<Shred Request> ", request.method.toUpperCase(),
request.url].join(" ")
return [ summary, "- Headers:", headers].join("\n");
}
}); |
Allow chainable 'on's: shred.get({ ... }).on( ... ). You can pass in a single function, a pair (event, function), or a hash: { event: function, event: function } | _.extend(Request.prototype,{
on: function(eventOrHash, listener) {
var emitter = this.emitter; |
Pass in a single argument as a function then make it the default response handler | if (arguments.length === 1 && typeof(eventOrHash) === 'function') {
emitter.on('response', eventOrHash);
} else if (arguments.length === 1 && typeof(eventOrHash) === 'object') {
_(eventOrHash).each(function(value,key) {
emitter.on(key,value);
});
} else {
emitter.on(eventOrHash, listener);
}
return this;
}
}); |
Add in the header methods. Again, these ensure we don't get the same header multiple times with different case conventions. | HeaderMixins.gettersAndSetters(Request); |
| var processOptions = function(request,options) {
request.log.debug("Processing request options .."); |
We'll use | request.emitter = (new Emitter);
|
Set up the handlers ... | if (options.on) {
_(options.on).each(function(value,key) {
request.emitter.on(key,value);
});
}
|
Make sure we were give a URL or a host | if (!options.url && !options.host) {
request.emitter.emit("error",
new Error("No url or url options (host, port, etc.)"));
return;
} |
Allow for the use of a proxy. |
if (options.url) {
if (options.proxy) {
request.url = options.proxy;
request.path = options.url;
} else {
request.url = options.url;
}
} |
Set the remaining options. | request.query = options.query||options.parameters;
request.method = options.method;
request.setHeader("user-agent",options.agent||"Shred for Node.js, Version 0.5.0");
request.setHeaders(options.headers);
|
The content entity can be set either using the | if (options.body||options.content) {
request.content = options.body||options.content;
}
request.timeout = options.timeout;
}; |
| var createRequest = function(request) {
var timeout
;
request.log.debug("Creating request ..");
request.log.debug(request);
|
Choose which Node.js standard library to use. Warning: Shred has not
been tested much with | var http = request.scheme == "http" ? HTTP : HTTPS;
|
Set up the real request using the selected library. The request won't be
sent until we call | request._raw = http.request({
host: request.host,
port: request.port,
method: request.method,
path: request.path+request.query,
headers: request.getHeaders() |
Provide a response handler. | }, function(response) {
request.log.debug("Received response .."); |
We haven't timed out and we have a response, so make sure we clear the timeout so it doesn't fire while we're processing the response. | clearTimeout(timeout); |
Construct a Shred | response = new Response(response, request, function(response) {
|
Set up some event magic. The precedence is given first to
status-specific handlers, then to responses for a given event, and then
finally to the more general | var emit = function(event) {
if (request.emitter.listeners(response.status).length>0) {
request.emitter.emit(response.status,response);
} else {
if (request.emitter.listeners(event).length>0) {
request.emitter.emit(event,response);
} else if (!response.isRedirect) {
request.emitter.emit("response",response);
}
}
};
|
Next, check for a redirect. We simply repeat the request with the URL
given in the | if (response.isRedirect) {
request.log.debug("Redirecting to "
+ response.getHeader("Location"));
request.url = response.getHeader("Location");
emit("redirect");
createRequest(request);
|
Okay, it's not a redirect. Is it an error of some kind? | } else if (response.isError) {
emit("error");
} else { |
It looks like we're good shape. Trigger the | emit("success");
}
});
}); |
We're still setting up the request. Next, we're going to handle error cases where we have no response. We don't emit an error event because that event takes a response. We don't response handlers to have to check for a null value. However, we should introduce a different event type for this type of error. | request._raw.on("error", function(error) {
request.log.error("Request failed: " + error.message);
}); |
We're almost there. Next, we need to write the request entity to the underlying request object. | if (request.content) {
request.log.debug("Streaming body: '" +
request.content.body.slice(0,59) + "' ... ");
request._raw.write(request.content.body);
} |
Finally, we need to set up the timeout. We do this last so that we don't start the clock ticking until the last possible moment. | if (request.timeout) {
timeout = setTimeout(function() {
request.log.debug("Timeout fired, aborting request ...");
request._raw.abort();
request.emit("timeout", request);
},request.timeout);
} |
The | request.log.debug("Sending request ...");
request._raw.end();
};
module.exports = Request;
|