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:
parent
c5ecbd99ac
commit
11913f5e4e
5 changed files with 181 additions and 69 deletions
29
docs/api.md
29
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:
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
147
lib/context.js
147
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.
|
||||
*
|
||||
|
|
|
@ -90,7 +90,6 @@ describe('app.respond', function(){
|
|||
app.use(function(next){
|
||||
return function *(){
|
||||
this.status = 400;
|
||||
this.body = null;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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 = '<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(){
|
||||
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');
|
||||
|
|
Loading…
Reference in a new issue