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.
This commit is contained in:
TJ Holowaychuk 2013-09-14 15:54:17 -07:00
parent c5ecbd99ac
commit 11913f5e4e
5 changed files with 181 additions and 69 deletions

View file

@ -192,9 +192,9 @@ app.context({
if you have a typo an error will be thrown, displaying this list if you have a typo an error will be thrown, displaying this list
so you can make a correction. 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 ### ctx.length
@ -219,12 +219,27 @@ app.context({
- `Buffer` written - `Buffer` written
- `Stream` piped - `Stream` piped
- `Object` json-stringified - `Object` json-stringified
- `null` no content response
When a Koa application is created it injects #### String
a middleware named `respond`, which handles
each of these `ctx.body` values. The `Content-Length` The Content-Type is defaulted to text/html or text/plain, both with
header field is set when possible, and objects are a default charset of utf-8. The Content-Length field is also set.
passed through `JSON.stringify()`.
#### 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` To alter the JSON response formatting use the `app.jsonSpaces`
setting, for example to compress JSON responses set: setting, for example to compress JSON responses set:

View file

@ -165,7 +165,7 @@ function respond(next){
var res = this.res; var res = this.res;
var body = this.body; var body = this.body;
var head = 'HEAD' == this.method; var head = 'HEAD' == this.method;
var ignore = 204 == this.status || 304 == this.status; var noContent = 204 == this.status || 304 == this.status;
// 404 // 404
if (null == body && 200 == this.status) { if (null == body && 200 == this.status) {
@ -173,28 +173,22 @@ function respond(next){
} }
// ignore body // ignore body
if (ignore) return res.end(); if (noContent) return res.end();
// status body // status body
if (null == body) { if (null == body) {
this.set('Content-Type', 'text/plain'); this.type = 'text';
body = http.STATUS_CODES[this.status]; body = http.STATUS_CODES[this.status];
} }
// Buffer body // Buffer body
if (Buffer.isBuffer(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(); if (head) return res.end();
return res.end(body); return res.end(body);
} }
// string body // string body
if ('string' == typeof 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(); if (head) return res.end();
return res.end(body); return res.end(body);
} }
@ -208,8 +202,7 @@ function respond(next){
// body: json // body: json
body = JSON.stringify(body, null, this.app.jsonSpaces); body = JSON.stringify(body, null, this.app.jsonSpaces);
this.set('Content-Length', Buffer.byteLength(body)); this.length = Buffer.byteLength(body);
this.set('Content-Type', 'application/json');
if (head) return res.end(); if (head) return res.end();
res.end(body); res.end(body);
} }

View file

@ -7,6 +7,7 @@ var debug = require('debug')('koa:context');
var Negotiator = require('negotiator'); var Negotiator = require('negotiator');
var statuses = require('./status'); var statuses = require('./status');
var qs = require('querystring'); var qs = require('querystring');
var Stream = require('stream');
var fresh = require('fresh'); var fresh = require('fresh');
var http = require('http'); var http = require('http');
var path = require('path'); var path = require('path');
@ -46,53 +47,6 @@ module.exports = {
return this.res._headers || {}; 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. * Return response status string.
* *
@ -147,6 +101,94 @@ module.exports = {
this.req.method = val; 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. * Get request pathname.
* *
@ -314,6 +356,17 @@ module.exports = {
return ~~len; 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. * Return parsed response Content-Length when present.
* *

View file

@ -90,7 +90,6 @@ describe('app.respond', function(){
app.use(function(next){ app.use(function(next){
return function *(){ return function *(){
this.status = 400; this.status = 400;
this.body = null;
} }
}); });

View file

@ -21,6 +21,60 @@ function context(req, res) {
return ctx; 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 = '<h1>Tobi</h1>';
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(){ describe('ctx.error(msg)', function(){
it('should set .status to 500', function(done){ it('should set .status to 500', function(done){
var ctx = context(); var ctx = context();
@ -91,9 +145,6 @@ describe('ctx.responseLength', function(){
ctx.body = new Buffer('foo'); ctx.body = new Buffer('foo');
ctx.responseLength.should.equal(3); ctx.responseLength.should.equal(3);
ctx.body = {};
assert(null == ctx.responseLength);
}) })
}) })
@ -227,6 +278,7 @@ describe('ctx.status=', function(){
app.use(function(next){ app.use(function(next){
return function *(){ return function *(){
this.body = { foo: 'bar' };
this.set('Content-Type', 'application/json'); this.set('Content-Type', 'application/json');
this.set('Content-Length', '15'); this.set('Content-Length', '15');
this.set('Transfer-Encoding', 'chunked'); this.set('Transfer-Encoding', 'chunked');