Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion __tests__/context/onerror.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,19 +56,25 @@ describe('ctx.onerror(err)', () => {
status: 418,
expose: true,
headers: {
'Content-Length': ['1', '2'],
'cOnTeNt-TyPe': ['text/plain', 'application/json'],
'Set-Cookie': ['first=1', 'second=2'],
'X-New-Header': 'Value'
}
})
})

const res = await request(app.callback())
.get('/')
.expect(418)
.timeout({ deadline: 1000 })
.expect(418, 'boom')
.expect('Content-Type', 'text/plain; charset=utf-8')
.expect('Content-Length', '4')
.expect('X-New-Header', 'Value')

assert.strictEqual(Object.prototype.hasOwnProperty.call(res.headers, 'vary'), false)
assert.strictEqual(Object.prototype.hasOwnProperty.call(res.headers, 'x-csrf-token'), false)
assert.deepStrictEqual(res.headers['set-cookie'], ['first=1', 'second=2'])
})

it('should ignore error after headerSent', async () => {
Expand Down
69 changes: 69 additions & 0 deletions __tests__/response/set.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,73 @@ describe('ctx.set(object)', () => {
assert.strictEqual(ctx.response.header.foo, '1')
assert.strictEqual(ctx.response.header.bar, '2')
})

it('should not assign multiple values to singleton headers', () => {
const ctx = context()

assert.throws(() => {
ctx.set('content-length', ['1', '2'])
}, /content-length must contain exactly one value/)

assert.throws(() => {
ctx.set({ 'Content-Length': ['1', '2'] })
}, /Content-Length must contain exactly one value/)

assert.throws(() => {
ctx.set({ 'cOnTeNt-TyPe': ['text/plain', 'application/json'] })
}, /cOnTeNt-TyPe must contain exactly one value/)
})

it('should normalize singleton header arrays', () => {
const ctx = context()

ctx.set('Content-Length', ['1'])
assert.strictEqual(ctx.response.get('Content-Length'), '1')

ctx.set({ 'Content-Type': ['application/json'] })
ctx.body = { ok: true }
assert.strictEqual(ctx.response.get('Content-Type'), 'application/json')

ctx.set('Content-Type', ['application/json'])
ctx.body = { ok: true }
assert.strictEqual(ctx.response.get('Content-Type'), 'application/json')
})

it('should reject empty arrays for singleton headers', () => {
const ctx = context()

assert.throws(() => ctx.set('Content-Type', []), /must contain exactly one value/)
assert.throws(() => ctx.set('Content-Length', []), /must contain exactly one value/)
assert.throws(() => ctx.set({ 'Content-Type': [] }), /must contain exactly one value/)
assert.throws(() => ctx.set({ 'Content-Length': [] }), /must contain exactly one value/)
})

it('should validate every field before assigning any values', () => {
const ctx = context()

assert.throws(() => {
ctx.set({
'X-Foo': 'bar',
'Content-Length': ['1', '2']
})
}, /Content-Length must contain exactly one value/)

assert.strictEqual(ctx.response.header['x-foo'], undefined)
})

it('should read each field value once', () => {
const ctx = context()
let reads = 0
const fields = {
get 'Content-Length' () {
reads++
return reads === 1 ? '1' : ['1', '2']
}
}

ctx.set(fields)

assert.strictEqual(reads, 1)
assert.strictEqual(ctx.response.header['content-length'], '1')
})
})
8 changes: 7 additions & 1 deletion lib/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,13 @@ const proto = module.exports = {
}

// then set those specified
this.set(err.headers)
const headers = err.headers || {}
Object.keys(headers).forEach(name => {
const lowerName = name.toLowerCase()
const generatedHeader = lowerName === 'content-type' || lowerName === 'content-length'
if (generatedHeader) return
this.set(name, headers[name])
})

// force text/plain
this.type = 'text'
Expand Down
27 changes: 19 additions & 8 deletions lib/response.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,19 @@ const getType = require('mime-types').contentType
const isStream = require('./is-stream.js')
const only = require('./only.js')

function isSingleValueHeader (field) {
const name = field.toLowerCase()
return name === 'content-type' || name === 'content-length'
}

function normalizeHeader (field, value) {
const singletonArray = isSingleValueHeader(field) && Array.isArray(value)
if (!singletonArray) return [field, value]

assert(value.length === 1, `${field} must contain exactly one value`)
return [field, value[0]]
}

/**
* Prototype.
*/
Expand Down Expand Up @@ -537,14 +550,12 @@ module.exports = {
set (field, val) {
if (this.headerSent || !field) return

if (typeof field === 'string') {
if (field.toLowerCase() === 'content-type') {
assert(!Array.isArray(val), 'Assign multiple Content-Type for response header is not allowed')
}
this.res.setHeader(field, val)
} else {
Object.keys(field).forEach(header => this.res.setHeader(header, field[header]))
}
const entries = typeof field === 'string' ? [[field, val]] : Object.entries(field)
const headers = entries.map(([name, value]) => normalizeHeader(name, value))

headers.forEach(([name, value]) => {
this.res.setHeader(name, value)
})
},

/**
Expand Down
Loading