diff --git a/lib/core/request.js b/lib/core/request.js index 7dbf781b4c4..614922f7efa 100644 --- a/lib/core/request.js +++ b/lib/core/request.js @@ -394,13 +394,21 @@ function processHeader (request, key, val) { } else if (headerName === 'transfer-encoding' || headerName === 'keep-alive' || headerName === 'upgrade') { throw new InvalidArgumentError(`invalid ${headerName} header`) } else if (headerName === 'connection') { - const value = typeof val === 'string' ? val.toLowerCase() : null - if (value !== 'close' && value !== 'keep-alive') { + // Per RFC 7230 Section 6.1, Connection header can contain + // a comma-separated list of connection option tokens (header names) + const value = typeof val === 'string' ? val : null + if (value === null) { throw new InvalidArgumentError('invalid connection header') } - if (value === 'close') { - request.reset = true + for (const token of value.toLowerCase().split(',')) { + const trimmed = token.trim() + if (!isValidHTTPToken(trimmed)) { + throw new InvalidArgumentError('invalid connection header') + } + if (trimmed === 'close') { + request.reset = true + } } } else if (headerName === 'expect') { throw new NotSupportedError('expect header not supported') diff --git a/test/invalid-headers.js b/test/invalid-headers.js index 5d49fcbf23f..c7801949343 100644 --- a/test/invalid-headers.js +++ b/test/invalid-headers.js @@ -51,7 +51,7 @@ test('invalid headers', (t) => { path: '/', method: 'GET', headers: { - connection: 'asd' + connection: 'invalid header with spaces' } }, (err, data) => { t.ok(err instanceof errors.InvalidArgumentError) diff --git a/test/request.js b/test/request.js index 6166a95e079..fd77c61e483 100644 --- a/test/request.js +++ b/test/request.js @@ -468,3 +468,100 @@ describe('Should include headers from iterable objects', scope => { }) }) }) + +describe('connection header per RFC 7230', () => { + test('should allow close', async (t) => { + t = tspl(t, { plan: 1 }) + + const server = createServer((req, res) => { + res.end('ok') + }) + + after(() => server.close()) + await new Promise((resolve) => server.listen(0, resolve)) + + const { statusCode, body } = await request({ + method: 'GET', + origin: `http://localhost:${server.address().port}`, + headers: { connection: 'close' } + }) + await body.dump() + t.strictEqual(statusCode, 200) + }) + + test('should allow keep-alive', async (t) => { + t = tspl(t, { plan: 1 }) + + const server = createServer((req, res) => { + res.end('ok') + }) + + after(() => server.close()) + await new Promise((resolve) => server.listen(0, resolve)) + + const { statusCode, body } = await request({ + method: 'GET', + origin: `http://localhost:${server.address().port}`, + headers: { connection: 'keep-alive' } + }) + await body.dump() + t.strictEqual(statusCode, 200) + }) + + test('should allow custom header name as connection option', async (t) => { + t = tspl(t, { plan: 1 }) + + const server = createServer((req, res) => { + res.end('ok') + }) + + after(() => server.close()) + await new Promise((resolve) => server.listen(0, resolve)) + + const { statusCode, body } = await request({ + method: 'GET', + origin: `http://localhost:${server.address().port}`, + headers: { + 'x-custom-header': 'value', + connection: 'x-custom-header' + } + }) + await body.dump() + t.strictEqual(statusCode, 200) + }) + + test('should allow comma-separated list of connection options', async (t) => { + t = tspl(t, { plan: 1 }) + + const server = createServer((req, res) => { + res.end('ok') + }) + + after(() => server.close()) + await new Promise((resolve) => server.listen(0, resolve)) + + const { statusCode, body } = await request({ + method: 'GET', + origin: `http://localhost:${server.address().port}`, + headers: { + 'x-custom-header': 'value', + connection: 'close, x-custom-header' + } + }) + await body.dump() + t.strictEqual(statusCode, 200) + }) + + test('should reject invalid tokens in connection header', async (t) => { + t = tspl(t, { plan: 2 }) + + await request({ + method: 'GET', + origin: 'http://localhost:1234', + headers: { connection: 'invalid header with spaces' } + }).catch((err) => { + t.ok(err instanceof errors.InvalidArgumentError) + t.strictEqual(err.message, 'invalid connection header') + }) + }) +})