Permalink
Browse files

Merge branch 'http' into devel

  • Loading branch information...
2 parents e1a557f + cde4a4a commit 7d269cd9bff8884ebfaefddf712b33ac29c0d6a4 @n1mmy n1mmy committed Apr 19, 2012
View
@@ -1355,6 +1355,85 @@ <h2 id="meteor_deps"><span>Meteor.deps</span></h2>
then restores its previous value.)
+<h2 id="meteor_http"><span>Meteor.http</span></h2>
+
+{{> api_box httpcall}}
+
+This function initiates an HTTP request to a remote server. It returns
+a result object with the contents of the HTTP response. The result
+object is detailed below.
+
+On the server, this function can be run synchronously by omitting
+the callback argument. In this case, the results are returned
+to the caller once the request completes. This is useful when making
+server-to-server HTTP API calls from with Meteor methods. You can easily
+delay return method success or failure to the client until after the HTTP
+call has finished. In this case, consider running
+[`this.unblock()`](#method_unblock) to allow other methods to start.
+
+The `url` argument can begin with `"http://"` or `"https://"`. On the
+client, you can also use relative URLs, on the server only absolute URLs
+are allowed. `url` can include a query string, but it will be
+overwritten if the `query` option is passed. The `params` option will be
+encoded and either added to the URL (GET requests and POST requests that
+already have `content` specified) or to the body of the request (POST
+requests without `content` specified)
+
+The callback receives two arguments, `error` and `result`. The `error`
+argument will contain an Error if the request fails in any way,
+including a bad HTTP status code. The result object is always
+defined. When run in synchronous mode, the `result` is returned from the
+function, and the `error` value is a stored as a property in `result`.
+
+Contents of the result object:
+
+<dl class="objdesc">
+
+<dt><span class="name">statusCode</span>
+ <span class="type">Number or null</span></dt>
+<dd>Numeric HTTP result status code, or null on error.</dd>
+
+<dt><span class="name">headers()</span>
+ <span class="type">Object</span></dt>
+<dd>Return a dictionary of HTTP headers from the response.</dd>
+
+<dt><span class="name">content()</span>
+ <span class="type">String</span></dt>
+<dd>Return the body of the HTTP response as a string.</dd>
+
+<dt><span class="name">data()</span>
+ <span class="type">Object</span></dt>
+<dd>Return the body of the document parsed as a JSON object</dd>
+
+<dt><span class="name">error</span>
+ <span class="type">Error</span></dt>
+<dd>Error object if the request failed. Matches the `error` callback parameter.</dd>
+
+
+</dl>
+
+Example server method:
+
+ Meteor.methods({check_twitter: function (user_id) {
+ this.unblock();
+ var result = Meteor.http.call("GET", "http://api.twitter.com/xxx",
+ {params: {user: user_id}});
+ if (result.statusCode === 200)
+ return true
+ return false;
+ }});
+
+Example asynchronous HTTP call:
+
+ Meteor.http.call("POST", "http://api.twitter.com/xxx",
+ {data: {some: "json", stuff: 1}},
+ function (error, result) {
+ if (result.statusCode === 200) {
+ Session.set("twizzled", true);
+ }
+ });
+
+
{{/better_markdown}}
</template>
View
@@ -743,3 +743,52 @@ Template.api.equals = {
descr: "The value to test against"}
]
};
+
+Template.api.httpcall = {
+ id: "meteor_http_call",
+ name: "Meteor.http.call(method, url, [options], [asyncCallback])",
+ locus: "Anywhere",
+ descr: ["Perform an outbound HTTP request."],
+ args: [
+ {name: "method",
+ type: "String",
+ descr: 'The HTTP method to use, e.g. "GET" or "POST".'},
+ {name: "url",
+ type: "String",
+ descr: 'The URL to retrieve.'},
+ {name: "asyncCallback",
+ type: "Function",
+ descr: "Optional callback. If passed, the method runs asynchronously, instead of synchronously, and calls asyncCallback. On the client, this callback is required."
+ }
+ ],
+ options: [
+ {name: "content",
+ type: "String",
+ descr: "HTTP request body, as a raw string."
+},
+ {name: "data",
+ type: "Object",
+ descr: "JSON-able object, encoded and placed in HTTP request body. Overwrites `content`."},
+ {name: "query",
+ type: "String",
+ descr: "Query string. If given, overwrites any query string in `url`"},
+ {name: "params",
+ type: "Object",
+ descr: "Parameters are encoded and placed in the URL for GETs, or the body for POSTs"
+ },
+ {name: "auth",
+ type: "String",
+ descr: 'HTTP basic authentication string, e.g. `"username:password"`'},
+ {name: "headers",
+ type: "Object",
+ descr: "Dictionary of values to place in HTTP headers."},
+ {name: "timeout",
+ type: "Number",
+ descr: "Fail the request if it takes longer that `timeout` milliseconds."},
+ {name: "followRedirects",
+ type: "Boolean",
+ descr: "Whether or not to follow HTTP redirects. Defaults to true. Can not be disabled on the client."}
+ ]
+};
+
+
View
@@ -162,6 +162,10 @@ var toc = [
// {instance: "env_var", name: "withValue", id: "env_var_withvalue"},
// {instance: "env_var", name: "bindEnvironment", id: "env_var_bindenvironment"}
// ]
+ ],
+
+ "Meteor.http", [
+ "Meteor.http.call"
]
],
@@ -0,0 +1,154 @@
+Meteor.http = Meteor.http || {};
+
+(function() {
+
+ Meteor.http.call = function(method, url, options, callback) {
+
+ ////////// Process arguments //////////
+
+ if (! callback && typeof options === "function") {
+ // support (method, url, callback) argument list
+ callback = options;
+ options = null;
+ }
+
+ options = options || {};
+
+ if (typeof callback !== "function")
+ throw new Error(
+ "Can't make a blocking HTTP call from the client; callback required.");
+
+ method = (method || "").toUpperCase();
+
+ var content = options.content;
+ if (options.data)
+ content = JSON.stringify(options.data);
+
+ var params_for_url, params_for_body;
+ if (content || method === "GET" || method === "HEAD")
+ params_for_url = options.params;
+ else
+ params_for_body = options.params;
+
+ var query_match = /^(.*?)(\?.*)?$/.exec(url);
+ url = Meteor.http._buildUrl(query_match[1], query_match[2],
+ options.query, params_for_url);
+
+
+ if (options.followRedirects === false)
+ throw new Error("Option followRedirects:false not supported on client.");
+
+ var username, password;
+ if (options.auth) {
+ var colonLoc = options.auth.indexOf(':');
+ if (colonLoc < 0)
+ throw new Error('auth option should be of the form "username:password"');
+ username = options.auth.substring(0, colonLoc);
+ password = options.auth.substring(colonLoc+1);
+ }
+
+ if (params_for_body) {
+ content = Meteor.http._encodeParams(params_for_body);
+ }
+
+ ////////// Callback wrapping //////////
+
+ // wrap callback to always return a result object, and always
+ // have an 'error' property in result
+ callback = (function(callback) {
+ return function(error, result) {
+ result = result || {};
+ result.error = error;
+ callback(error, result);
+ };
+ })(callback);
+
+ // safety belt: only call the callback once.
+ callback = _.once(callback);
+
+
+ ////////// Kickoff! //////////
+
+ // from this point on, errors are because of something remote, not
+ // something we should check in advance. Turn exceptions into error
+ // results.
+ try {
+ // setup XHR object
+ var xhr;
+ if (typeof XMLHttpRequest !== "undefined")
+ xhr = new XMLHttpRequest();
+ else if (typeof ActiveXObject !== "undefined")
+ xhr = new ActiveXObject("Microsoft.XMLHttp"); // IE6
+ else
+ throw new Error("Can't create XMLHttpRequest"); // ???
+
+ xhr.open(method, url, true, username, password);
+
+ if (options.headers)
+ for (var k in options.headers)
+ xhr.setRequestHeader(k, options.headers[k]);
+
+
+ // setup timeout
+ var timed_out = false;
+ var timer;
+ if (options.timeout) {
+ timer = Meteor.setTimeout(function() {
+ timed_out = true;
+ xhr.abort();
+ }, options.timeout);
+ };
+
+ // callback on complete
+ xhr.onreadystatechange = function(evt) {
+ if (xhr.readyState === 4) { // COMPLETE
+ if (timer)
+ Meteor.clearTimeout(timer);
+
+ if (timed_out) {
+ callback(new Error("timeout"));
+ } else if (! xhr.status) {
+ // no HTTP response
+ callback(new Error("network"));
+ } else {
+ var response = {};
+ response.statusCode = xhr.status;
+ response.content = function() {
+ return xhr.responseText;
+ };
+ response.data = function() {
+ return JSON.parse(response.content());
+ };
+ response.headers = function () {
+ var header_str = xhr.getAllResponseHeaders();
+ var headers_raw = header_str.split(/\r?\n/);
+ var headers = {};
+ _.each(headers_raw, function (h) {
+ var m = /^(.*?):(?:\s+)(.*)$/.exec(h);
+ if (m && m.length === 3)
+ headers[m[1].toLowerCase()] = m[2];
+ });
+ return headers;
+ };
+
+ var error = null;
+ if (xhr.status >= 400)
+ error = new Error("failed");
+
+ callback(error, response);
+ }
+ }
+ };
+
+ // send it on its way
+ xhr.send(content);
+
+ } catch (err) {
+ callback(err);
+ }
+
+ };
+
+
+})();
+
@@ -0,0 +1,38 @@
+
+Meteor.http = Meteor.http || {};
+
+(function() {
+
+ Meteor.http._encodeParams = function(params) {
+ var buf = [];
+ _.each(params, function(value, key) {
+ if (buf.length)
+ buf.push('&');
+ buf.push(encodeURIComponent(key), '=', encodeURIComponent(value));
+ });
+ return buf.join('').replace(/%20/g, '+');
+ };
+
+ Meteor.http._buildUrl = function(before_qmark, from_qmark, opt_query, opt_params) {
+ var url_without_query = before_qmark;
+ var query = from_qmark ? from_qmark.slice(1) : null;
+
+ if (typeof opt_query === "string")
+ query = String(opt_query);
+
+ if (opt_params) {
+ query = query || "";
+ var prms = Meteor.http._encodeParams(opt_params);
+ if (query && prms)
+ query += '&';
+ query += prms;
+ }
+
+ var url = url_without_query;
+ if (query !== null)
+ url += ("?"+query);
+
+ return url;
+ };
+
+})();
Oops, something went wrong.

0 comments on commit 7d269cd

Please sign in to comment.