mach
Version:
HTTP for JavaScript
232 lines (199 loc) • 7.36 kB
JavaScript
"use strict";
/* jshint -W058 */
var d = require("describe-property");
var isBinary = require("bodec").isBinary;
var decodeBase64 = require("./utils/decodeBase64");
var encodeBase64 = require("./utils/encodeBase64");
var stringifyQuery = require("./utils/stringifyQuery");
var Promise = require("./utils/Promise");
var Location = require("./Location");
var Message = require("./Message");
function locationPropertyAlias(name) {
return d.gs(function () {
return this.location[name];
}, function (value) {
this.location[name] = value;
});
}
function defaultErrorHandler(error) {
if (typeof console !== "undefined" && console.error) {
console.error(error && error.stack || error);
} else {
throw error; // Don't silently swallow errors!
}
}
function defaultCloseHandler() {}
function defaultApp(conn) {
conn.status = 404;
conn.response.contentType = "text/plain";
conn.response.content = "Not found: " + conn.method + " " + conn.path;
}
/**
* An HTTP connection that acts as the asynchronous primitive for
* the duration of the request/response cycle.
*
* Important features are:
*
* - request A Message representing the request being made. In
* a server environment, this is an "incoming" message
* that was probably generated by a web browser or some
* other consumer. In a client environment, this is an
* "outgoing" message that we send to a remote server.
* - response A Message representing the response to the request.
* In a server environment, this is an "outgoing" message
* that will be sent back to the client. In a client
* environment, this is the response that was received
* from the remote server.
* - method The HTTP method that the request uses
* - location The URL of the request. In a server environment, this
* is derived from the URL path used in the request as
* well as a combination of the Host, X-Forwarded-* and
* other relevant headers.
* - version The version of HTTP used in the request
* - status The HTTP status code of the response
* - statusText The HTTP status text that corresponds to the status
* - responseText This is a special property that contains the entire
* content of the response. It is present by default when
* making client requests for convenience, but may also be
* disabled when you need to stream the response.
*
* Options may be any of the following:
*
* - content The request content, defaults to ""
* - headers The request headers, defaults to {}
* - method The request HTTP method, defaults to "GET"
* - location/url The request Location or URL
* - params The request params
* - onError A function that is called when there is an error
* - onClose A function that is called when the request closes
*
* The options may also be a URL string to specify the URL.
*/
function Connection(options) {
options = options || {};
var location;
if (typeof options === "string") {
location = options; // options may be a URL string.
} else if (options.location || options.url) {
location = options.location || options.url;
} else if (typeof window === "object") {
location = window.location.href;
}
this.location = location;
this.version = options.version || "1.1";
this.method = options.method;
this.onError = (options.onError || defaultErrorHandler).bind(this);
this.onClose = (options.onClose || defaultCloseHandler).bind(this);
this.request = new Message(options.content, options.headers);
this.response = new Message();
// Params may be given as an object.
if (options.params) {
if (this.method === "GET" || this.method === "HEAD") {
this.query = options.params;
} else {
this.request.contentType = "application/x-www-form-urlencoded";
this.request.content = stringifyQuery(options.params);
}
}
this.withCredentials = options.withCredentials || false;
this.remoteHost = options.remoteHost || null;
this.remoteUser = options.remoteUser || null;
this.basename = "";
this.responseText = null;
this.status = 200;
}
Object.defineProperties(Connection.prototype, {
/**
* The method used in the request.
*/
method: d.gs(function () {
return this._method;
}, function (value) {
this._method = typeof value === "string" ? value.toUpperCase() : "GET";
}),
/**
* The Location of the request.
*/
location: d.gs(function () {
return this._location;
}, function (value) {
this._location = value instanceof Location ? value : new Location(value);
}),
href: locationPropertyAlias("href"),
protocol: locationPropertyAlias("protocol"),
host: locationPropertyAlias("host"),
hostname: locationPropertyAlias("hostname"),
port: locationPropertyAlias("port"),
search: locationPropertyAlias("search"),
queryString: locationPropertyAlias("queryString"),
query: locationPropertyAlias("query"),
/**
* True if the request uses SSL, false otherwise.
*/
isSSL: d.gs(function () {
return this.protocol === "https:";
}),
/**
* The username:password used in the request, an empty string
* if no auth was provided.
*/
auth: d.gs(function () {
var header = this.request.headers.Authorization;
if (header) {
var parts = header.split(" ", 2);
var scheme = parts[0];
if (scheme.toLowerCase() === "basic") return decodeBase64(parts[1]);
return header;
}
return this.location.auth;
}, function (value) {
var headers = this.request.headers;
if (value && typeof value === "string") {
headers.Authorization = "Basic " + encodeBase64(value);
} else {
delete headers.Authorization;
}
}),
/**
* The portion of the original URL path that is still relevant
* for request processing.
*/
pathname: d.gs(function () {
return this.location.pathname.replace(this.basename, "") || "/";
}, function (value) {
this.location.pathname = this.basename + value;
}),
/**
* The URL path with query string.
*/
path: d.gs(function () {
return this.pathname + this.search;
}, function (value) {
this.location.path = this.basename + value;
}),
/**
* Calls the given `app` with this connection as the only argument.
* as the first argument and returns a promise for a Response.
*/
call: d(function (app) {
app = app || defaultApp;
var conn = this;
try {
return Promise.resolve(app(conn)).then(function (value) {
if (value == null) return;
if (typeof value === "number") {
conn.status = value;
} else if (typeof value === "string" || isBinary(value) || typeof value.pipe === "function") {
conn.response.content = value;
} else {
if (value.headers != null) conn.response.headers = value.headers;
if (value.content != null) conn.response.content = value.content;
if (value.status != null) conn.status = value.status;
}
});
} catch (error) {
return Promise.reject(error);
}
})
});
module.exports = Connection;