From 11913f5e4e79b6a9416f69dc07d5975785cb0b97 Mon Sep 17 00:00:00 2001 From: TJ Holowaychuk Date: Sat, 14 Sep 2013 15:54:17 -0700 Subject: [PATCH] add ctx.body= setter this prevents a bunch of redundant checks that middleware may need to check response length, type etc. the less code floating around based on our supported response body types the better, giving us more freedom to change these as needed, and just less error-prone code in general. --- docs/api.md | 29 ++++++--- lib/application.js | 15 ++--- lib/context.js | 147 ++++++++++++++++++++++++++++++-------------- test/application.js | 1 - test/context.js | 58 ++++++++++++++++- 5 files changed, 181 insertions(+), 69 deletions(-) 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');