Comparing version 0.0.1 to 0.0.2
@@ -1,2 +0,1 @@ | ||
/** | ||
@@ -10,3 +9,6 @@ * Module dependencies. | ||
var context = require('./context'); | ||
var request = require('./request'); | ||
var response = require('./response'); | ||
var Cookies = require('cookies'); | ||
var Keygrip = require('keygrip'); | ||
var Stream = require('stream'); | ||
@@ -43,4 +45,5 @@ var http = require('http'); | ||
this.middleware = []; | ||
this.Context = createContext(); | ||
this.context(context); | ||
this.context = Object.create(context); | ||
this.request = Object.create(request); | ||
this.response = Object.create(response); | ||
} | ||
@@ -65,2 +68,3 @@ | ||
app.listen = function(){ | ||
debug('listen'); | ||
var server = http.createServer(this.callback()); | ||
@@ -85,61 +89,75 @@ return server.listen.apply(server, arguments); | ||
/** | ||
* Mixin `obj` to this app's context. | ||
* Return a request handler callback | ||
* for node's native http server. | ||
* | ||
* app.context({ | ||
* get something(){ | ||
* return 'hi'; | ||
* }, | ||
* @return {Function} | ||
* @api public | ||
*/ | ||
app.callback = function(){ | ||
var mw = [respond].concat(this.middleware); | ||
var gen = compose(mw); | ||
var self = this; | ||
return function(req, res, next){ | ||
var ctx = self.createContext(req, res); | ||
next = next || ctx.onerror; | ||
ctx.socket.once('error', next); | ||
co.call(ctx, gen)(next); | ||
} | ||
}; | ||
/** | ||
* Set signed cookie keys. | ||
* | ||
* set something(val){ | ||
* this._something = val; | ||
* }, | ||
* These are passed to [KeyGrip](https://github.com/jed/keygrip), | ||
* however you may also pass your own `KeyGrip` instance. For | ||
* example the following are acceptable: | ||
* | ||
* render: function(){ | ||
* this.body = '<html></html>'; | ||
* } | ||
* }); | ||
* app.keys = ['im a newer secret', 'i like turtle']; | ||
* app.keys = new KeyGrip(['im a newer secret', 'i like turtle'], 'sha256'); | ||
* | ||
* @param {Object} obj | ||
* @return {Application} self | ||
* @param {Array|KeyGrip} keys | ||
* @api public | ||
*/ | ||
app.context = function(obj){ | ||
var ctx = this.Context.prototype; | ||
var names = Object.getOwnPropertyNames(obj); | ||
app.__defineSetter__('keys', function(keys){ | ||
var ok = Array.isArray(keys) || keys instanceof Keygrip; | ||
debug('keys %j', keys); | ||
if (!ok) throw new TypeError('app.keys must be an array or Keygrip'); | ||
if (!(keys instanceof Keygrip)) keys = new Keygrip(keys); | ||
this._keys = keys; | ||
}); | ||
debug('context: %j', names); | ||
names.forEach(function(name){ | ||
if (Object.getOwnPropertyDescriptor(ctx, name)) { | ||
debug('context: overwriting %j', name); | ||
} | ||
var descriptor = Object.getOwnPropertyDescriptor(obj, name); | ||
Object.defineProperty(ctx, name, descriptor); | ||
}); | ||
return this; | ||
}; | ||
/** | ||
* Return a request handler callback | ||
* for node's native http server. | ||
* Get `Keygrip` instance. | ||
* | ||
* @return {Function} | ||
* @return {Keygrip} | ||
* @api public | ||
*/ | ||
app.callback = function(){ | ||
var mw = [respond].concat(this.middleware); | ||
var fn = compose(mw)(downstream); | ||
var self = this; | ||
app.__defineGetter__('keys', function(){ | ||
return this._keys; | ||
}); | ||
return function(req, res){ | ||
var ctx = new self.Context(self, req, res); | ||
/** | ||
* Initialize a new context. | ||
* | ||
* @api private | ||
*/ | ||
co.call(ctx, function *(){ | ||
yield fn; | ||
})(function(err){ | ||
if (err) ctx.onerror(err); | ||
}); | ||
} | ||
app.createContext = function(req, res){ | ||
var context = Object.create(this.context); | ||
var request = context.request = Object.create(this.request); | ||
var response = context.response = Object.create(this.response); | ||
context.app = request.app = response.app = this; | ||
context.req = request.req = response.req = req; | ||
context.res = request.res = response.res = res; | ||
request.ctx = response.ctx = context; | ||
request.response = response; | ||
response.request = request; | ||
context.onerror = context.onerror.bind(context); | ||
context.originalUrl = request.originalUrl = req.url; | ||
context.cookies = new Cookies(req, res, this.keys); | ||
return context; | ||
}; | ||
@@ -164,79 +182,56 @@ | ||
function respond(next){ | ||
return function *respond(){ | ||
this.status = 200; | ||
if (this.app.poweredBy) this.set('X-Powered-By', 'koa'); | ||
function *respond(next){ | ||
this.status = 200; | ||
if (this.app.poweredBy) this.set('X-Powered-By', 'koa'); | ||
yield next; | ||
yield next; | ||
var app = this.app; | ||
var res = this.res; | ||
var body = this.body; | ||
var head = 'HEAD' == this.method; | ||
var noContent = 204 == this.status || 304 == this.status; | ||
var res = this.res; | ||
var body = this.body; | ||
var head = 'HEAD' == this.method; | ||
var noContent = ~[204, 205, 304].indexOf(this.status); | ||
// 404 | ||
if (null == body && 200 == this.status) { | ||
this.status = 404; | ||
} | ||
// 404 | ||
if (null == body && 200 == this.status) { | ||
this.status = 404; | ||
} | ||
// ignore body | ||
if (noContent) return res.end(); | ||
// ignore body | ||
if (noContent) return res.end(); | ||
// status body | ||
if (null == body) { | ||
this.type = 'text'; | ||
body = http.STATUS_CODES[this.status]; | ||
} | ||
// status body | ||
if (null == body) { | ||
this.type = 'text'; | ||
body = http.STATUS_CODES[this.status]; | ||
} | ||
// Buffer body | ||
if (Buffer.isBuffer(body)) { | ||
if (head) return res.end(); | ||
return res.end(body); | ||
} | ||
// Buffer body | ||
if (Buffer.isBuffer(body)) { | ||
if (head) return res.end(); | ||
return res.end(body); | ||
} | ||
// string body | ||
if ('string' == typeof body) { | ||
if (head) return res.end(); | ||
return res.end(body); | ||
} | ||
// Stream body | ||
if (body instanceof Stream) { | ||
if (!~body.listeners('error').indexOf(this.onerror)) body.on('error', this.onerror); | ||
if (head) return res.end(); | ||
return body.pipe(res); | ||
} | ||
// body: json | ||
body = JSON.stringify(body, null, this.app.jsonSpaces); | ||
this.length = Buffer.byteLength(body); | ||
// string body | ||
if ('string' == typeof body) { | ||
if (head) return res.end(); | ||
res.end(body); | ||
return res.end(body); | ||
} | ||
} | ||
/** | ||
* Default downstream middleware. | ||
* | ||
* @api private | ||
*/ | ||
// Stream body | ||
if (body instanceof Stream) { | ||
if (!~body.listeners('error').indexOf(this.onerror)) body.on('error', this.onerror); | ||
function *downstream(){} | ||
if (head) { | ||
if (body.close) body.close(); | ||
return res.end(); | ||
} | ||
/** | ||
* Create a new `Context` constructor. | ||
* | ||
* @return {Function} | ||
* @api private | ||
*/ | ||
return body.pipe(res); | ||
} | ||
function createContext() { | ||
return function Context(app, req, res){ | ||
this.app = app; | ||
this.req = req; | ||
this.res = res; | ||
this.cookies = new Cookies(req, res); | ||
this.onerror = this.onerror.bind(this); | ||
} | ||
// body: json | ||
body = JSON.stringify(body, null, this.app.jsonSpaces); | ||
this.length = Buffer.byteLength(body); | ||
if (head) return res.end(); | ||
res.end(body); | ||
} |
@@ -0,1 +1,2 @@ | ||
/** | ||
@@ -6,15 +7,3 @@ * Module dependencies. | ||
var debug = require('debug')('koa:context'); | ||
var Negotiator = require('negotiator'); | ||
var statuses = require('./status'); | ||
var qs = require('querystring'); | ||
var Stream = require('stream'); | ||
var fresh = require('fresh'); | ||
var http = require('http'); | ||
var path = require('path'); | ||
var mime = require('mime'); | ||
var basename = path.basename; | ||
var extname = path.extname; | ||
var url = require('url'); | ||
var parse = url.parse; | ||
var stringify = url.format; | ||
@@ -28,3 +17,3 @@ /** | ||
/** | ||
* Return request header. | ||
* Inspect implementation. | ||
* | ||
@@ -35,8 +24,8 @@ * @return {Object} | ||
get header() { | ||
return this.req.headers; | ||
inspect: function(){ | ||
return this.toJSON(); | ||
}, | ||
/** | ||
* Return response header. | ||
* Return JSON representation. | ||
* | ||
@@ -47,1009 +36,459 @@ * @return {Object} | ||
get responseHeader() { | ||
// TODO: wtf | ||
return this.res._headers || {}; | ||
toJSON: function(){ | ||
return { | ||
request: this.request, | ||
response: this.response | ||
} | ||
}, | ||
/** | ||
* Return response status string. | ||
* Throw an error with `msg` and optional `status` | ||
* defaulting to 500. Note that these are user-level | ||
* errors, and the message may be exposed to the client. | ||
* | ||
* @return {String} | ||
* this.throw(403) | ||
* this.throw('name required', 400) | ||
* this.throw('something exploded') | ||
* | ||
* @param {String|Number} msg | ||
* @param {Number} status | ||
* @api public | ||
*/ | ||
get statusString() { | ||
return http.STATUS_CODES[this.status]; | ||
throw: function(msg, status){ | ||
// TODO: switch order... feels weird now that im used to express | ||
if ('number' == typeof msg) { | ||
var tmp = msg; | ||
msg = http.STATUS_CODES[tmp]; | ||
status = tmp; | ||
} | ||
var err = new Error(msg); | ||
err.status = status || 500; | ||
err.expose = true; | ||
throw err; | ||
}, | ||
/** | ||
* Get request URL. | ||
* Alias for .throw() for backwards compatibility. | ||
* Do not use - will be removed in the future. | ||
* | ||
* @return {String} | ||
* @api public | ||
* @param {String|Number} msg | ||
* @param {Number} status | ||
* @api private | ||
*/ | ||
get url() { | ||
return this.req.url; | ||
error: function(msg, status){ | ||
this.throw(msg, status); | ||
}, | ||
/** | ||
* Set request URL. | ||
* Default error handling. | ||
* | ||
* @api public | ||
* @param {Error} err | ||
* @api private | ||
*/ | ||
set url(val) { | ||
this.req.url = val; | ||
}, | ||
onerror: function(err){ | ||
// don't do anything if there is no error. | ||
// this allows you to pass `this.onerror` | ||
// to node-style callbacks. | ||
if (!err) return; | ||
/** | ||
* Get request method. | ||
* | ||
* @return {String} | ||
* @api public | ||
*/ | ||
// nothing we can do here other | ||
// than delegate to the app-level | ||
// handler and log. | ||
if (this.headerSent || !this.socket.writable) { | ||
err.headerSent = true; | ||
this.app.emit('error', err, this); | ||
return; | ||
} | ||
get method() { | ||
return this.req.method; | ||
// delegate | ||
this.app.emit('error', err, this); | ||
// force text/plain | ||
this.type = 'text'; | ||
// ENOENT support | ||
if ('ENOENT' == err.code) err.status = 404; | ||
// default to 500 | ||
err.status = err.status || 500; | ||
// respond | ||
var code = http.STATUS_CODES[err.status]; | ||
var msg = err.expose ? err.message : code; | ||
this.status = err.status; | ||
this.res.end(msg); | ||
}, | ||
/** | ||
* Set request method. | ||
* | ||
* @param {String} val | ||
* @api public | ||
* Delegate to Request#header. | ||
*/ | ||
set method(val) { | ||
this.req.method = val; | ||
get header() { | ||
return this.request.header; | ||
}, | ||
/** | ||
* Get response status code. | ||
* | ||
* @return {Number} | ||
* @api public | ||
* Delegate to Request#url. | ||
*/ | ||
get status() { | ||
return this.res.statusCode; | ||
get url() { | ||
return this.request.url; | ||
}, | ||
get statusCode() { | ||
return this.res.statusCode; | ||
}, | ||
/** | ||
* Set response status code. | ||
* | ||
* @param {Number|String} val | ||
* @api public | ||
* Delegate to Request#url=. | ||
*/ | ||
set status(val) { | ||
if ('string' == typeof val) { | ||
var n = statuses[val.toLowerCase()]; | ||
if (!n) throw new Error(statusError(val)); | ||
val = n; | ||
} | ||
this.res.statusCode = val; | ||
var noContent = 304 == this.status || 204 == this.status; | ||
if (noContent && this.body) this.body = null; | ||
set url(val) { | ||
this.request.url = val; | ||
}, | ||
set statusCode(val) { | ||
this.status = val; | ||
}, | ||
/** | ||
* Get response body. | ||
* | ||
* @return {Mixed} | ||
* @api public | ||
* Delegate to Request#method. | ||
*/ | ||
get body() { | ||
return this._body; | ||
get method() { | ||
return this.request.method; | ||
}, | ||
/** | ||
* Set response body. | ||
* | ||
* @param {String|Buffer|Object|Stream} val | ||
* @api public | ||
* Delegate to Request#method=. | ||
*/ | ||
set body(val) { | ||
this._body = val; | ||
// no content | ||
if (null == val) { | ||
var s = this.status; | ||
this.status = 304 == s ? 304 : 204; | ||
this.res.removeHeader('Content-Type'); | ||
this.res.removeHeader('Content-Length'); | ||
this.res.removeHeader('Transfer-Encoding'); | ||
return; | ||
} | ||
// set the content-type only if not yet set | ||
var setType = !this.responseHeader['content-type']; | ||
// string | ||
if ('string' == typeof val) { | ||
if (setType) this.type = ~val.indexOf('<') ? 'html' : 'text'; | ||
this.length = Buffer.byteLength(val); | ||
return; | ||
} | ||
// buffer | ||
if (Buffer.isBuffer(val)) { | ||
if (setType) this.type = 'bin'; | ||
this.length = val.length; | ||
return; | ||
} | ||
// stream | ||
if (val instanceof Stream) { | ||
if (setType) this.type = 'bin'; | ||
return; | ||
} | ||
// json | ||
this.type = 'json'; | ||
set method(val) { | ||
this.request.method = val; | ||
}, | ||
/** | ||
* Get request pathname. | ||
* | ||
* @return {String} | ||
* @api public | ||
* Delegate to Response#status. | ||
*/ | ||
get path() { | ||
var c = this._pathcache = this._pathcache || {}; | ||
return c[this.url] || (c[this.url] = parse(this.url).pathname); | ||
get status() { | ||
return this.response.status; | ||
}, | ||
/** | ||
* Set pathname, retaining the query-string when present. | ||
* | ||
* @param {String} path | ||
* @api public | ||
* Delegate to Response#status=. | ||
*/ | ||
set path(path) { | ||
var url = parse(this.url); | ||
url.pathname = path; | ||
this.url = stringify(url); | ||
set status(val) { | ||
this.response.status = val; | ||
}, | ||
/** | ||
* Get parsed query-string. | ||
* | ||
* @return {Object} | ||
* @api public | ||
* Delegate to Response#body. | ||
*/ | ||
get query() { | ||
var str = this.querystring; | ||
if (!str) return {}; | ||
var c = this._querycache = this._querycache || {}; | ||
return c[str] || (c[str] = qs.parse(str)); | ||
get body() { | ||
return this.response.body; | ||
}, | ||
/** | ||
* Set query-string as an object. | ||
* | ||
* @param {Object} obj | ||
* @api public | ||
* Delegate to Response#body=. | ||
*/ | ||
set query(obj) { | ||
this.querystring = qs.stringify(obj); | ||
set body(val) { | ||
this.response.body = val; | ||
}, | ||
/** | ||
* Get query string. | ||
* | ||
* @return {String} | ||
* @api public | ||
* Delegate to Request#path. | ||
*/ | ||
get querystring() { | ||
var c = this._qscache = this._qscache || {}; | ||
return c[this.url] || (c[this.url] = parse(this.url).query || ''); | ||
get path() { | ||
return this.request.path; | ||
}, | ||
/** | ||
* Set querystring. | ||
* | ||
* @param {String} str | ||
* @api public | ||
* Delegate to Request#path=. | ||
*/ | ||
set querystring(str) { | ||
var url = parse(this.url); | ||
url.search = str; | ||
this.url = stringify(url); | ||
set path(val) { | ||
this.request.path = val; | ||
}, | ||
/** | ||
* Parse the "Host" header field hostname | ||
* and support X-Forwarded-Host when a | ||
* proxy is enabled. | ||
* | ||
* @return {String} | ||
* @api public | ||
* Delegate to Request#query. | ||
*/ | ||
get host() { | ||
var proxy = this.app.proxy; | ||
var host = proxy && this.get('X-Forwarded-Host'); | ||
host = host || this.get('Host'); | ||
if (!host) return; | ||
return host.split(/\s*,\s*/)[0].split(':')[0]; | ||
get query() { | ||
return this.request.query; | ||
}, | ||
/** | ||
* Check if the request is fresh, aka | ||
* Last-Modified and/or the ETag | ||
* still match. | ||
* | ||
* @return {Boolean} | ||
* @api public | ||
* Delegate to Request#query=. | ||
*/ | ||
get fresh() { | ||
var method = this.method; | ||
var s = this.status; | ||
// GET or HEAD for weak freshness validation only | ||
if ('GET' != method && 'HEAD' != method) return false; | ||
// 2xx or 304 as per rfc2616 14.26 | ||
if ((s >= 200 && s < 300) || 304 == s) { | ||
return fresh(this.header, this.responseHeader); | ||
} | ||
return false; | ||
set query(val) { | ||
this.request.query = val; | ||
}, | ||
/** | ||
* Check if the request is stale, aka | ||
* "Last-Modified" and / or the "ETag" for the | ||
* resource has changed. | ||
* | ||
* @return {Boolean} | ||
* @api public | ||
* Delegate to Request#querystring. | ||
*/ | ||
get stale() { | ||
return !this.fresh; | ||
get querystring() { | ||
return this.request.querystring; | ||
}, | ||
/** | ||
* Check if the request is idempotent. | ||
* | ||
* @return {Boolean} | ||
* @api public | ||
* Delegate to Request#querystring=. | ||
*/ | ||
get idempotent() { | ||
return 'GET' == this.method | ||
|| 'HEAD' == this.method; | ||
set querystring(val) { | ||
this.request.querystring = val; | ||
}, | ||
/** | ||
* Return the request socket. | ||
* | ||
* @return {Connection} | ||
* @api public | ||
* Delegate to Request#search. | ||
*/ | ||
get socket() { | ||
// TODO: TLS | ||
return this.req.socket; | ||
get search() { | ||
return this.request.search; | ||
}, | ||
/** | ||
* Return parsed Content-Length when present. | ||
* | ||
* @return {Number} | ||
* @api public | ||
* Delegate to Request#search=. | ||
*/ | ||
get length() { | ||
var len = this.get('Content-Length'); | ||
if (null == len) return; | ||
return ~~len; | ||
set search(val) { | ||
this.request.search = val; | ||
}, | ||
/** | ||
* Set Content-Length field to `n`. | ||
* | ||
* @param {Number} n | ||
* @api public | ||
* Delegate to Request#host. | ||
*/ | ||
set length(n) { | ||
this.set('Content-Length', n); | ||
get host() { | ||
return this.request.host; | ||
}, | ||
/** | ||
* Return parsed response Content-Length when present. | ||
* | ||
* @return {Number} | ||
* @api public | ||
* Delegate to Request#fresh. | ||
*/ | ||
get responseLength() { | ||
var len = this.responseHeader['content-length']; | ||
var body = this.body; | ||
if (null == len) { | ||
if (!body) return; | ||
if ('string' == typeof body) return Buffer.byteLength(body); | ||
return body.length; | ||
} | ||
return ~~len; | ||
get fresh() { | ||
return this.request.fresh; | ||
}, | ||
/** | ||
* Return the protocol string "http" or "https" | ||
* when requested with TLS. When the proxy setting | ||
* is enabled the "X-Forwarded-Proto" header | ||
* field will be trusted. If you're running behind | ||
* a reverse proxy that supplies https for you this | ||
* may be enabled. | ||
* | ||
* @return {String} | ||
* @api public | ||
* Delegate to Request#stale. | ||
*/ | ||
get protocol() { | ||
var proxy = this.app.proxy; | ||
if (this.socket.encrypted) return 'https'; | ||
if (!proxy) return 'http'; | ||
var proto = this.get('X-Forwarded-Proto') || 'http'; | ||
return proto.split(/\s*,\s*/)[0]; | ||
get stale() { | ||
return this.request.stale; | ||
}, | ||
/** | ||
* Short-hand for: | ||
* | ||
* this.protocol == 'https' | ||
* | ||
* @return {Boolean} | ||
* @api public | ||
* Delegate to Request#idempotent. | ||
*/ | ||
get secure() { | ||
return 'https' == this.protocol; | ||
get idempotent() { | ||
return this.request.idempotent; | ||
}, | ||
/** | ||
* Return the remote address, or when | ||
* `app.proxy` is `true` return | ||
* the upstream addr. | ||
* | ||
* @return {String} | ||
* @api public | ||
* Delegate to Request#socket. | ||
*/ | ||
get ip() { | ||
return this.ips[0] || this.connection.remoteAddress; | ||
get socket() { | ||
return this.request.socket; | ||
}, | ||
/** | ||
* When `app.proxy` is `true`, parse | ||
* the "X-Forwarded-For" ip address list. | ||
* | ||
* For example if the value were "client, proxy1, proxy2" | ||
* you would receive the array `["client", "proxy1", "proxy2"]` | ||
* where "proxy2" is the furthest down-stream. | ||
* | ||
* @return {Array} | ||
* @api public | ||
* Delegate to Request#length. | ||
*/ | ||
get ips() { | ||
var proxy = this.app.proxy; | ||
var val = this.get('X-Forwarded-For'); | ||
return proxy && val | ||
? val.split(/ *, */) | ||
: []; | ||
get length() { | ||
return this.request.length; | ||
}, | ||
/** | ||
* Return subdomains as an array. | ||
* | ||
* Subdomains are the dot-separated parts of the host before the main domain of | ||
* the app. By default, the domain of the app is assumed to be the last two | ||
* parts of the host. This can be changed by setting `app.subdomainOffset`. | ||
* | ||
* For example, if the domain is "tobi.ferrets.example.com": | ||
* If `app.subdomainOffset` is not set, this.subdomains is `["ferrets", "tobi"]`. | ||
* If `app.subdomainOffset` is 3, this.subdomains is `["tobi"]`. | ||
* | ||
* @return {Array} | ||
* @api public | ||
* Delegate to Request#length. | ||
*/ | ||
get subdomains() { | ||
var offset = this.app.subdomainOffset; | ||
return (this.host || '') | ||
.split('.') | ||
.reverse() | ||
.slice(offset); | ||
set length(val) { | ||
this.response.length = val; | ||
}, | ||
/** | ||
* Check if the given `type(s)` is acceptable, returning | ||
* the best match when true, otherwise `undefined`, in which | ||
* case you should respond with 406 "Not Acceptable". | ||
* | ||
* The `type` value may be a single mime type string | ||
* such as "application/json", the extension name | ||
* such as "json" or an array `["json", "html", "text/plain"]`. When a list | ||
* or array is given the _best_ match, if any is returned. | ||
* | ||
* Examples: | ||
* | ||
* // Accept: text/html | ||
* this.accepts('html'); | ||
* // => "html" | ||
* | ||
* // Accept: text/*, application/json | ||
* this.accepts('html'); | ||
* // => "html" | ||
* this.accepts('text/html'); | ||
* // => "text/html" | ||
* this.accepts('json', 'text'); | ||
* // => "json" | ||
* this.accepts('application/json'); | ||
* // => "application/json" | ||
* | ||
* // Accept: text/*, application/json | ||
* this.accepts('image/png'); | ||
* this.accepts('png'); | ||
* // => undefined | ||
* | ||
* // Accept: text/*;q=.5, application/json | ||
* this.accepts(['html', 'json']); | ||
* this.accepts('html', 'json'); | ||
* // => "json" | ||
* | ||
* @param {String|Array} type(s)... | ||
* @return {String} | ||
* @api public | ||
* Delegate to Request#protocol. | ||
*/ | ||
accepts: function(types){ | ||
// TODO: memoize | ||
if (!Array.isArray(types)) types = [].slice.call(arguments); | ||
var normalized = types.map(extToMime); | ||
var n = new Negotiator(this.req); | ||
var accepts = n.preferredMediaTypes(normalized); | ||
var first = accepts[0]; | ||
if (!first) return false; | ||
return types[normalized.indexOf(first)]; | ||
get protocol() { | ||
return this.request.protocol; | ||
}, | ||
/** | ||
* Return accepted encodings. | ||
* | ||
* Given `Accept-Encoding: gzip, deflate` | ||
* an array sorted by quality is returned: | ||
* | ||
* ['gzip', 'deflate'] | ||
* | ||
* @return {Array} | ||
* @api public | ||
* Delegate to Request#secure. | ||
*/ | ||
get acceptedEncodings() { | ||
var n = new Negotiator(this.req); | ||
return n.preferredEncodings(); | ||
get secure() { | ||
return this.request.secure; | ||
}, | ||
/** | ||
* Return accepted charsets. | ||
* | ||
* Given `Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5` | ||
* an array sorted by quality is returned: | ||
* | ||
* ['utf-8', 'utf-7', 'iso-8859-1'] | ||
* | ||
* @return {Array} | ||
* @api public | ||
* Delegate to Request#ip. | ||
*/ | ||
get acceptedCharsets() { | ||
var n = new Negotiator(this.req); | ||
return n.preferredCharsets(); | ||
get ip() { | ||
return this.request.ip; | ||
}, | ||
/** | ||
* Return accepted languages. | ||
* | ||
* Given `Accept-Language: en;q=0.8, es, pt` | ||
* an array sorted by quality is returned: | ||
* | ||
* ['es', 'pt', 'en'] | ||
* | ||
* @return {Array} | ||
* @api public | ||
* Delegate to Request#ips. | ||
*/ | ||
get acceptedLanguages() { | ||
var n = new Negotiator(this.req); | ||
return n.preferredLanguages(); | ||
get ips() { | ||
return this.request.ips; | ||
}, | ||
/** | ||
* Return accepted media types. | ||
* | ||
* Given `Accept: application/*;q=0.2, image/jpeg;q=0.8, text/html` | ||
* an array sorted by quality is returned: | ||
* | ||
* ['text/html', 'image/jpeg', 'application/*'] | ||
* | ||
* @return {Array} | ||
* @api public | ||
* Delegate to Request#subdomains. | ||
*/ | ||
get accepted() { | ||
var n = new Negotiator(this.req); | ||
return n.preferredMediaTypes(); | ||
get subdomains() { | ||
return this.request.subdomains; | ||
}, | ||
/** | ||
* Check if a header has been written to the socket. | ||
* | ||
* @return {Boolean} | ||
* @api public | ||
* Delegate to Response#headerSent. | ||
*/ | ||
get headerSent() { | ||
return this.res.headersSent; | ||
return this.response.headerSent; | ||
}, | ||
get headersSent() { | ||
return this.res.headersSent; | ||
}, | ||
/** | ||
* Throw an error with `msg` and optional `status` | ||
* defaulting to 500. Note that these are user-level | ||
* errors, and the message may be exposed to the client. | ||
* | ||
* this.error(403) | ||
* this.error('name required', 400) | ||
* this.error('something exploded') | ||
* | ||
* @param {String|Number} msg | ||
* @param {Number} status | ||
* @api public | ||
* Delegate to Response#type=. | ||
*/ | ||
error: function(msg, status){ | ||
// TODO: switch order... feels weird now that im used to express | ||
if ('number' == typeof msg) { | ||
var tmp = msg; | ||
msg = http.STATUS_CODES[tmp]; | ||
status = tmp; | ||
} | ||
var err = new Error(msg); | ||
err.status = status || 500; | ||
err.expose = true; | ||
throw err; | ||
set type(val) { | ||
this.response.type = val; | ||
}, | ||
/** | ||
* Default error handling. | ||
* | ||
* @param {Error} err | ||
* @api private | ||
* Delegate to Request#type. | ||
*/ | ||
onerror: function(err){ | ||
// don't do anything if there is no error. | ||
// this allows you to pass `this.onerror` | ||
// to node-style callbacks. | ||
if (!err) return; | ||
// nothing we can do here other | ||
// than delegate to the app-level | ||
// handler and log. | ||
if (this.headerSent) { | ||
err.headerSent = true; | ||
this.app.emit('error', err, this); | ||
return; | ||
} | ||
// delegate | ||
this.app.emit('error', err, this); | ||
// force text/plain | ||
this.type = 'text'; | ||
// ENOENT support | ||
if ('ENOENT' == err.code) err.status = 404; | ||
// default to 500 | ||
err.status = err.status || 500; | ||
// respond | ||
var code = http.STATUS_CODES[err.status]; | ||
var msg = err.expose ? err.message : code; | ||
this.status = err.status; | ||
this.res.end(msg); | ||
get type() { | ||
return this.request.type; | ||
}, | ||
/** | ||
* Vary on `field`. | ||
* | ||
* @param {String} field | ||
* @api public | ||
* Delegate to Response#lastModified=. | ||
*/ | ||
vary: function(field){ | ||
this.append('Vary', field); | ||
set lastModified(val) { | ||
this.response.lastModified = val; | ||
}, | ||
/** | ||
* Check if the incoming request contains the "Content-Type" | ||
* header field, and it contains the give mime `type`. | ||
* | ||
* Examples: | ||
* | ||
* // With Content-Type: text/html; charset=utf-8 | ||
* this.is('html'); | ||
* this.is('text/html'); | ||
* this.is('text/*'); | ||
* // => true | ||
* | ||
* // When Content-Type is application/json | ||
* this.is('json'); | ||
* this.is('application/json'); | ||
* this.is('application/*'); | ||
* // => true | ||
* | ||
* this.is('html'); | ||
* // => false | ||
* | ||
* @param {String} type | ||
* @return {Boolean} | ||
* @api public | ||
* Delegate to Response#etag=. | ||
*/ | ||
is: function(type){ | ||
var ct = this.type; | ||
if (!ct) return false; | ||
ct = ct.split(';')[0]; | ||
// extension given | ||
if (!~type.indexOf('/')) type = mime.lookup(type); | ||
// type or subtype match | ||
if (~type.indexOf('*')) { | ||
type = type.split('/'); | ||
ct = ct.split('/'); | ||
if ('*' == type[0] && type[1] == ct[1]) return true; | ||
if ('*' == type[1] && type[0] == ct[0]) return true; | ||
return false; | ||
} | ||
// exact match | ||
return type == ct; | ||
set etag(val) { | ||
this.response.etag = val; | ||
}, | ||
/** | ||
* Perform a 302 redirect to `url`. | ||
* | ||
* The string "back" is special-cased | ||
* to provide Referrer support, when Referrer | ||
* is not present `alt` or "/" is used. | ||
* | ||
* Examples: | ||
* | ||
* this.redirect('back'); | ||
* this.redirect('back', '/index.html'); | ||
* this.redirect('/login'); | ||
* this.redirect('http://google.com'); | ||
* | ||
* @param {String} url | ||
* @param {String} alt | ||
* @api public | ||
* Delegate to Request#remove(). | ||
*/ | ||
redirect: function(url, alt){ | ||
if ('back' == url) url = this.get('Referrer') || alt || '/'; | ||
this.set('Location', url); | ||
this.status = 302; | ||
// html | ||
if (this.accepts('html')) { | ||
url = escape(url); | ||
this.type = 'text/html; charset=utf-8'; | ||
this.body = 'Redirecting to <a href="' + url + '">' + url + '</a>.'; | ||
return; | ||
} | ||
// text | ||
this.body = 'Redirecting to ' + url + '.'; | ||
remove: function() { | ||
return this.response.remove.apply(this.response, arguments); | ||
}, | ||
/** | ||
* Set Content-Disposition header to "attachment" with optional `filename`. | ||
* | ||
* @param {String} filename | ||
* @api public | ||
* Delegate to Request#accepts(). | ||
*/ | ||
attachment: function(filename){ | ||
if (filename) this.type = extname(filename); | ||
this.set('Content-Disposition', filename | ||
? 'attachment; filename="' + basename(filename) + '"' | ||
: 'attachment'); | ||
accepts: function() { | ||
return this.request.accepts.apply(this.request, arguments); | ||
}, | ||
/** | ||
* Set Content-Type response header with `type` through `mime.lookup()` | ||
* when it does not contain "/", or set the Content-Type to `type` otherwise. | ||
* | ||
* Examples: | ||
* | ||
* this.type = '.html'; | ||
* this.type = 'html'; | ||
* this.type = 'json'; | ||
* this.type = 'application/json'; | ||
* this.type = 'png'; | ||
* | ||
* @param {String} type | ||
* @api public | ||
* Delegate to Request#acceptsCharsets(). | ||
*/ | ||
set type(type){ | ||
if (!~type.indexOf('/')) { | ||
type = mime.lookup(type); | ||
var cs = mime.charsets.lookup(type); | ||
if (cs) type += '; charset=' + cs.toLowerCase(); | ||
} | ||
this.set('Content-Type', type); | ||
acceptsCharsets: function() { | ||
return this.request.acceptsCharsets.apply(this.request, arguments); | ||
}, | ||
/** | ||
* Return the request mime type void of | ||
* parameters such as "charset". | ||
* | ||
* @return {String} | ||
* @api public | ||
* Delegate to Request#acceptsEncodings(). | ||
*/ | ||
get type() { | ||
var type = this.get('Content-Type'); | ||
if (!type) return; | ||
return type.split(';')[0]; | ||
acceptsEncodings: function() { | ||
return this.request.acceptsEncodings.apply(this.request, arguments); | ||
}, | ||
/** | ||
* Return request header. | ||
* | ||
* The `Referrer` header field is special-cased, | ||
* both `Referrer` and `Referer` are interchangeable. | ||
* | ||
* Examples: | ||
* | ||
* this.get('Content-Type'); | ||
* // => "text/plain" | ||
* | ||
* this.get('content-type'); | ||
* // => "text/plain" | ||
* | ||
* this.get('Something'); | ||
* // => undefined | ||
* | ||
* @param {String} name | ||
* @return {String} | ||
* @api public | ||
* Delegate to Request#acceptsLanguages(). | ||
*/ | ||
get: function(name){ | ||
var req = this.req; | ||
switch (name = name.toLowerCase()) { | ||
case 'referer': | ||
case 'referrer': | ||
return req.headers.referrer || req.headers.referer; | ||
default: | ||
return req.headers[name]; | ||
} | ||
acceptsLanguages: function() { | ||
return this.request.acceptsLanguages.apply(this.request, arguments); | ||
}, | ||
/** | ||
* Set header `field` to `val`, or pass | ||
* an object of header fields. | ||
* | ||
* Examples: | ||
* | ||
* this.set('Foo', ['bar', 'baz']); | ||
* this.set('Accept', 'application/json'); | ||
* this.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' }); | ||
* | ||
* @param {String|Object|Array} field | ||
* @param {String} val | ||
* @api public | ||
* Delegate to Response#vary(). | ||
*/ | ||
set: function(field, val){ | ||
if (2 == arguments.length) { | ||
if (Array.isArray(val)) val = val.map(String); | ||
else val = String(val); | ||
this.res.setHeader(field, val); | ||
} else { | ||
for (var key in field) { | ||
this.set(key, field[key]); | ||
} | ||
} | ||
vary: function() { | ||
return this.response.vary.apply(this.response, arguments); | ||
}, | ||
setHeader: function(field, val){ | ||
this.set(field, val); | ||
}, | ||
/** | ||
* Get the current response header `name`. | ||
* | ||
* @param {String} name | ||
* @api public | ||
* Delegate to Request#is(). | ||
*/ | ||
getHeader: function(name){ | ||
return this.res.getHeader(name); | ||
is: function() { | ||
return this.request.is.apply(this.request, arguments); | ||
}, | ||
/** | ||
* Remove the current response header `name`. | ||
* | ||
* @param {String} name | ||
* @api public | ||
* Delegate to Response#append(). | ||
*/ | ||
removeHeader: function(name){ | ||
return this.res.removeHeader(name); | ||
append: function() { | ||
return this.response.append.apply(this.response, arguments); | ||
}, | ||
/** | ||
* Get the trailing headers to a request. | ||
* | ||
* @param {Object} | ||
* @api public | ||
* Delegate to Request#get(). | ||
*/ | ||
get trailers() { | ||
return this.req.trailers; | ||
get: function() { | ||
return this.request.get.apply(this.request, arguments); | ||
}, | ||
/** | ||
* Add trailing headers to the response. | ||
* | ||
* Maybe: | ||
* - throw if not chunked encoding | ||
* | ||
* @param {object} headers | ||
* @api public | ||
* Delegate to Response#set(). | ||
*/ | ||
addTrailers: function(headers){ | ||
return this.res.addTrailers(headers); | ||
set: function() { | ||
return this.response.set.apply(this.response, arguments); | ||
}, | ||
/** | ||
* Append `val` to header `field`. | ||
* | ||
* @param {String} field | ||
* @param {String} val | ||
* @api public | ||
* Delegate to Response#redirect(). | ||
*/ | ||
append: function(field, val){ | ||
field = field.toLowerCase(); | ||
var header = this.responseHeader; | ||
var list = header[field]; | ||
// not set | ||
if (!list) return this.set(field, val); | ||
// append | ||
list = list.split(/ *, */); | ||
if (!~list.indexOf(val)) list.push(val); | ||
this.set(field, list.join(', ')); | ||
redirect: function() { | ||
return this.response.redirect.apply(this.response, arguments); | ||
}, | ||
/** | ||
* Inspect implementation. | ||
* | ||
* TODO: add tests | ||
* | ||
* @return {Object} | ||
* @api public | ||
* Delegate to Response#attachment(). | ||
*/ | ||
inspect: function(){ | ||
var o = this.toJSON(); | ||
o.body = this.body; | ||
o.statusString = this.statusString; | ||
return o; | ||
attachment: function() { | ||
return this.response.attachment.apply(this.response, arguments); | ||
}, | ||
/** | ||
* Return JSON representation. | ||
* | ||
* @return {Object} | ||
* @api public | ||
*/ | ||
toJSON: function(){ | ||
return { | ||
method: this.method, | ||
status: this.status, | ||
header: this.header, | ||
responseHeader: this.responseHeader | ||
} | ||
} | ||
}; | ||
/** | ||
* Convert extnames to mime. | ||
* | ||
* @param {String} type | ||
* @return {String} | ||
* @api private | ||
*/ | ||
function extToMime(type) { | ||
if (~type.indexOf('/')) return type; | ||
return mime.lookup(type); | ||
} | ||
/** | ||
* Return status error message. | ||
* | ||
* @param {String} val | ||
* @return {String} | ||
* @api private | ||
*/ | ||
function statusError(val) { | ||
var s = 'invalid status string "' + val + '", try:\n\n'; | ||
Object.keys(statuses).forEach(function(name){ | ||
var n = statuses[name]; | ||
s += ' - ' + n + ' "' + name + '"\n'; | ||
}); | ||
return s; | ||
} | ||
/** | ||
* Escape special characters in the given string of html. | ||
* | ||
* @param {String} html | ||
* @return {String} | ||
* @api private | ||
*/ | ||
function escape(html) { | ||
return String(html) | ||
.replace(/&/g, '&') | ||
.replace(/"/g, '"') | ||
.replace(/</g, '<') | ||
.replace(/>/g, '>'); | ||
} |
{ | ||
"name": "koa", | ||
"version": "0.0.1", | ||
"version": "0.0.2", | ||
"description": "Koa web app framework", | ||
@@ -24,18 +24,25 @@ "main": "index.js", | ||
"dependencies": { | ||
"co": "2.0.0", | ||
"co": "~2.3.0", | ||
"debug": "*", | ||
"mime": "1.2.10", | ||
"fresh": "0.2.0", | ||
"negotiator": "0.2.7", | ||
"koa-compose": "1.0.0", | ||
"cookies": "~0.3.6" | ||
"mime": "~1.2.11", | ||
"fresh": "~0.2.0", | ||
"negotiator": "~0.3.0", | ||
"koa-compose": "~2.0.0", | ||
"cookies": "~0.3.7", | ||
"keygrip": "~0.2.4" | ||
}, | ||
"devDependencies": { | ||
"bytes": "*", | ||
"should": "1.2.2", | ||
"mocha": "1.12.0", | ||
"supertest": "0.7.1", | ||
"co-fs": "~1.0.1", | ||
"co-views": "0.0.1", | ||
"ejs": "~0.8.4" | ||
"bytes": "~0.2.1", | ||
"should": "~2.1.0", | ||
"mocha": "~1.14.0", | ||
"supertest": "~0.8.1", | ||
"co-fs": "~1.1", | ||
"co-views": "~0.1.0", | ||
"ejs": "~0.8.4", | ||
"koa-logger": "~1.0.1", | ||
"koa-static": "~1.2.0", | ||
"co-busboy": "git://github.com/cojs/busboy", | ||
"koa-route": "~1.0.2", | ||
"swig": "~1.1.0", | ||
"co-body": "0.0.1" | ||
}, | ||
@@ -42,0 +49,0 @@ "engines": { |
@@ -1,6 +0,7 @@ | ||
![koa middleware framework for nodejs](https://i.cloudup.com/uXIzgVnPWG-150x150.png) | ||
[![Build Status](https://travis-ci.org/koajs/koa.png)](https://travis-ci.org/koajs/koa) | ||
Expressive middleware for node.js using generators via [co](https://github.com/visionmedia/co) | ||
to make writing web applications and REST APIs more enjoyable to write. | ||
to make web applications and REST APIs more enjoyable to write. | ||
@@ -28,4 +29,5 @@ Only methods that are common to nearly all HTTP servers are integrated directly into Koa's small ~400 SLOC codebase. This | ||
- [API](docs/api.md) documentation | ||
- [Middleware](https://github.com/koajs/koa/wiki/Koa) list | ||
- [API](docs/api/index.md) documentation | ||
- [Examples](https://github.com/koajs/examples) | ||
- [Middleware](https://github.com/koajs/koa/wiki) list | ||
- [Wiki](https://github.com/koajs/koa/wiki) | ||
@@ -46,9 +48,7 @@ - [G+ Community](https://plus.google.com/communities/101845768320796750641) | ||
app.use(function(next){ | ||
return function *(){ | ||
var start = new Date; | ||
yield next; | ||
var ms = new Date - start; | ||
console.log('%s %s - %s', this.method, this.url, ms); | ||
} | ||
app.use(function *(next){ | ||
var start = new Date; | ||
yield next; | ||
var ms = new Date - start; | ||
console.log('%s %s - %s', this.method, this.url, ms); | ||
}); | ||
@@ -58,7 +58,4 @@ | ||
app.use(function(next){ | ||
return function *(){ | ||
yield next; | ||
this.body = 'Hello World'; | ||
} | ||
app.use(function *(){ | ||
this.body = 'Hello World'; | ||
}); | ||
@@ -77,36 +74,32 @@ | ||
Average latency with one middleware: | ||
If you like silly benchmarks, here's the requests per second using | ||
[wrk](https://github.com/wg/wrk) 3.x on my MBP. | ||
0.628ms | ||
Average latency with __400__ noop middleware: | ||
1.5ms | ||
If you like silly benchmarks, here's the requests per second: | ||
``` | ||
1 middleware | ||
8083.45 | ||
8367.03 | ||
5 middleware | ||
7449.95 | ||
8074.10 | ||
10 middleware | ||
7166.66 | ||
7526.55 | ||
15 middleware | ||
6992.18 | ||
7399.92 | ||
20 middleware | ||
6650.28 | ||
7055.33 | ||
30 middleware | ||
6113.57 | ||
6460.17 | ||
50 middleware | ||
5117.46 | ||
5671.98 | ||
100 middleware | ||
4349.37 | ||
``` | ||
With __50__ middleware (likely much more than you'll need), that's __307,020__ requests per minute, and __18,421,200__ per hour, __442 million__ per day, so unless you're a facebook and can't manage to spin up more | ||
With __50__ middleware (likely much more than you'll need), that's __340,260__ requests per minute, and __20,415,600__ per hour, and over __440 million__ per day, so unless you're a Facebook and can't manage to spin up more | ||
than one process to scale horizontally you'll be fine ;) | ||
@@ -117,4 +110,4 @@ | ||
- [TJ Holowaychuk](https://github.com/visionmedia) | ||
- [Jonathan Ong](https://github.com/jonathanong) | ||
- [Julian Gruber](https://github.com/juliangruber) | ||
- [Jonathan Ong](https://github.com/jonathanong) | ||
@@ -121,0 +114,0 @@ # License |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
40754
11
1546
8
13
112
5
+ Addedkeygrip@~0.2.4
+ Addedco@2.3.0(transitive)
+ Addedfresh@0.2.4(transitive)
+ Addedkeygrip@0.2.4(transitive)
+ Addedkoa-compose@2.0.1(transitive)
+ Addedmime@1.2.11(transitive)
+ Addednegotiator@0.3.0(transitive)
- Removedco@2.0.0(transitive)
- Removedfresh@0.2.0(transitive)
- Removedkoa-compose@1.0.0(transitive)
- Removedmime@1.2.10(transitive)
- Removednegotiator@0.2.7(transitive)
Updatedco@~2.3.0
Updatedcookies@~0.3.7
Updatedfresh@~0.2.0
Updatedkoa-compose@~2.0.0
Updatedmime@~1.2.11
Updatednegotiator@~0.3.0