feat: ignore set header/status when header sent (#1137)

This commit is contained in:
Yiyu He 2018-02-11 16:25:24 +08:00 committed by GitHub
parent 0923ef6182
commit 3c23aa5b74
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 86 additions and 22 deletions

View file

@ -80,9 +80,10 @@ module.exports = {
*/ */
set status(code) { set status(code) {
if (this.headerSent) return;
assert('number' == typeof code, 'status code must be a number'); assert('number' == typeof code, 'status code must be a number');
assert(statuses[code], `invalid status code: ${code}`); assert(statuses[code], `invalid status code: ${code}`);
assert(!this.res.headersSent, 'headers have already been sent');
this._explicitStatus = true; this._explicitStatus = true;
this.res.statusCode = code; this.res.statusCode = code;
if (this.req.httpVersionMajor < 2) this.res.statusMessage = statuses[code]; if (this.req.httpVersionMajor < 2) this.res.statusMessage = statuses[code];
@ -133,8 +134,6 @@ module.exports = {
const original = this._body; const original = this._body;
this._body = val; this._body = val;
if (this.res.headersSent) return;
// no content // no content
if (null == val) { if (null == val) {
if (!statuses.empty[this.status]) this.status = 204; if (!statuses.empty[this.status]) this.status = 204;
@ -233,6 +232,8 @@ module.exports = {
*/ */
vary(field) { vary(field) {
if (this.headerSent) return;
vary(this.res, field); vary(this.res, field);
}, },
@ -434,6 +435,8 @@ module.exports = {
*/ */
set(field, val) { set(field, val) {
if (this.headerSent) return;
if (2 == arguments.length) { if (2 == arguments.length) {
if (Array.isArray(val)) val = val.map(String); if (Array.isArray(val)) val = val.map(String);
else val = String(val); else val = String(val);
@ -481,6 +484,8 @@ module.exports = {
*/ */
remove(field) { remove(field) {
if (this.headerSent) return;
this.res.removeHeader(field); this.res.removeHeader(field);
}, },

View file

@ -39,6 +39,51 @@ describe('app.respond', () => {
.expect(200) .expect(200)
.expect('lol'); .expect('lol');
}); });
it('should ignore set header after header sent', () => {
const app = new Koa();
app.use(ctx => {
ctx.body = 'Hello';
ctx.respond = false;
const res = ctx.res;
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('lol');
ctx.set('foo', 'bar');
});
const server = app.listen();
return request(server)
.get('/')
.expect(200)
.expect('lol')
.expect(res => {
assert(!res.headers.foo);
});
});
it('should ignore set status after header sent', () => {
const app = new Koa();
app.use(ctx => {
ctx.body = 'Hello';
ctx.respond = false;
const res = ctx.res;
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('lol');
ctx.status = 201;
});
const server = app.listen();
return request(server)
.get('/')
.expect(200)
.expect('lol');
});
}); });
describe('when this.type === null', () => { describe('when this.type === null', () => {

View file

@ -61,39 +61,27 @@ describe('ctx.flushHeaders()', () => {
.expect('Body'); .expect('Body');
}); });
it('should fail to set the headers after flushHeaders', async () => { it('should ignore set header after flushHeaders', async () => {
const app = new Koa(); const app = new Koa();
app.use((ctx, next) => { app.use((ctx, next) => {
ctx.status = 401; ctx.status = 401;
ctx.res.setHeader('Content-Type', 'text/plain'); ctx.res.setHeader('Content-Type', 'text/plain');
ctx.flushHeaders(); ctx.flushHeaders();
ctx.body = ''; ctx.body = 'foo';
try {
ctx.set('X-Shouldnt-Work', 'Value'); ctx.set('X-Shouldnt-Work', 'Value');
} catch (err) { ctx.remove('Content-Type');
ctx.body += 'ctx.set fail '; ctx.vary('Content-Type');
}
try {
ctx.status = 200;
} catch (err) {
ctx.body += 'ctx.status fail ';
}
try {
ctx.length = 10;
} catch (err) {
ctx.body += 'ctx.length fail';
}
}); });
const server = app.listen(); const server = app.listen();
const res = await request(server) const res = await request(server)
.get('/') .get('/')
.expect(401) .expect(401)
.expect('Content-Type', 'text/plain') .expect('Content-Type', 'text/plain');
.expect('ctx.set fail ctx.status fail ctx.length fail');
assert.equal(res.headers['x-shouldnt-work'], undefined, 'header set after flushHeaders'); assert.equal(res.headers['x-shouldnt-work'], undefined, 'header set after flushHeaders');
assert.equal(res.headers.vary, undefined, 'header set after flushHeaders');
}); });
it('should flush headers first and delay to send data', done => { it('should flush headers first and delay to send data', done => {
@ -134,4 +122,30 @@ describe('ctx.flushHeaders()', () => {
.end(); .end();
}); });
}); });
it('should catch stream error', done => {
const PassThrough = require('stream').PassThrough;
const app = new Koa();
app.once('error', err => {
assert(err.message === 'mock error');
done();
});
app.use(ctx => {
ctx.type = 'json';
ctx.status = 200;
ctx.headers['Link'] = '</css/mycss.css>; as=style; rel=preload, <https://img.craftflair.com>; rel=preconnect; crossorigin';
ctx.length = 20;
ctx.flushHeaders();
const stream = ctx.body = new PassThrough();
setTimeout(() => {
stream.emit('error', new Error('mock error'));
}, 100);
});
const server = app.listen();
request(server).get('/').end();
});
}); });