From 010924bf21494d826cdea0fd1fb333930f30a870 Mon Sep 17 00:00:00 2001 From: Maksym Shenderuk Date: Fri, 9 Feb 2024 20:40:56 +0300 Subject: [PATCH] Add support for passing iterable objects as headers (#2708) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update tests with iterable object cases * Add support for iterable object headers * Update tests with cases of malformed headers * Add check for malformed headers * Update lib/core/request.js Co-authored-by: Mert Can Altın * Update lib/core/request.js Co-authored-by: Mert Can Altın * Fix code after unverified improvement broke functionality --------- Co-authored-by: Mert Can Altın --- lib/core/request.js | 17 ++++-- test/request.js | 135 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 147 insertions(+), 5 deletions(-) diff --git a/lib/core/request.js b/lib/core/request.js index 74e0ca16eaa..bee7a47af92 100644 --- a/lib/core/request.js +++ b/lib/core/request.js @@ -164,10 +164,19 @@ class Request { processHeader(this, headers[i], headers[i + 1]) } } else if (headers && typeof headers === 'object') { - const keys = Object.keys(headers) - for (let i = 0; i < keys.length; i++) { - const key = keys[i] - processHeader(this, key, headers[key]) + if (headers[Symbol.iterator]) { + for (const header of headers) { + if (!Array.isArray(header) || header.length !== 2) { + throw new InvalidArgumentError('headers must be in key-value pair format') + } + const [key, value] = header + processHeader(this, key, value) + } + } else { + const keys = Object.keys(headers) + for (const key of keys) { + processHeader(this, key, headers[key]) + } } } else if (headers != null) { throw new InvalidArgumentError('headers must be an object or an array') diff --git a/test/request.js b/test/request.js index 30bf745c3f1..bbfab5b3f5c 100644 --- a/test/request.js +++ b/test/request.js @@ -2,7 +2,7 @@ const { createServer } = require('node:http') const { test } = require('tap') -const { request } = require('..') +const { request, errors } = require('..') test('no-slash/one-slash pathname should be included in req.path', async (t) => { const pathServer = createServer((req, res) => { @@ -246,3 +246,136 @@ test('DispatchOptions#reset', scope => { }) }) }) + +test('Should include headers from iterable objects', scope => { + scope.plan(4) + + scope.test('Should include headers built with Headers global object', async t => { + const server = createServer((req, res) => { + t.equal('GET', req.method) + t.equal(`localhost:${server.address().port}`, req.headers.host) + t.equal(req.headers.hello, 'world') + res.statusCode = 200 + res.end('hello') + }) + + const headers = new Headers() + headers.set('hello', 'world') + + t.plan(3) + + t.teardown(server.close.bind(server)) + + await new Promise((resolve, reject) => { + server.listen(0, (err) => { + if (err != null) reject(err) + else resolve() + }) + }) + + await request({ + method: 'GET', + origin: `http://localhost:${server.address().port}`, + reset: true, + headers + }) + }) + + scope.test('Should include headers built with Map', async t => { + const server = createServer((req, res) => { + t.equal('GET', req.method) + t.equal(`localhost:${server.address().port}`, req.headers.host) + t.equal(req.headers.hello, 'world') + res.statusCode = 200 + res.end('hello') + }) + + const headers = new Map() + headers.set('hello', 'world') + + t.plan(3) + + t.teardown(server.close.bind(server)) + + await new Promise((resolve, reject) => { + server.listen(0, (err) => { + if (err != null) reject(err) + else resolve() + }) + }) + + await request({ + method: 'GET', + origin: `http://localhost:${server.address().port}`, + reset: true, + headers + }) + }) + + scope.test('Should include headers built with custom iterable object', async t => { + const server = createServer((req, res) => { + t.equal('GET', req.method) + t.equal(`localhost:${server.address().port}`, req.headers.host) + t.equal(req.headers.hello, 'world') + res.statusCode = 200 + res.end('hello') + }) + + const headers = { + * [Symbol.iterator] () { + yield ['hello', 'world'] + } + } + + t.plan(3) + + t.teardown(server.close.bind(server)) + + await new Promise((resolve, reject) => { + server.listen(0, (err) => { + if (err != null) reject(err) + else resolve() + }) + }) + + await request({ + method: 'GET', + origin: `http://localhost:${server.address().port}`, + reset: true, + headers + }) + }) + + scope.test('Should throw error if headers iterable object does not yield key-value pairs', async t => { + const server = createServer((req, res) => { + res.end('hello') + }) + + const headers = { + * [Symbol.iterator] () { + yield 'Bad formatted header' + } + } + + t.plan(2) + + t.teardown(server.close.bind(server)) + + await new Promise((resolve, reject) => { + server.listen(0, (err) => { + if (err != null) reject(err) + else resolve() + }) + }) + + await request({ + method: 'GET', + origin: `http://localhost:${server.address().port}`, + reset: true, + headers + }).catch((err) => { + t.type(err, errors.InvalidArgumentError) + t.equal(err.message, 'headers must be in key-value pair format') + }) + }) +})