diff --git a/docs/api.md b/docs/api.md index 7fd47a5..0a89223 100644 --- a/docs/api.md +++ b/docs/api.md @@ -192,9 +192,9 @@ app.context({ if you have a typo an error will be thrown, displaying this list so you can make a correction. -### ctx.hasContent +### ctx.length= - When the response status is __204__ or __304__ this returns __false__. + Set response Content-Length to the given value. ### ctx.length @@ -219,12 +219,27 @@ app.context({ - `Buffer` written - `Stream` piped - `Object` json-stringified + - `null` no content response - When a Koa application is created it injects - a middleware named `respond`, which handles - each of these `ctx.body` values. The `Content-Length` - header field is set when possible, and objects are - passed through `JSON.stringify()`. +#### String + + The Content-Type is defaulted to text/html or text/plain, both with + a default charset of utf-8. The Content-Length field is also set. + +#### Buffer + + The Content-Type is defaulted to application/octet-stream, and Content-Length + is also set. + +#### Stream + + The Content-Type is defaulted to application/octet-stream. + +#### Object + + The Content-Type is defaulted to application/json. + +#### Notes To alter the JSON response formatting use the `app.jsonSpaces` setting, for example to compress JSON responses set: diff --git a/lib/application.js b/lib/application.js index a232ec4..dcabf72 100644 --- a/lib/application.js +++ b/lib/application.js @@ -165,7 +165,7 @@ function respond(next){ var res = this.res; var body = this.body; var head = 'HEAD' == this.method; - var ignore = 204 == this.status || 304 == this.status; + var noContent = 204 == this.status || 304 == this.status; // 404 if (null == body && 200 == this.status) { @@ -173,28 +173,22 @@ function respond(next){ } // ignore body - if (ignore) return res.end(); + if (noContent) return res.end(); // status body if (null == body) { - this.set('Content-Type', 'text/plain'); + this.type = 'text'; body = http.STATUS_CODES[this.status]; } // Buffer body if (Buffer.isBuffer(body)) { - var ct = this.responseHeader['content-type']; - if (!ct) this.set('Content-Type', 'application/octet-stream'); - this.set('Content-Length', body.length); if (head) return res.end(); return res.end(body); } // string body if ('string' == typeof body) { - var ct = this.responseHeader['content-type']; - if (!ct) this.set('Content-Type', 'text/plain; charset=utf-8'); - this.set('Content-Length', Buffer.byteLength(body)); if (head) return res.end(); return res.end(body); } @@ -208,8 +202,7 @@ function respond(next){ // body: json body = JSON.stringify(body, null, this.app.jsonSpaces); - this.set('Content-Length', Buffer.byteLength(body)); - this.set('Content-Type', 'application/json'); + this.length = Buffer.byteLength(body); if (head) return res.end(); res.end(body); } diff --git a/lib/context.js b/lib/context.js index fcf882a..592aadd 100644 --- a/lib/context.js +++ b/lib/context.js @@ -7,6 +7,7 @@ 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'); @@ -46,53 +47,6 @@ module.exports = { return this.res._headers || {}; }, - /** - * Get response status code. - * - * @return {Number} - * @api public - */ - - get status() { - return this.res.statusCode; - }, - - /** - * Set response status code. - * - * @param {Number|String} val - * @api public - */ - - 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; - - if (!this.hasContent) { - this.res.removeHeader('Content-Type'); - this.res.removeHeader('Content-Length'); - this.res.removeHeader('Transfer-Encoding'); - } - }, - - /** - * Check if the response has content, - * aka is not a 204 or 304 response. - * - * @return {Boolean} - * @api public - */ - - get hasContent() { - var s = this.status; - return 204 != s && 304 != s; - }, - /** * Return response status string. * @@ -147,6 +101,94 @@ module.exports = { this.req.method = val; }, + /** + * Get response status code. + * + * @return {Number} + * @api public + */ + + get status() { + return this.res.statusCode; + }, + + /** + * Set response status code. + * + * @param {Number|String} val + * @api public + */ + + 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; + }, + + /** + * Get response body. + * + * @return {Mixed} + * @api public + */ + + get body() { + return this._body; + }, + + /** + * Set response body. + * + * @param {String|Buffer|Object|Stream} val + * @api public + */ + + set body(val) { + this._body = val; + + if (this.type) return; + + // 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; + } + + // string + if ('string' == typeof val) { + this.type = ~val.indexOf('<') ? 'html' : 'text'; + this.length = Buffer.byteLength(val); + return; + } + + // buffer + if (Buffer.isBuffer(val)) { + this.type = 'bin'; + this.length = val.length; + return; + } + + // stream + if (val instanceof Stream) { + this.type = 'bin'; + return; + } + + // json + this.type = 'json'; + }, + /** * Get request pathname. * @@ -314,6 +356,17 @@ module.exports = { return ~~len; }, + /** + * Set Content-Length field to `n`. + * + * @param {Number} n + * @api public + */ + + set length(n) { + this.set('Content-Length', n); + }, + /** * Return parsed response Content-Length when present. * diff --git a/test/application.js b/test/application.js index 57ff6ca..e4ebbf9 100644 --- a/test/application.js +++ b/test/application.js @@ -90,7 +90,6 @@ describe('app.respond', function(){ app.use(function(next){ return function *(){ this.status = 400; - this.body = null; } }); diff --git a/test/context.js b/test/context.js index ea364c5..aff3021 100644 --- a/test/context.js +++ b/test/context.js @@ -21,6 +21,60 @@ function context(req, res) { return ctx; } +describe('ctx.body=', function(){ + describe('when a string is given', function(){ + it('should default to text', function(){ + var ctx = context(); + ctx.body = 'Tobi'; + assert('text/plain; charset=utf-8' == ctx.responseHeader['content-type']); + }) + + it('should set length', function(){ + var ctx = context(); + ctx.body = 'Tobi'; + assert('4' == ctx.responseHeader['content-length']); + }) + }) + + describe('when an html string is given', function(){ + it('should default to html', function(){ + var ctx = context(); + ctx.body = '

Tobi

'; + assert('text/html; charset=utf-8' == ctx.responseHeader['content-type']); + }) + }) + + describe('when a stream is given', function(){ + it('should default to an octet stream', function(){ + var ctx = context(); + ctx.body = fs.createReadStream('LICENSE'); + assert('application/octet-stream' == ctx.responseHeader['content-type']); + }) + }) + + describe('when a buffer is given', function(){ + it('should default to an octet stream', function(){ + var ctx = context(); + ctx.body = new Buffer('hey'); + assert('application/octet-stream' == ctx.responseHeader['content-type']); + }) + + it('should set length', function(){ + var ctx = context(); + ctx.body = new Buffer('Tobi'); + assert('4' == ctx.responseHeader['content-length']); + }) + }) + + describe('when an object is given', function(){ + it('should default to json', function(){ + var ctx = context(); + ctx.body = { foo: 'bar' }; + assert('application/json' == ctx.responseHeader['content-type']); + }) + }) +}) + describe('ctx.error(msg)', function(){ it('should set .status to 500', function(done){ var ctx = context(); @@ -91,9 +145,6 @@ describe('ctx.responseLength', function(){ ctx.body = new Buffer('foo'); ctx.responseLength.should.equal(3); - - ctx.body = {}; - assert(null == ctx.responseLength); }) }) @@ -227,6 +278,7 @@ describe('ctx.status=', function(){ app.use(function(next){ return function *(){ + this.body = { foo: 'bar' }; this.set('Content-Type', 'application/json'); this.set('Content-Length', '15'); this.set('Transfer-Encoding', 'chunked');