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
|
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:
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
147
lib/context.js
147
lib/context.js
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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');
|
||||||
|
|
Loading…
Reference in a new issue