From 2b56a548083d9bc44f89962459ee6325c38742d5 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Mon, 22 Mar 2021 11:27:06 +0100 Subject: [PATCH 01/35] feat: redirect --- index.js | 1 + lib/redirect.js | 146 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+) create mode 100644 lib/redirect.js diff --git a/index.js b/index.js index 9d5864597c6..a7a41783e33 100644 --- a/index.js +++ b/index.js @@ -18,6 +18,7 @@ function undici (url, opts) { undici.Pool = Pool undici.Client = Client undici.errors = errors +undici.redirect = require('./lib/redirect') undici.Agent = Agent undici.request = request diff --git a/lib/redirect.js b/lib/redirect.js new file mode 100644 index 00000000000..e70f5205e9c --- /dev/null +++ b/lib/redirect.js @@ -0,0 +1,146 @@ +const util = require('./core/util') +const { InvalidArgumentError } = require('./core/errors') + +function dispatch (origin, agent, { maxRedirections = 10, ...opts }, handler) { + if (maxRedirections != null && (maxRedirections < 0 || !Number.isInteger(maxRedirections))) { + throw new InvalidArgumentError('maxRedirections must be a positive number') + } + + const client = agent.get(origin) + + if (!maxRedirections) { + return client.dispatch(opts, handler) + } + + // TODO (fix): Restartable streams? + if (util.isStream(opts.body) && util.bodyLength(opts.body) !== 0) { + return client.dispatch(opts, handler) + } + + client.dispatch(opts, { + agent, + opts: { ...opts, maxRedirections: maxRedirections - 1 }, + origin, + maxRedirections, + handler, + location: null, + + onConnect (abort) { + this.onConnect(abort) + }, + + onUpgrade (statusCode, headers, socket) { + this.onUpgrade(statusCode, headers, socket) + }, + + onError (error) { + this.onError(error) + }, + + onHeaders (statusCode, headers, resume) { + // Check if statusCode is 3xx, if there is a location header and if the redirection can be followed + this.location = this.maxRedirections + ? redirectLocation(statusCode, headers) + : null + + if (!this.location) { + return this.handler.onHeaders(statusCode, headers, resume) + } + + this.location = new URL(this.location, this.origin) + + // Remove headers referring to the original URL. + // By default it is Host only, unless it's a 303 (see below), which removes also all Content-* headers. + // https://tools.ietf.org/html/rfc7231#section-6.4 + this.opts.headers = cleanRequestHeaders(this.opts.headers, statusCode === 303) + + // https://tools.ietf.org/html/rfc7231#section-6.4.4 + // In case of HTTP 303, always replace method to be either HEAD or GET + if (statusCode === 303) { + this.opts.method = 'GET' + // TOOD (fix): What if body? + } + }, + + onData (chunk) { + if (!this.location) { + return this.handler.onData(chunk) + } + }, + + onComplete (trailers) { + if (this.location) { + dispatch(this.location, this.opts, this.handler, this.agent) + } else { + this.handler.onComplete(trailers) + } + } + }) +} + +function redirectLocation (statusCode, headers) { + // TODO (fix): Different redirect codes are handled differently? + if ([300, 301, 302, 303, 307, 308].indexOf(statusCode) === -1) { + return null + } + + for (let i = 0; i < headers.length; i += 2) { + // Find the matching headers, then return its value + if (headers[i].length && headers[i].toLowerCase() === 'location') { + return headers[i + 1] + } + } + + return null +} + +// https://tools.ietf.org/html/rfc7231#section-6.4.4 +function shouldRemoveHeader (header, removeContent) { + return ( + (header.length === 4 && header.toLowerCase() === 'host') || + (removeContent && header.toLowerCase().indexOf('content-') === 0) + ) +} + +// https://tools.ietf.org/html/rfc7231#section-6.4 +function cleanRequestHeaders (headers, removeContent) { + const ret = [] + if (Array.isArray(headers)) { + for (let i = 0; i < headers.length; i += 2) { + if (!shouldRemoveHeader(headers[i], removeContent)) { + ret.push(headers[i], headers[i + 1]) + } + } + } else if (headers && typeof headers === 'object') { + for (const [key, val] of Object.entries(headers)) { + if (!shouldRemoveHeader(key, removeContent)) { + ret.push(key, val) + } + } + } else if (headers != null) { + throw new InvalidArgumentError('headers must be an object or an array') + } + return ret +} + +function dispatchWithRedirectAgent (fn) { + return (url, { agent, method = 'GET', ...opts } = {}, ...additionalArgs) => { + if (opts.path != null) { + throw new InvalidArgumentError('unsupported opts.path') + } + + const { origin, pathname, search } = util.parseURL(url) + + const path = `${pathname || '/'}${search || ''}` + + return fn.call({ dispatch: dispatch.bind(null, origin, agent) }, { ...opts, method, path }, ...additionalArgs) + } +} + +module.exports = { + request: dispatchWithRedirectAgent(require('./client-request')), + stream: dispatchWithRedirectAgent(require('./client-stream')), + pipeline: dispatchWithRedirectAgent(require('./client-pipeline')), + connect: dispatchWithRedirectAgent(require('./client-connect')), + upgrade: dispatchWithRedirectAgent(require('./client-upgrade')) +} From 7607b90402610cdf881327d78e7aeb042038ced2 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Wed, 27 Apr 2022 10:57:39 +0200 Subject: [PATCH 02/35] fix linting --- lib/core/client.js | 4 +++- test/client-pipelining.js | 2 +- test/client-stream.js | 4 ++-- test/client.js | 4 ++-- test/promises.js | 2 +- test/socket-handle-error.js | 2 ++ 6 files changed, 11 insertions(+), 7 deletions(-) diff --git a/lib/core/client.js b/lib/core/client.js index 1aa0ebb91bd..820213428ed 100644 --- a/lib/core/client.js +++ b/lib/core/client.js @@ -193,7 +193,9 @@ class Client extends EventEmitter { this[kSocket].authorizationError ) && !this[kSocket].destroyed - ) ? 1 : 0 + ) + ? 1 + : 0 } get pending () { diff --git a/test/client-pipelining.js b/test/client-pipelining.js index 09441b2ac2e..cb027c9f204 100644 --- a/test/client-pipelining.js +++ b/test/client-pipelining.js @@ -34,7 +34,7 @@ test('20 times GET with pipelining 10', (t) => { }) t.tearDown(client.close.bind(client)) - for (var i = 0; i < num; i++) { + for (let i = 0; i < num; i++) { makeRequest(i) } diff --git a/test/client-stream.js b/test/client-stream.js index 51c60194f35..67e30f8462f 100644 --- a/test/client-stream.js +++ b/test/client-stream.js @@ -684,7 +684,7 @@ test('stream needDrain', (t) => { } while (dst.write(Buffer.alloc(4096))) { - + // Do nothing... } const orgWrite = dst.write @@ -737,7 +737,7 @@ test('stream legacy needDrain', (t) => { } while (dst.write(Buffer.alloc(4096))) { - + // Do nothing... } const orgWrite = dst.write diff --git a/test/client.js b/test/client.js index d4c738a219c..bce05ff0142 100644 --- a/test/client.js +++ b/test/client.js @@ -501,7 +501,7 @@ test('10 times GET', (t) => { const client = new Client(`http://localhost:${server.address().port}`) t.tearDown(client.close.bind(client)) - for (var i = 0; i < num; i++) { + for (let i = 0; i < num; i++) { makeRequest(i) } @@ -534,7 +534,7 @@ test('10 times HEAD', (t) => { const client = new Client(`http://localhost:${server.address().port}`) t.tearDown(client.close.bind(client)) - for (var i = 0; i < num; i++) { + for (let i = 0; i < num; i++) { makeRequest(i) } diff --git a/test/promises.js b/test/promises.js index 545005817ce..ad3cdc2ada6 100644 --- a/test/promises.js +++ b/test/promises.js @@ -175,7 +175,7 @@ test('20 times GET with pipelining 10, async await support', (t) => { }) t.tearDown(client.close.bind(client)) - for (var i = 0; i < num; i++) { + for (let i = 0; i < num; i++) { makeRequest(i) } diff --git a/test/socket-handle-error.js b/test/socket-handle-error.js index 951a03ea97f..fe2497c4288 100644 --- a/test/socket-handle-error.js +++ b/test/socket-handle-error.js @@ -10,6 +10,7 @@ test('stop error', (t) => { const server = createServer((req, res) => { while (res.write(Buffer.alloc(4096))) { + // Do nothing... } }) t.tearDown(server.close.bind(server)) @@ -41,6 +42,7 @@ test('resume error', (t) => { const server = createServer((req, res) => { while (res.write(Buffer.alloc(4096))) { + // Do nothing... } }) t.tearDown(server.close.bind(server)) From a4d185e0ec6a3b5434fcc0c3913be541590a8c9b Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Wed, 27 Apr 2022 13:58:43 +0200 Subject: [PATCH 03/35] fix: linting --- test/headers-as-array.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/headers-as-array.js b/test/headers-as-array.js index 6717d0a2438..01d181c68dd 100644 --- a/test/headers-as-array.js +++ b/test/headers-as-array.js @@ -20,7 +20,7 @@ test('handle headers as array', (t) => { client.request({ path: '/', method: 'GET', - headers: headers + headers }, () => {}) }) }) @@ -38,7 +38,7 @@ test('fail if headers array is odd', (t) => { client.request({ path: '/', method: 'GET', - headers: headers + headers }, (err) => { t.ok(err instanceof errors.InvalidArgumentError) t.strictEqual(err.message, 'headers array must be even') From 176cce27d027eb91e99e0c012db3c22be6ecd6f9 Mon Sep 17 00:00:00 2001 From: Igor Savin Date: Wed, 18 May 2022 15:38:22 +0300 Subject: [PATCH 04/35] Implement support for querystring params --- README.md | 2 ++ lib/core/request.js | 6 +++-- test/client.js | 55 ++++++++++++++++++++++++++++++++++++++++++++- types/client.d.ts | 2 ++ 4 files changed, 62 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 827ad6900bb..d641b089a53 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,8 @@ Options: Default: `null`. * `headers: Object|Array|Null`, an object with header-value pairs or an array with header-value pairs bi-indexed (`['header1', 'value1', 'header2', 'value2']`). Default: `null`. +* `params: Object|Null`, an object with query string param value pairs. + Default: `null`. * `signal: AbortSignal|EventEmitter|Null` Default: `null`. * `idempotent: Boolean`, whether the requests can be safely retried or not. diff --git a/lib/core/request.js b/lib/core/request.js index 919805d275d..9d92703e366 100644 --- a/lib/core/request.js +++ b/lib/core/request.js @@ -4,8 +4,9 @@ const { InvalidArgumentError, NotSupportedError } = require('./errors') -const util = require('./util') const assert = require('assert') +const querystring = require('querystring') +const util = require('./util') const kHandler = Symbol('handler') @@ -15,6 +16,7 @@ class Request { method, body, headers, + params, idempotent, upgrade }, handler) { @@ -50,7 +52,7 @@ class Request { this.upgrade = upgrade || method === 'CONNECT' || null - this.path = path + this.path = params && Object.keys(params).length > 0 ? `${path}?${querystring.stringify(params)}` : path this.idempotent = idempotent == null ? method === 'HEAD' || method === 'GET' diff --git a/test/client.js b/test/client.js index bce05ff0142..e7a453d9f31 100644 --- a/test/client.js +++ b/test/client.js @@ -1,12 +1,13 @@ 'use strict' +const EE = require('events') +const querystring = require('querystring') const { test } = require('tap') const { Client, errors } = require('..') const { createServer } = require('http') const { readFileSync, createReadStream } = require('fs') const { Readable } = require('stream') const { kSocket } = require('../lib/core/symbols') -const EE = require('events') const { kConnect } = require('../lib/core/symbols') test('basic get', (t) => { @@ -79,6 +80,48 @@ test('basic get', (t) => { }) }) +test('basic get with query params', (t) => { + t.plan(5) + + const server = createServer(serverRequestParams(t, { + foo: '1', + bar: 'bar', + multi: [ + '1', + '2' + ] + })) + t.tearDown(server.close.bind(server)) + + const params = { + foo: 1, + bar: 'bar', + multi: [1, 2] + } + + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`, { + keepAliveTimeout: 300e3 + }) + t.tearDown(client.close.bind(client)) + + t.strictEqual(client.url.origin, `http://localhost:${server.address().port}`) + + const signal = new EE() + client.request({ + signal, + path: '/', + method: 'GET', + params + }, (err, data) => { + t.error(err) + const { statusCode } = data + t.strictEqual(statusCode, 200) + }) + t.strictEqual(signal.listenerCount('abort'), 1) + }) +}) + test('basic head', (t) => { t.plan(14) @@ -179,6 +222,16 @@ test('head with host header', (t) => { }) }) +function serverRequestParams (t, expected) { + return function (req, res) { + const queryParams = querystring.parse(req.url.replace('/?', '')) // We remove opening /? because parser assumes it to be a part of param name + t.strictSame(JSON.parse(JSON.stringify(queryParams)), expected) + + req.setEncoding('utf8') + res.end('hello') + } +} + function postServer (t, expected) { return function (req, res) { t.strictEqual(req.url, '/') diff --git a/types/client.d.ts b/types/client.d.ts index 020dbe767b0..11569225924 100644 --- a/types/client.d.ts +++ b/types/client.d.ts @@ -89,6 +89,8 @@ declare namespace Client { body?: string | Buffer | Uint8Array | Readable | null; /** Default: `null` */ headers?: IncomingHttpHeaders | null; + /** Default: 'null' */ + params?: Record | null; /** The timeout after which a request will time out, in milliseconds. Monitors time between receiving a complete headers. Use 0 to disable it entirely. Default: `30e3` milliseconds (30s). */ headersTimeout?: number; /** The timeout after which a request will time out, in milliseconds. Monitors time between receiving a body data. Use 0 to disable it entirely. Default: `30e3` milliseconds (30s). */ From bf7433a19b82eb62efefb46854bb193d8fedc6db Mon Sep 17 00:00:00 2001 From: Igor Savin Date: Wed, 18 May 2022 15:50:40 +0300 Subject: [PATCH 05/35] Remove leaked change --- lib/redirect.js | 146 ------------------------------------------------ 1 file changed, 146 deletions(-) delete mode 100644 lib/redirect.js diff --git a/lib/redirect.js b/lib/redirect.js deleted file mode 100644 index e70f5205e9c..00000000000 --- a/lib/redirect.js +++ /dev/null @@ -1,146 +0,0 @@ -const util = require('./core/util') -const { InvalidArgumentError } = require('./core/errors') - -function dispatch (origin, agent, { maxRedirections = 10, ...opts }, handler) { - if (maxRedirections != null && (maxRedirections < 0 || !Number.isInteger(maxRedirections))) { - throw new InvalidArgumentError('maxRedirections must be a positive number') - } - - const client = agent.get(origin) - - if (!maxRedirections) { - return client.dispatch(opts, handler) - } - - // TODO (fix): Restartable streams? - if (util.isStream(opts.body) && util.bodyLength(opts.body) !== 0) { - return client.dispatch(opts, handler) - } - - client.dispatch(opts, { - agent, - opts: { ...opts, maxRedirections: maxRedirections - 1 }, - origin, - maxRedirections, - handler, - location: null, - - onConnect (abort) { - this.onConnect(abort) - }, - - onUpgrade (statusCode, headers, socket) { - this.onUpgrade(statusCode, headers, socket) - }, - - onError (error) { - this.onError(error) - }, - - onHeaders (statusCode, headers, resume) { - // Check if statusCode is 3xx, if there is a location header and if the redirection can be followed - this.location = this.maxRedirections - ? redirectLocation(statusCode, headers) - : null - - if (!this.location) { - return this.handler.onHeaders(statusCode, headers, resume) - } - - this.location = new URL(this.location, this.origin) - - // Remove headers referring to the original URL. - // By default it is Host only, unless it's a 303 (see below), which removes also all Content-* headers. - // https://tools.ietf.org/html/rfc7231#section-6.4 - this.opts.headers = cleanRequestHeaders(this.opts.headers, statusCode === 303) - - // https://tools.ietf.org/html/rfc7231#section-6.4.4 - // In case of HTTP 303, always replace method to be either HEAD or GET - if (statusCode === 303) { - this.opts.method = 'GET' - // TOOD (fix): What if body? - } - }, - - onData (chunk) { - if (!this.location) { - return this.handler.onData(chunk) - } - }, - - onComplete (trailers) { - if (this.location) { - dispatch(this.location, this.opts, this.handler, this.agent) - } else { - this.handler.onComplete(trailers) - } - } - }) -} - -function redirectLocation (statusCode, headers) { - // TODO (fix): Different redirect codes are handled differently? - if ([300, 301, 302, 303, 307, 308].indexOf(statusCode) === -1) { - return null - } - - for (let i = 0; i < headers.length; i += 2) { - // Find the matching headers, then return its value - if (headers[i].length && headers[i].toLowerCase() === 'location') { - return headers[i + 1] - } - } - - return null -} - -// https://tools.ietf.org/html/rfc7231#section-6.4.4 -function shouldRemoveHeader (header, removeContent) { - return ( - (header.length === 4 && header.toLowerCase() === 'host') || - (removeContent && header.toLowerCase().indexOf('content-') === 0) - ) -} - -// https://tools.ietf.org/html/rfc7231#section-6.4 -function cleanRequestHeaders (headers, removeContent) { - const ret = [] - if (Array.isArray(headers)) { - for (let i = 0; i < headers.length; i += 2) { - if (!shouldRemoveHeader(headers[i], removeContent)) { - ret.push(headers[i], headers[i + 1]) - } - } - } else if (headers && typeof headers === 'object') { - for (const [key, val] of Object.entries(headers)) { - if (!shouldRemoveHeader(key, removeContent)) { - ret.push(key, val) - } - } - } else if (headers != null) { - throw new InvalidArgumentError('headers must be an object or an array') - } - return ret -} - -function dispatchWithRedirectAgent (fn) { - return (url, { agent, method = 'GET', ...opts } = {}, ...additionalArgs) => { - if (opts.path != null) { - throw new InvalidArgumentError('unsupported opts.path') - } - - const { origin, pathname, search } = util.parseURL(url) - - const path = `${pathname || '/'}${search || ''}` - - return fn.call({ dispatch: dispatch.bind(null, origin, agent) }, { ...opts, method, path }, ...additionalArgs) - } -} - -module.exports = { - request: dispatchWithRedirectAgent(require('./client-request')), - stream: dispatchWithRedirectAgent(require('./client-stream')), - pipeline: dispatchWithRedirectAgent(require('./client-pipeline')), - connect: dispatchWithRedirectAgent(require('./client-connect')), - upgrade: dispatchWithRedirectAgent(require('./client-upgrade')) -} From 433a455f5e88a451ed0e0d7e41f2c6dcadf36190 Mon Sep 17 00:00:00 2001 From: Igor Savin Date: Wed, 18 May 2022 15:58:29 +0300 Subject: [PATCH 06/35] Fix test --- test/client.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/client.js b/test/client.js index b4aa6870efe..11a91d84f20 100644 --- a/test/client.js +++ b/test/client.js @@ -82,7 +82,7 @@ test('basic get', (t) => { }) test('basic get with query params', (t) => { - t.plan(5) + t.plan(4) const server = createServer(serverRequestParams(t, { foo: '1', @@ -106,8 +106,6 @@ test('basic get with query params', (t) => { }) t.tearDown(client.close.bind(client)) - t.strictEqual(client.url.origin, `http://localhost:${server.address().port}`) - const signal = new EE() client.request({ signal, From 5afcdc8cd822aaf7806a1639a4cf8a6e080762f6 Mon Sep 17 00:00:00 2001 From: Igor Savin Date: Wed, 18 May 2022 16:28:38 +0300 Subject: [PATCH 07/35] Address code review comments --- docs/api/Dispatcher.md | 1 + lib/core/request.js | 8 +++++++- test/client.js | 46 +++++++++++++++++++++++++++++++++++------- 3 files changed, 47 insertions(+), 8 deletions(-) diff --git a/docs/api/Dispatcher.md b/docs/api/Dispatcher.md index 56b34275209..2cbc6088163 100644 --- a/docs/api/Dispatcher.md +++ b/docs/api/Dispatcher.md @@ -194,6 +194,7 @@ Returns: `Boolean` - `false` if dispatcher is busy and further dispatch calls wo * **method** `string` * **body** `string | Buffer | Uint8Array | stream.Readable | Iterable | AsyncIterable | null` (optional) - Default: `null` * **headers** `UndiciHeaders | string[]` (optional) - Default: `null`. +* **params** `Record 0 ? `${path}?${querystring.stringify(params)}` : path + if (params && Object.keys(params).length > 0) { + const searchParams = new URLSearchParams(params) + const delimiter = path.indexOf('?') === -1 ? '?' : '&' + this.path = `${path}${delimiter}${searchParams.toString()}` + } else { + this.path = path + } this.origin = origin diff --git a/test/client.js b/test/client.js index 11a91d84f20..0b39d88a13e 100644 --- a/test/client.js +++ b/test/client.js @@ -3,7 +3,6 @@ const EE = require('events') const { readFileSync, createReadStream } = require('fs') const { createServer } = require('http') -const querystring = require('querystring') const { Readable } = require('stream') const { test } = require('tap') const { Client, errors } = require('..') @@ -87,10 +86,7 @@ test('basic get with query params', (t) => { const server = createServer(serverRequestParams(t, { foo: '1', bar: 'bar', - multi: [ - '1', - '2' - ] + multi: '1,2' })) t.tearDown(server.close.bind(server)) @@ -121,6 +117,40 @@ test('basic get with query params', (t) => { }) }) +test('basic get with query params partially in path', (t) => { + t.plan(4) + + const server = createServer(serverRequestParams(t, { + foo: '1', + bar: '2' + })) + t.tearDown(server.close.bind(server)) + + const params = { + foo: 1, + } + + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`, { + keepAliveTimeout: 300e3 + }) + t.tearDown(client.close.bind(client)) + + const signal = new EE() + client.request({ + signal, + path: '/?bar=2', + method: 'GET', + params + }, (err, data) => { + t.error(err) + const { statusCode } = data + t.strictEqual(statusCode, 200) + }) + t.strictEqual(signal.listenerCount('abort'), 1) + }) +}) + test('basic head', (t) => { t.plan(14) @@ -294,8 +324,10 @@ test('head with host header', (t) => { function serverRequestParams (t, expected) { return function (req, res) { - const queryParams = querystring.parse(req.url.replace('/?', '')) // We remove opening /? because parser assumes it to be a part of param name - t.strictSame(JSON.parse(JSON.stringify(queryParams)), expected) + const [ , paramString ] = req.url.split( '?' ); + const searchParams = new URLSearchParams( paramString ); + const searchParamsObject = Object.fromEntries(searchParams.entries()) + t.strictSame(searchParamsObject, expected) req.setEncoding('utf8') res.end('hello') From 05bc04291b6d88cee9aee526e4a79c1994061854 Mon Sep 17 00:00:00 2001 From: Igor Savin Date: Wed, 18 May 2022 16:34:14 +0300 Subject: [PATCH 08/35] Address linting errors --- lib/core/request.js | 1 - test/client.js | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/core/request.js b/lib/core/request.js index 11c3fdcfdad..3c82329ee45 100644 --- a/lib/core/request.js +++ b/lib/core/request.js @@ -5,7 +5,6 @@ const { NotSupportedError } = require('./errors') const assert = require('assert') -const querystring = require('querystring') const util = require('./util') const kHandler = Symbol('handler') diff --git a/test/client.js b/test/client.js index 0b39d88a13e..60bea75d635 100644 --- a/test/client.js +++ b/test/client.js @@ -127,7 +127,7 @@ test('basic get with query params partially in path', (t) => { t.tearDown(server.close.bind(server)) const params = { - foo: 1, + foo: 1 } server.listen(0, () => { @@ -324,8 +324,8 @@ test('head with host header', (t) => { function serverRequestParams (t, expected) { return function (req, res) { - const [ , paramString ] = req.url.split( '?' ); - const searchParams = new URLSearchParams( paramString ); + const [, paramString] = req.url.split('?') + const searchParams = new URLSearchParams(paramString) const searchParamsObject = Object.fromEntries(searchParams.entries()) t.strictSame(searchParamsObject, expected) From 52089aacfef87429c22eb5b48ed583103a8e001b Mon Sep 17 00:00:00 2001 From: Igor Savin Date: Wed, 18 May 2022 16:37:02 +0300 Subject: [PATCH 09/35] Improve coverage --- test/client.js | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/test/client.js b/test/client.js index 60bea75d635..52d1844e826 100644 --- a/test/client.js +++ b/test/client.js @@ -117,6 +117,35 @@ test('basic get with query params', (t) => { }) }) +test('basic get with empty query params', (t) => { + t.plan(4) + + const server = createServer(serverRequestParams(t, {})) + t.tearDown(server.close.bind(server)) + + const params = {} + + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`, { + keepAliveTimeout: 300e3 + }) + t.tearDown(client.close.bind(client)) + + const signal = new EE() + client.request({ + signal, + path: '/', + method: 'GET', + params + }, (err, data) => { + t.error(err) + const { statusCode } = data + t.strictEqual(statusCode, 200) + }) + t.strictEqual(signal.listenerCount('abort'), 1) + }) +}) + test('basic get with query params partially in path', (t) => { t.plan(4) From a89e7aaac6c88e5279ffeb1e160f19ac2e490747 Mon Sep 17 00:00:00 2001 From: Igor Savin Date: Wed, 18 May 2022 21:36:10 +0300 Subject: [PATCH 10/35] Comment out new tests to see if they are causing problems in CI --- test/client.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/client.js b/test/client.js index 52d1844e826..48a670388fd 100644 --- a/test/client.js +++ b/test/client.js @@ -80,6 +80,7 @@ test('basic get', (t) => { }) }) +/* test('basic get with query params', (t) => { t.plan(4) @@ -180,6 +181,8 @@ test('basic get with query params partially in path', (t) => { }) }) + + */ test('basic head', (t) => { t.plan(14) From 06a00939e627239f2873906d83afef0edc9e013d Mon Sep 17 00:00:00 2001 From: Igor Savin Date: Wed, 18 May 2022 21:52:16 +0300 Subject: [PATCH 11/35] Improve coverage --- test/client.js | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/test/client.js b/test/client.js index 48a670388fd..d465cadfec0 100644 --- a/test/client.js +++ b/test/client.js @@ -1,6 +1,5 @@ 'use strict' -const EE = require('events') const { readFileSync, createReadStream } = require('fs') const { createServer } = require('http') const { Readable } = require('stream') @@ -8,6 +7,7 @@ const { test } = require('tap') const { Client, errors } = require('..') const { kSocket } = require('../lib/core/symbols') const { wrapWithAsyncIterable } = require('./utils/async-iterators') +const EE = require('events') const { kUrl, kSize, kConnect, kBusy, kConnected, kRunning } = require('../lib/core/symbols') test('basic get', (t) => { @@ -80,16 +80,24 @@ test('basic get', (t) => { }) }) -/* + test('basic get with query params', (t) => { t.plan(4) - const server = createServer(serverRequestParams(t, { - foo: '1', - bar: 'bar', - multi: '1,2' - })) - t.tearDown(server.close.bind(server)) + const server = createServer((req, res) => { + const [, paramString] = req.url.split('?') + const searchParams = new URLSearchParams(paramString) + const searchParamsObject = Object.fromEntries(searchParams.entries()) + t.strictSame(searchParamsObject, { + foo: '1', + bar: 'bar', + multi: '1,2' + }) + + res.statusCode = 200 + res.end('hello') + }) + t.teardown(server.close.bind(server)) const params = { foo: 1, @@ -118,6 +126,7 @@ test('basic get with query params', (t) => { }) }) +/* test('basic get with empty query params', (t) => { t.plan(4) @@ -180,9 +189,8 @@ test('basic get with query params partially in path', (t) => { t.strictEqual(signal.listenerCount('abort'), 1) }) }) - - */ + test('basic head', (t) => { t.plan(14) @@ -361,7 +369,7 @@ function serverRequestParams (t, expected) { const searchParamsObject = Object.fromEntries(searchParams.entries()) t.strictSame(searchParamsObject, expected) - req.setEncoding('utf8') + res.statusCode = 200 res.end('hello') } } From 18744d4b9257eec5fc1e114c9b057b579fca36b4 Mon Sep 17 00:00:00 2001 From: Igor Savin Date: Wed, 18 May 2022 21:54:59 +0300 Subject: [PATCH 12/35] Fix linting --- test/client.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/client.js b/test/client.js index d465cadfec0..493074d0ad0 100644 --- a/test/client.js +++ b/test/client.js @@ -80,7 +80,6 @@ test('basic get', (t) => { }) }) - test('basic get with query params', (t) => { t.plan(4) @@ -362,6 +361,7 @@ test('head with host header', (t) => { }) }) +/* function serverRequestParams (t, expected) { return function (req, res) { const [, paramString] = req.url.split('?') @@ -373,6 +373,7 @@ function serverRequestParams (t, expected) { res.end('hello') } } + */ function postServer (t, expected) { return function (req, res) { From 3fdd223b71b942212fcd3dd2e8b17c96e3524c6e Mon Sep 17 00:00:00 2001 From: Igor Savin Date: Wed, 18 May 2022 22:35:09 +0300 Subject: [PATCH 13/35] Adjust two other tests too --- test/client.js | 47 ++++++++++++++++++++++++----------------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/test/client.js b/test/client.js index 493074d0ad0..4b8ce357796 100644 --- a/test/client.js +++ b/test/client.js @@ -125,12 +125,19 @@ test('basic get with query params', (t) => { }) }) -/* test('basic get with empty query params', (t) => { t.plan(4) - const server = createServer(serverRequestParams(t, {})) - t.tearDown(server.close.bind(server)) + const server = createServer((req, res) => { + const [, paramString] = req.url.split('?') + const searchParams = new URLSearchParams(paramString) + const searchParamsObject = Object.fromEntries(searchParams.entries()) + t.strictSame(searchParamsObject, {}) + + res.statusCode = 200 + res.end('hello') + }) + t.teardown(server.close.bind(server)) const params = {} @@ -158,11 +165,20 @@ test('basic get with empty query params', (t) => { test('basic get with query params partially in path', (t) => { t.plan(4) - const server = createServer(serverRequestParams(t, { - foo: '1', - bar: '2' - })) - t.tearDown(server.close.bind(server)) + const server = createServer((req, res) => { + const [, paramString] = req.url.split('?') + const searchParams = new URLSearchParams(paramString) + const searchParamsObject = Object.fromEntries(searchParams.entries()) + t.strictSame(searchParamsObject, { + foo: '1', + bar: '2' + }) + + res.statusCode = 200 + res.end('hello') + }) + t.teardown(server.close.bind(server)) + const params = { foo: 1 @@ -188,7 +204,6 @@ test('basic get with query params partially in path', (t) => { t.strictEqual(signal.listenerCount('abort'), 1) }) }) - */ test('basic head', (t) => { t.plan(14) @@ -361,20 +376,6 @@ test('head with host header', (t) => { }) }) -/* -function serverRequestParams (t, expected) { - return function (req, res) { - const [, paramString] = req.url.split('?') - const searchParams = new URLSearchParams(paramString) - const searchParamsObject = Object.fromEntries(searchParams.entries()) - t.strictSame(searchParamsObject, expected) - - res.statusCode = 200 - res.end('hello') - } -} - */ - function postServer (t, expected) { return function (req, res) { t.equal(req.url, '/') From 8f6f99892505b3cfb1c4f654cbfa6e56f353f2eb Mon Sep 17 00:00:00 2001 From: Igor Savin Date: Wed, 18 May 2022 22:35:40 +0300 Subject: [PATCH 14/35] Remove excessive condition Co-authored-by: Robert Nagy --- lib/core/request.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/core/request.js b/lib/core/request.js index 3c82329ee45..0b8b6ad7241 100644 --- a/lib/core/request.js +++ b/lib/core/request.js @@ -98,7 +98,7 @@ class Request { this.upgrade = upgrade || null - if (params && Object.keys(params).length > 0) { + if (params) { const searchParams = new URLSearchParams(params) const delimiter = path.indexOf('?') === -1 ? '?' : '&' this.path = `${path}${delimiter}${searchParams.toString()}` From 4c711c63616210c43973212d34c50772a718533a Mon Sep 17 00:00:00 2001 From: Igor Savin Date: Wed, 18 May 2022 22:43:07 +0300 Subject: [PATCH 15/35] Fix linting --- test/client.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/client.js b/test/client.js index 4b8ce357796..d60c0400e87 100644 --- a/test/client.js +++ b/test/client.js @@ -179,7 +179,6 @@ test('basic get with query params partially in path', (t) => { }) t.teardown(server.close.bind(server)) - const params = { foo: 1 } From 5d3502593b3f03e4cb7a0781662e4298d2a45d7e Mon Sep 17 00:00:00 2001 From: Igor Savin Date: Wed, 18 May 2022 23:37:18 +0300 Subject: [PATCH 16/35] Handle additional use-cases --- lib/core/request.js | 5 +-- lib/core/util.js | 93 ++++++++++++++++++++++++++++++++++++++- test/client.js | 104 ++++++++++++++++++++++++++++++++++++-------- 3 files changed, 181 insertions(+), 21 deletions(-) diff --git a/lib/core/request.js b/lib/core/request.js index 0b8b6ad7241..868d415163d 100644 --- a/lib/core/request.js +++ b/lib/core/request.js @@ -6,6 +6,7 @@ const { } = require('./errors') const assert = require('assert') const util = require('./util') +const { buildURL } = require('./util') const kHandler = Symbol('handler') @@ -99,9 +100,7 @@ class Request { this.upgrade = upgrade || null if (params) { - const searchParams = new URLSearchParams(params) - const delimiter = path.indexOf('?') === -1 ? '?' : '&' - this.path = `${path}${delimiter}${searchParams.toString()}` + this.path = buildURL(path, params) } else { this.path = path } diff --git a/lib/core/util.js b/lib/core/util.js index 0cb60b0d2a4..4dd9e88f6ce 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -26,6 +26,96 @@ function isBlobLike (object) { ) } +/** + * Iterate over an Array or an Object invoking a function for each item. + * + * If `obj` is an Array callback will be called passing + * the value, index, and complete array for each item. + * + * If 'obj' is an Object callback will be called passing + * the value, key, and complete object for each property. + * + * @param {Object|Array} obj The object to iterate + * @param {Function} fn The callback to invoke for each item + */ +function forEach (obj, fn) { + // Don't bother if no value provided + if (obj === null || typeof obj === 'undefined') { + return + } + + // Force an array if not already something iterable + if (typeof obj !== 'object') { + /*eslint no-param-reassign:0*/ + obj = [obj] + } + + if (Array.isArray(obj)) { + // Iterate over array values + for (var i = 0, l = obj.length; i < l; i++) { + fn.call(null, obj[i], i, obj) + } + } else { + // Iterate over object keys + for (var key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + fn.call(null, obj[key], key, obj) + } + } + } +} + +function encode (val) { + return encodeURIComponent(val).replace(/%3A/gi, ':').replace(/%24/g, '$').replace(/%2C/gi, ',').replace(/%20/g, '+').replace(/%5B/gi, '[').replace(/%5D/gi, ']') +} + +function isDate (val) { + return toString.call(val) === '[object Date]' +} + +function isObject (val) { + return val !== null && typeof val === 'object' +} + +function buildURL (url, params) { + if (!params) { + return url + } + + const parts = [] + forEach(params, function serialize (val, key) { + if (val === null || typeof val === 'undefined') { + return + } + + if (!Array.isArray(val)) { + val = [val] + } + + forEach(val, function parseValue (v) { + if (isDate(v)) { + v = v.toISOString() + } else if (isObject(v)) { + v = JSON.stringify(v) + } + parts.push(encode(key) + '=' + encode(v)) + }) + }) + + const serializedParams = parts.join('&') + + if (serializedParams) { + const hashmarkIndex = url.indexOf('#') + if (hashmarkIndex !== -1) { + url = url.slice(0, hashmarkIndex) + } + + url += (url.indexOf('?') === -1 ? '?' : '&') + serializedParams + } + + return url +} + function parseURL (url) { if (typeof url === 'string') { url = new URL(url) @@ -357,5 +447,6 @@ module.exports = { isBuffer, validateHandler, getSocketInfo, - isFormDataLike + isFormDataLike, + buildURL } diff --git a/test/client.js b/test/client.js index d60c0400e87..ef12eaed27b 100644 --- a/test/client.js +++ b/test/client.js @@ -84,13 +84,14 @@ test('basic get with query params', (t) => { t.plan(4) const server = createServer((req, res) => { - const [, paramString] = req.url.split('?') - const searchParams = new URLSearchParams(paramString) - const searchParamsObject = Object.fromEntries(searchParams.entries()) + const searchParamsObject = buildParams(req.url) t.strictSame(searchParamsObject, { + date: '1995-12-16T22:00:00.000Z', + obj: '%7B%22id%22:1%7D', + bool: 'true', foo: '1', bar: 'bar', - multi: '1,2' + multi: ['1','2'] }) res.statusCode = 200 @@ -99,6 +100,9 @@ test('basic get with query params', (t) => { t.teardown(server.close.bind(server)) const params = { + date: new Date(1995, 11, 17), + obj: { id: 1 }, + bool: true, foo: 1, bar: 'bar', multi: [1, 2] @@ -108,7 +112,7 @@ test('basic get with query params', (t) => { const client = new Client(`http://localhost:${server.address().port}`, { keepAliveTimeout: 300e3 }) - t.tearDown(client.close.bind(client)) + t.teardown(client.close.bind(client)) const signal = new EE() client.request({ @@ -119,9 +123,56 @@ test('basic get with query params', (t) => { }, (err, data) => { t.error(err) const { statusCode } = data - t.strictEqual(statusCode, 200) + t.equal(statusCode, 200) }) - t.strictEqual(signal.listenerCount('abort'), 1) + t.equal(signal.listenerCount('abort'), 1) + }) +}) + +test('basic get with query params ignores hashmark', (t) => { + t.plan(4) + + const server = createServer((req, res) => { + const searchParamsObject = buildParams(req.url) + t.strictSame(searchParamsObject, { + date: '1995-12-16T22:00:00.000Z', + obj: '%7B%22id%22:1%7D', + foo: '1', + bar: 'bar', + multi: ['1','2'] + }) + + res.statusCode = 200 + res.end('hello') + }) + t.teardown(server.close.bind(server)) + + const params = { + date: new Date(1995, 11, 17), + obj: { id: 1 }, + foo: 1, + bar: 'bar', + multi: [1, 2] + } + + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`, { + keepAliveTimeout: 300e3 + }) + t.teardown(client.close.bind(client)) + + const signal = new EE() + client.request({ + signal, + path: '/#', + method: 'GET', + params + }, (err, data) => { + t.error(err) + const { statusCode } = data + t.equal(statusCode, 200) + }) + t.equal(signal.listenerCount('abort'), 1) }) }) @@ -129,9 +180,7 @@ test('basic get with empty query params', (t) => { t.plan(4) const server = createServer((req, res) => { - const [, paramString] = req.url.split('?') - const searchParams = new URLSearchParams(paramString) - const searchParamsObject = Object.fromEntries(searchParams.entries()) + const searchParamsObject = buildParams(req.url) t.strictSame(searchParamsObject, {}) res.statusCode = 200 @@ -145,7 +194,7 @@ test('basic get with empty query params', (t) => { const client = new Client(`http://localhost:${server.address().port}`, { keepAliveTimeout: 300e3 }) - t.tearDown(client.close.bind(client)) + t.teardown(client.close.bind(client)) const signal = new EE() client.request({ @@ -156,9 +205,9 @@ test('basic get with empty query params', (t) => { }, (err, data) => { t.error(err) const { statusCode } = data - t.strictEqual(statusCode, 200) + t.equal(statusCode, 200) }) - t.strictEqual(signal.listenerCount('abort'), 1) + t.equal(signal.listenerCount('abort'), 1) }) }) @@ -166,9 +215,7 @@ test('basic get with query params partially in path', (t) => { t.plan(4) const server = createServer((req, res) => { - const [, paramString] = req.url.split('?') - const searchParams = new URLSearchParams(paramString) - const searchParamsObject = Object.fromEntries(searchParams.entries()) + const searchParamsObject = buildParams(req.url) t.strictSame(searchParamsObject, { foo: '1', bar: '2' @@ -187,7 +234,7 @@ test('basic get with query params partially in path', (t) => { const client = new Client(`http://localhost:${server.address().port}`, { keepAliveTimeout: 300e3 }) - t.tearDown(client.close.bind(client)) + t.teardown(client.close.bind(client)) const signal = new EE() client.request({ @@ -1713,3 +1760,26 @@ test('async iterator yield object error', (t) => { }) }) }) + +function buildParams(path) { + const cleanPath = path.replace('/?', '').replace('/', '').split('&') + const builtParams = cleanPath.reduce((acc, entry) => { + const [key, value] = entry.split('=') + if (key.length === 0) { + return acc + } + + if (acc[key]) { + if (Array.isArray(acc[key])) { + acc[key].push(value) + } else { + acc[key] = [acc[key], value] + } + } else { + acc[key] = value + } + return acc + }, {}) + + return builtParams +} From babcf18d1b1ec48051402aaf2f6a67070511a0a5 Mon Sep 17 00:00:00 2001 From: Igor Savin Date: Wed, 18 May 2022 23:43:05 +0300 Subject: [PATCH 17/35] Simplify --- lib/core/request.js | 2 +- lib/core/util.js | 47 ++++----------------------------------------- test/client.js | 8 ++++---- 3 files changed, 9 insertions(+), 48 deletions(-) diff --git a/lib/core/request.js b/lib/core/request.js index 868d415163d..da88bdd82e9 100644 --- a/lib/core/request.js +++ b/lib/core/request.js @@ -100,7 +100,7 @@ class Request { this.upgrade = upgrade || null if (params) { - this.path = buildURL(path, params) + this.path = buildURL(path, params) } else { this.path = path } diff --git a/lib/core/util.js b/lib/core/util.js index 4dd9e88f6ce..d64961abddf 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -26,45 +26,6 @@ function isBlobLike (object) { ) } -/** - * Iterate over an Array or an Object invoking a function for each item. - * - * If `obj` is an Array callback will be called passing - * the value, index, and complete array for each item. - * - * If 'obj' is an Object callback will be called passing - * the value, key, and complete object for each property. - * - * @param {Object|Array} obj The object to iterate - * @param {Function} fn The callback to invoke for each item - */ -function forEach (obj, fn) { - // Don't bother if no value provided - if (obj === null || typeof obj === 'undefined') { - return - } - - // Force an array if not already something iterable - if (typeof obj !== 'object') { - /*eslint no-param-reassign:0*/ - obj = [obj] - } - - if (Array.isArray(obj)) { - // Iterate over array values - for (var i = 0, l = obj.length; i < l; i++) { - fn.call(null, obj[i], i, obj) - } - } else { - // Iterate over object keys - for (var key in obj) { - if (Object.prototype.hasOwnProperty.call(obj, key)) { - fn.call(null, obj[key], key, obj) - } - } - } -} - function encode (val) { return encodeURIComponent(val).replace(/%3A/gi, ':').replace(/%24/g, '$').replace(/%2C/gi, ',').replace(/%20/g, '+').replace(/%5B/gi, '[').replace(/%5D/gi, ']') } @@ -83,7 +44,7 @@ function buildURL (url, params) { } const parts = [] - forEach(params, function serialize (val, key) { + for (let [key, val] of Object.entries(params)) { if (val === null || typeof val === 'undefined') { return } @@ -92,15 +53,15 @@ function buildURL (url, params) { val = [val] } - forEach(val, function parseValue (v) { + for (let v of val) { if (isDate(v)) { v = v.toISOString() } else if (isObject(v)) { v = JSON.stringify(v) } parts.push(encode(key) + '=' + encode(v)) - }) - }) + } + } const serializedParams = parts.join('&') diff --git a/test/client.js b/test/client.js index ef12eaed27b..90498a806d5 100644 --- a/test/client.js +++ b/test/client.js @@ -91,7 +91,7 @@ test('basic get with query params', (t) => { bool: 'true', foo: '1', bar: 'bar', - multi: ['1','2'] + multi: ['1', '2'] }) res.statusCode = 200 @@ -139,7 +139,7 @@ test('basic get with query params ignores hashmark', (t) => { obj: '%7B%22id%22:1%7D', foo: '1', bar: 'bar', - multi: ['1','2'] + multi: ['1', '2'] }) res.statusCode = 200 @@ -1761,7 +1761,7 @@ test('async iterator yield object error', (t) => { }) }) -function buildParams(path) { +function buildParams (path) { const cleanPath = path.replace('/?', '').replace('/', '').split('&') const builtParams = cleanPath.reduce((acc, entry) => { const [key, value] = entry.split('=') @@ -1774,7 +1774,7 @@ function buildParams(path) { acc[key].push(value) } else { acc[key] = [acc[key], value] - } + } } else { acc[key] = value } From 2eb2797c98b18150b13bd02ce91befc5dbafd55f Mon Sep 17 00:00:00 2001 From: Igor Savin Date: Wed, 18 May 2022 23:50:12 +0300 Subject: [PATCH 18/35] Try to fix tests in CI --- test/client.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/test/client.js b/test/client.js index 90498a806d5..a65ce5c18e8 100644 --- a/test/client.js +++ b/test/client.js @@ -83,10 +83,11 @@ test('basic get', (t) => { test('basic get with query params', (t) => { t.plan(4) + const date = new Date(1995, 11, 17) const server = createServer((req, res) => { const searchParamsObject = buildParams(req.url) t.strictSame(searchParamsObject, { - date: '1995-12-16T22:00:00.000Z', + date: date.toISOString(), obj: '%7B%22id%22:1%7D', bool: 'true', foo: '1', @@ -100,7 +101,7 @@ test('basic get with query params', (t) => { t.teardown(server.close.bind(server)) const params = { - date: new Date(1995, 11, 17), + date, obj: { id: 1 }, bool: true, foo: 1, @@ -135,7 +136,6 @@ test('basic get with query params ignores hashmark', (t) => { const server = createServer((req, res) => { const searchParamsObject = buildParams(req.url) t.strictSame(searchParamsObject, { - date: '1995-12-16T22:00:00.000Z', obj: '%7B%22id%22:1%7D', foo: '1', bar: 'bar', @@ -148,7 +148,6 @@ test('basic get with query params ignores hashmark', (t) => { t.teardown(server.close.bind(server)) const params = { - date: new Date(1995, 11, 17), obj: { id: 1 }, foo: 1, bar: 'bar', From a471788e8072b6428aa0dcf82f79cd9012a75227 Mon Sep 17 00:00:00 2001 From: Igor Savin Date: Wed, 18 May 2022 23:52:16 +0300 Subject: [PATCH 19/35] Make code more consistent --- lib/core/request.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/core/request.js b/lib/core/request.js index da88bdd82e9..e2eb67f47fc 100644 --- a/lib/core/request.js +++ b/lib/core/request.js @@ -6,7 +6,6 @@ const { } = require('./errors') const assert = require('assert') const util = require('./util') -const { buildURL } = require('./util') const kHandler = Symbol('handler') @@ -100,7 +99,7 @@ class Request { this.upgrade = upgrade || null if (params) { - this.path = buildURL(path, params) + this.path = util.buildURL(path, params) } else { this.path = path } From 7bb502ae44224bfbbb05c12b65bca10e26dd8a51 Mon Sep 17 00:00:00 2001 From: Igor Savin Date: Thu, 19 May 2022 12:22:51 +0300 Subject: [PATCH 20/35] Address code review comments --- docs/api/Dispatcher.md | 2 +- lib/core/request.js | 6 ++--- lib/core/util.js | 14 +++++++----- test/client.js | 50 ++++++++++++++++++++++++++++++++---------- types/dispatcher.d.ts | 2 ++ 5 files changed, 53 insertions(+), 21 deletions(-) diff --git a/docs/api/Dispatcher.md b/docs/api/Dispatcher.md index 2cbc6088163..39c554ea80c 100644 --- a/docs/api/Dispatcher.md +++ b/docs/api/Dispatcher.md @@ -194,7 +194,7 @@ Returns: `Boolean` - `false` if dispatcher is busy and further dispatch calls wo * **method** `string` * **body** `string | Buffer | Uint8Array | stream.Readable | Iterable | AsyncIterable | null` (optional) - Default: `null` * **headers** `UndiciHeaders | string[]` (optional) - Default: `null`. -* **params** `Record { const searchParamsObject = buildParams(req.url) t.strictSame(searchParamsObject, { date: date.toISOString(), - obj: '%7B%22id%22:1%7D', bool: 'true', foo: '1', bar: 'bar', @@ -100,9 +99,8 @@ test('basic get with query params', (t) => { }) t.teardown(server.close.bind(server)) - const params = { + const query = { date, - obj: { id: 1 }, bool: true, foo: 1, bar: 'bar', @@ -120,7 +118,7 @@ test('basic get with query params', (t) => { signal, path: '/', method: 'GET', - params + query }, (err, data) => { t.error(err) const { statusCode } = data @@ -130,13 +128,42 @@ test('basic get with query params', (t) => { }) }) +test('basic get with query params with object throws an error', (t) => { + t.plan(1) + + const server = createServer((req, res) => { + t.fail() + }) + t.teardown(server.close.bind(server)) + + const query = { + obj: { id : 1 }, + } + + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`, { + keepAliveTimeout: 300e3 + }) + t.teardown(client.close.bind(client)) + + const signal = new EE() + client.request({ + signal, + path: '/', + method: 'GET', + query + }, (err, data) => { + t.equal(err.message, 'Passing object as a query param is not supported, please serialize to string up-front') + }) + }) +}) + test('basic get with query params ignores hashmark', (t) => { t.plan(4) const server = createServer((req, res) => { const searchParamsObject = buildParams(req.url) t.strictSame(searchParamsObject, { - obj: '%7B%22id%22:1%7D', foo: '1', bar: 'bar', multi: ['1', '2'] @@ -147,8 +174,7 @@ test('basic get with query params ignores hashmark', (t) => { }) t.teardown(server.close.bind(server)) - const params = { - obj: { id: 1 }, + const query = { foo: 1, bar: 'bar', multi: [1, 2] @@ -165,7 +191,7 @@ test('basic get with query params ignores hashmark', (t) => { signal, path: '/#', method: 'GET', - params + query }, (err, data) => { t.error(err) const { statusCode } = data @@ -187,7 +213,7 @@ test('basic get with empty query params', (t) => { }) t.teardown(server.close.bind(server)) - const params = {} + const query = {} server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { @@ -200,7 +226,7 @@ test('basic get with empty query params', (t) => { signal, path: '/', method: 'GET', - params + query }, (err, data) => { t.error(err) const { statusCode } = data @@ -225,7 +251,7 @@ test('basic get with query params partially in path', (t) => { }) t.teardown(server.close.bind(server)) - const params = { + const query = { foo: 1 } @@ -240,7 +266,7 @@ test('basic get with query params partially in path', (t) => { signal, path: '/?bar=2', method: 'GET', - params + query }, (err, data) => { t.error(err) const { statusCode } = data diff --git a/types/dispatcher.d.ts b/types/dispatcher.d.ts index 9b2af26e6d2..7a48a5a3366 100644 --- a/types/dispatcher.d.ts +++ b/types/dispatcher.d.ts @@ -47,6 +47,8 @@ declare namespace Dispatcher { body?: string | Buffer | Uint8Array | Readable | null | FormData; /** Default: `null` */ headers?: IncomingHttpHeaders | string[] | null; + /** Query string params to be embedded in the request URL. Default: `null` */ + query?: Record; /** Whether the requests can be safely retried or not. If `false` the request won't be sent until all preceding requests in the pipeline have completed. Default: `true` if `method` is `HEAD` or `GET`. */ idempotent?: boolean; /** Upgrade the request. Should be used to specify the kind of upgrade i.e. `'Websocket'`. Default: `method === 'CONNECT' || null`. */ From edd2272aae6c29ae0002aca0c6631816173e866a Mon Sep 17 00:00:00 2001 From: Igor Savin Date: Thu, 19 May 2022 12:26:24 +0300 Subject: [PATCH 21/35] Fix linting --- test/client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/client.js b/test/client.js index f6e6fa89b0b..545aaa2153f 100644 --- a/test/client.js +++ b/test/client.js @@ -137,7 +137,7 @@ test('basic get with query params with object throws an error', (t) => { t.teardown(server.close.bind(server)) const query = { - obj: { id : 1 }, + obj: { id: 1 } } server.listen(0, () => { From fb9f35407e0debfa7846762abf7af4435920ef76 Mon Sep 17 00:00:00 2001 From: Igor Savin Date: Thu, 19 May 2022 12:37:09 +0300 Subject: [PATCH 22/35] Cleanup --- lib/core/util.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/core/util.js b/lib/core/util.js index 2e027cb0e0c..212c45bef79 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -39,8 +39,6 @@ function isObject (val) { } function buildURL (url, queryParams) { - console.log('rara') - console.log(JSON.stringify(queryParams)) if (!queryParams) { return url } @@ -76,8 +74,6 @@ function buildURL (url, queryParams) { url += (url.includes('?') ? '&' : '?') + serializedParams } - console.log('afgerg') - console.log(url) return url } From ad84f02a68678d4d022498657ca61635a35906d9 Mon Sep 17 00:00:00 2001 From: Igor Savin Date: Thu, 19 May 2022 13:55:19 +0300 Subject: [PATCH 23/35] Cleanup and improve coverage --- lib/core/util.js | 14 +++++--------- test/client.js | 18 +++++------------- 2 files changed, 10 insertions(+), 22 deletions(-) diff --git a/lib/core/util.js b/lib/core/util.js index 212c45bef79..1f9ac6ddc7d 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -26,10 +26,6 @@ function isBlobLike (object) { ) } -function encode (val) { - return encodeURIComponent(val).replace(/%3A/gi, ':').replace(/%24/g, '$').replace(/%2C/gi, ',').replace(/%20/g, '+').replace(/%5B/gi, '[').replace(/%5D/gi, ']') -} - function isDate (val) { return toString.call(val) === '[object Date]' } @@ -39,14 +35,14 @@ function isObject (val) { } function buildURL (url, queryParams) { - if (!queryParams) { - return url + if (url.includes('?')) { + throw new Error('Query params cannot be passed when url contains "?" already.') } const parts = [] for (let [key, val] of Object.entries(queryParams)) { if (val === null || typeof val === 'undefined') { - return + continue } if (!Array.isArray(val)) { @@ -59,7 +55,7 @@ function buildURL (url, queryParams) { } else if (isObject(v)) { throw new Error('Passing object as a query param is not supported, please serialize to string up-front') } - parts.push(encode(key) + '=' + encode(v)) + parts.push(key + '=' + v) } } @@ -71,7 +67,7 @@ function buildURL (url, queryParams) { url = url.slice(0, hashmarkIndex) } - url += (url.includes('?') ? '&' : '?') + serializedParams + url += '&' + serializedParams } return url diff --git a/test/client.js b/test/client.js index 545aaa2153f..a10a0a36c70 100644 --- a/test/client.js +++ b/test/client.js @@ -104,6 +104,8 @@ test('basic get with query params', (t) => { bool: true, foo: 1, bar: 'bar', + nullVal: null, + undefinedVal: undefined, multi: [1, 2] } @@ -237,17 +239,10 @@ test('basic get with empty query params', (t) => { }) test('basic get with query params partially in path', (t) => { - t.plan(4) + t.plan(1) const server = createServer((req, res) => { - const searchParamsObject = buildParams(req.url) - t.strictSame(searchParamsObject, { - foo: '1', - bar: '2' - }) - - res.statusCode = 200 - res.end('hello') + t.fail() }) t.teardown(server.close.bind(server)) @@ -268,11 +263,8 @@ test('basic get with query params partially in path', (t) => { method: 'GET', query }, (err, data) => { - t.error(err) - const { statusCode } = data - t.strictEqual(statusCode, 200) + t.equal(err.message, 'Query params cannot be passed when url contains "?" already.') }) - t.strictEqual(signal.listenerCount('abort'), 1) }) }) From 86014ddc0949643de27a38b17db985454d56a092 Mon Sep 17 00:00:00 2001 From: Igor Savin Date: Thu, 19 May 2022 14:01:27 +0300 Subject: [PATCH 24/35] Restore encoding with explanation --- lib/core/util.js | 9 ++++++++- test/client.js | 2 ++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/core/util.js b/lib/core/util.js index 1f9ac6ddc7d..66681cc8879 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -34,6 +34,13 @@ function isObject (val) { return val !== null && typeof val === 'object' } +// based on https://github.com/axios/axios/blob/63e559fa609c40a0a460ae5d5a18c3470ffc6c9e/lib/helpers/buildURL.js (MIT license) +// this escapes all non-browser-url friendly characters, but restores the ones that are supported by browsers +function encode(val) { + return encodeURIComponent(val).replace(/%3A/gi, ':').replace(/%24/g, '$').replace(/%2C/gi, ',').replace(/%20/g, '+').replace(/%5B/gi, '[').replace(/%5D/gi, ']') +} + +// based on https://github.com/axios/axios/blob/63e559fa609c40a0a460ae5d5a18c3470ffc6c9e/lib/helpers/buildURL.js (MIT license) function buildURL (url, queryParams) { if (url.includes('?')) { throw new Error('Query params cannot be passed when url contains "?" already.') @@ -55,7 +62,7 @@ function buildURL (url, queryParams) { } else if (isObject(v)) { throw new Error('Passing object as a query param is not supported, please serialize to string up-front') } - parts.push(key + '=' + v) + parts.push(encode(key) + '=' + encode(v)) } } diff --git a/test/client.js b/test/client.js index a10a0a36c70..dbf669ba234 100644 --- a/test/client.js +++ b/test/client.js @@ -91,6 +91,7 @@ test('basic get with query params', (t) => { bool: 'true', foo: '1', bar: 'bar', + ':$,%2B[]%40%5E*()-': ':$,%2B[]%40%5E*()-', multi: ['1', '2'] }) @@ -106,6 +107,7 @@ test('basic get with query params', (t) => { bar: 'bar', nullVal: null, undefinedVal: undefined, + ':$,+[]@^*()-': ':$,+[]@^*()-', multi: [1, 2] } From 5e0bf8b29b4a2d0592b406d4d0ae8f307c7f3ece Mon Sep 17 00:00:00 2001 From: Igor Savin Date: Thu, 19 May 2022 14:03:19 +0300 Subject: [PATCH 25/35] Fix linting --- lib/core/util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/core/util.js b/lib/core/util.js index 66681cc8879..d0b0e3627aa 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -36,7 +36,7 @@ function isObject (val) { // based on https://github.com/axios/axios/blob/63e559fa609c40a0a460ae5d5a18c3470ffc6c9e/lib/helpers/buildURL.js (MIT license) // this escapes all non-browser-url friendly characters, but restores the ones that are supported by browsers -function encode(val) { +function encode (val) { return encodeURIComponent(val).replace(/%3A/gi, ':').replace(/%24/g, '$').replace(/%2C/gi, ',').replace(/%20/g, '+').replace(/%5B/gi, '[').replace(/%5D/gi, ']') } From ffc6db2fb6e634417bab7b3f67f57f625e348109 Mon Sep 17 00:00:00 2001 From: Igor Savin Date: Thu, 19 May 2022 14:07:18 +0300 Subject: [PATCH 26/35] Fix docs --- docs/api/Dispatcher.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/Dispatcher.md b/docs/api/Dispatcher.md index 39c554ea80c..34f2bb8c715 100644 --- a/docs/api/Dispatcher.md +++ b/docs/api/Dispatcher.md @@ -194,7 +194,7 @@ Returns: `Boolean` - `false` if dispatcher is busy and further dispatch calls wo * **method** `string` * **body** `string | Buffer | Uint8Array | stream.Readable | Iterable | AsyncIterable | null` (optional) - Default: `null` * **headers** `UndiciHeaders | string[]` (optional) - Default: `null`. -* **query** `Record | null` (optional) - Default: `null` - Query string params to be embedded in the request URL. * **idempotent** `boolean` (optional) - Default: `true` if `method` is `'HEAD'` or `'GET'` - Whether the requests can be safely retried or not. If `false` the request won't be sent until all preceding requests in the pipeline has completed. * **blocking** `boolean` (optional) - Default: `false` - Whether the response is expected to take a long time and would end up blocking the pipeline. When this is set to `true` further pipelining will be avoided on the same connection until headers have been received. * **upgrade** `string | null` (optional) - Default: `null` - Upgrade the request. Should be used to specify the kind of upgrade i.e. `'Websocket'`. From 54c97e5b7fe4a6431f9ac659b35eb17f703fd2ab Mon Sep 17 00:00:00 2001 From: Igor Savin Date: Thu, 19 May 2022 14:23:02 +0300 Subject: [PATCH 27/35] Reduce amount of unescaped characters --- lib/core/util.js | 5 ++--- test/client.js | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/core/util.js b/lib/core/util.js index d0b0e3627aa..54313eb4175 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -34,10 +34,9 @@ function isObject (val) { return val !== null && typeof val === 'object' } -// based on https://github.com/axios/axios/blob/63e559fa609c40a0a460ae5d5a18c3470ffc6c9e/lib/helpers/buildURL.js (MIT license) -// this escapes all non-browser-url friendly characters, but restores the ones that are supported by browsers +// this escapes all non-uri friendly characters, but restores : which is commonly used for date formatting function encode (val) { - return encodeURIComponent(val).replace(/%3A/gi, ':').replace(/%24/g, '$').replace(/%2C/gi, ',').replace(/%20/g, '+').replace(/%5B/gi, '[').replace(/%5D/gi, ']') + return encodeURIComponent(val).replace(/%3A/gi, ':') } // based on https://github.com/axios/axios/blob/63e559fa609c40a0a460ae5d5a18c3470ffc6c9e/lib/helpers/buildURL.js (MIT license) diff --git a/test/client.js b/test/client.js index dbf669ba234..907d882ae11 100644 --- a/test/client.js +++ b/test/client.js @@ -91,7 +91,7 @@ test('basic get with query params', (t) => { bool: 'true', foo: '1', bar: 'bar', - ':$,%2B[]%40%5E*()-': ':$,%2B[]%40%5E*()-', + ':%24%2C%2B%5B%5D%40%5E*()-': ':%24%2C%2B%5B%5D%40%5E*()-', multi: ['1', '2'] }) From 6f3802ecab7495e2d54ca7f43e61fb63029dfbfd Mon Sep 17 00:00:00 2001 From: Igor Savin Date: Thu, 19 May 2022 14:23:38 +0300 Subject: [PATCH 28/35] Simplify branch Co-authored-by: Robert Nagy --- lib/core/request.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/core/request.js b/lib/core/request.js index 47e53706df2..89a1f3ef442 100644 --- a/lib/core/request.js +++ b/lib/core/request.js @@ -98,11 +98,7 @@ class Request { this.upgrade = upgrade || null - if (query) { - this.path = util.buildURL(path, query) - } else { - this.path = path - } + this.path = query ? util.buildURL(path, query) : path this.origin = origin From c3f71f94ea2e07818a748110c9a033efba1fa847 Mon Sep 17 00:00:00 2001 From: Igor Savin Date: Thu, 19 May 2022 14:29:24 +0300 Subject: [PATCH 29/35] Fix symbol Co-authored-by: Robert Nagy --- lib/core/util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/core/util.js b/lib/core/util.js index 54313eb4175..2ef29b5a9e1 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -73,7 +73,7 @@ function buildURL (url, queryParams) { url = url.slice(0, hashmarkIndex) } - url += '&' + serializedParams + url += '?' + serializedParams } return url From 63e7f074873f5c3897eaab2aeee783bd427da78a Mon Sep 17 00:00:00 2001 From: Igor Savin Date: Thu, 19 May 2022 14:29:52 +0300 Subject: [PATCH 30/35] Explicitly do not support # Co-authored-by: Robert Nagy --- lib/core/util.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/core/util.js b/lib/core/util.js index 2ef29b5a9e1..f7b5c49a649 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -41,8 +41,8 @@ function encode (val) { // based on https://github.com/axios/axios/blob/63e559fa609c40a0a460ae5d5a18c3470ffc6c9e/lib/helpers/buildURL.js (MIT license) function buildURL (url, queryParams) { - if (url.includes('?')) { - throw new Error('Query params cannot be passed when url contains "?" already.') + if (url.includes('?') || url.includes('#')) { + throw new Error('Query params cannot be passed when url already contains "?" or "#".') } const parts = [] From 5dba6b7fcfbb9bb6f02ca569baa4f2bba053f68f Mon Sep 17 00:00:00 2001 From: Igor Savin Date: Thu, 19 May 2022 14:37:45 +0300 Subject: [PATCH 31/35] Adjust based on latest comments --- lib/core/util.js | 2 +- test/client.js | 51 ++++++++++++++++++++++++++++++++---------------- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/lib/core/util.js b/lib/core/util.js index f7b5c49a649..e472283b318 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -57,7 +57,7 @@ function buildURL (url, queryParams) { for (let v of val) { if (isDate(v)) { - v = v.toISOString() + throw new Error('Passing date as a query param is not supported, please serialize to string up-front') } else if (isObject(v)) { throw new Error('Passing object as a query param is not supported, please serialize to string up-front') } diff --git a/test/client.js b/test/client.js index 907d882ae11..cb2ed5864b8 100644 --- a/test/client.js +++ b/test/client.js @@ -83,11 +83,9 @@ test('basic get', (t) => { test('basic get with query params', (t) => { t.plan(4) - const date = new Date(1995, 11, 17) const server = createServer((req, res) => { const searchParamsObject = buildParams(req.url) t.strictSame(searchParamsObject, { - date: date.toISOString(), bool: 'true', foo: '1', bar: 'bar', @@ -101,7 +99,6 @@ test('basic get with query params', (t) => { t.teardown(server.close.bind(server)) const query = { - date, bool: true, foo: 1, bar: 'bar', @@ -162,19 +159,42 @@ test('basic get with query params with object throws an error', (t) => { }) }) -test('basic get with query params ignores hashmark', (t) => { - t.plan(4) +test('basic get with query params with date throws an error', (t) => { + t.plan(1) + const date = new Date() const server = createServer((req, res) => { - const searchParamsObject = buildParams(req.url) - t.strictSame(searchParamsObject, { - foo: '1', - bar: 'bar', - multi: ['1', '2'] + t.fail() + }) + t.teardown(server.close.bind(server)) + + const query = { + dateObj: date + } + + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`, { + keepAliveTimeout: 300e3 }) + t.teardown(client.close.bind(client)) - res.statusCode = 200 - res.end('hello') + const signal = new EE() + client.request({ + signal, + path: '/', + method: 'GET', + query + }, (err, data) => { + t.equal(err.message, 'Passing date as a query param is not supported, please serialize to string up-front') + }) + }) +}) + +test('basic get with query params fails if url includes hashmark', (t) => { + t.plan(1) + + const server = createServer((req, res) => { + t.fail() }) t.teardown(server.close.bind(server)) @@ -197,11 +217,8 @@ test('basic get with query params ignores hashmark', (t) => { method: 'GET', query }, (err, data) => { - t.error(err) - const { statusCode } = data - t.equal(statusCode, 200) + t.equal(err.message, 'Query params cannot be passed when url already contains "?" or "#".') }) - t.equal(signal.listenerCount('abort'), 1) }) }) @@ -265,7 +282,7 @@ test('basic get with query params partially in path', (t) => { method: 'GET', query }, (err, data) => { - t.equal(err.message, 'Query params cannot be passed when url contains "?" already.') + t.equal(err.message, 'Query params cannot be passed when url already contains "?" or "#".') }) }) }) From 8d2c7d4d75ced645146335c2ba579d989565f0e1 Mon Sep 17 00:00:00 2001 From: Igor Savin Date: Thu, 19 May 2022 14:40:48 +0300 Subject: [PATCH 32/35] Fix linting --- lib/core/util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/core/util.js b/lib/core/util.js index e472283b318..fe66489ae0f 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -55,7 +55,7 @@ function buildURL (url, queryParams) { val = [val] } - for (let v of val) { + for (const v of val) { if (isDate(v)) { throw new Error('Passing date as a query param is not supported, please serialize to string up-front') } else if (isObject(v)) { From ed81bbbb862ed3ff3fc84620cc84b04b47c305e2 Mon Sep 17 00:00:00 2001 From: Igor Savin Date: Thu, 19 May 2022 16:46:36 +0300 Subject: [PATCH 33/35] Address code review comments --- lib/core/util.js | 16 ++++++---------- test/client.js | 34 +++++++++++++++++++++++++++++++--- 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/lib/core/util.js b/lib/core/util.js index fe66489ae0f..d9e80964479 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -34,9 +34,9 @@ function isObject (val) { return val !== null && typeof val === 'object' } -// this escapes all non-uri friendly characters, but restores : which is commonly used for date formatting +// this escapes all non-uri friendly characters function encode (val) { - return encodeURIComponent(val).replace(/%3A/gi, ':') + return encodeURIComponent(val) } // based on https://github.com/axios/axios/blob/63e559fa609c40a0a460ae5d5a18c3470ffc6c9e/lib/helpers/buildURL.js (MIT license) @@ -44,6 +44,9 @@ function buildURL (url, queryParams) { if (url.includes('?') || url.includes('#')) { throw new Error('Query params cannot be passed when url already contains "?" or "#".') } + if (!isObject(queryParams)) { + throw new Error('Query params must be an object') + } const parts = [] for (let [key, val] of Object.entries(queryParams)) { @@ -56,9 +59,7 @@ function buildURL (url, queryParams) { } for (const v of val) { - if (isDate(v)) { - throw new Error('Passing date as a query param is not supported, please serialize to string up-front') - } else if (isObject(v)) { + if (isObject(v)) { throw new Error('Passing object as a query param is not supported, please serialize to string up-front') } parts.push(encode(key) + '=' + encode(v)) @@ -68,11 +69,6 @@ function buildURL (url, queryParams) { const serializedParams = parts.join('&') if (serializedParams) { - const hashmarkIndex = url.indexOf('#') - if (hashmarkIndex !== -1) { - url = url.slice(0, hashmarkIndex) - } - url += '?' + serializedParams } diff --git a/test/client.js b/test/client.js index cb2ed5864b8..61b4bf97f7d 100644 --- a/test/client.js +++ b/test/client.js @@ -89,7 +89,7 @@ test('basic get with query params', (t) => { bool: 'true', foo: '1', bar: 'bar', - ':%24%2C%2B%5B%5D%40%5E*()-': ':%24%2C%2B%5B%5D%40%5E*()-', + '%60~%3A%24%2C%2B%5B%5D%40%5E*()-': '%60~%3A%24%2C%2B%5B%5D%40%5E*()-', multi: ['1', '2'] }) @@ -104,7 +104,7 @@ test('basic get with query params', (t) => { bar: 'bar', nullVal: null, undefinedVal: undefined, - ':$,+[]@^*()-': ':$,+[]@^*()-', + '`~:$,+[]@^*()-': '`~:$,+[]@^*()-', multi: [1, 2] } @@ -159,6 +159,34 @@ test('basic get with query params with object throws an error', (t) => { }) }) +test('basic get with non-object query params throws an error', (t) => { + t.plan(1) + + const server = createServer((req, res) => { + t.fail() + }) + t.teardown(server.close.bind(server)) + + const query = '{ obj: { id: 1 } }' + + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`, { + keepAliveTimeout: 300e3 + }) + t.teardown(client.close.bind(client)) + + const signal = new EE() + client.request({ + signal, + path: '/', + method: 'GET', + query + }, (err, data) => { + t.equal(err.message, 'Query params must be an object') + }) + }) +}) + test('basic get with query params with date throws an error', (t) => { t.plan(1) @@ -185,7 +213,7 @@ test('basic get with query params with date throws an error', (t) => { method: 'GET', query }, (err, data) => { - t.equal(err.message, 'Passing date as a query param is not supported, please serialize to string up-front') + t.equal(err.message, 'Passing object as a query param is not supported, please serialize to string up-front') }) }) }) From a454d002a06d1c164547692a38d44c595ff96220 Mon Sep 17 00:00:00 2001 From: Igor Savin Date: Thu, 19 May 2022 16:48:42 +0300 Subject: [PATCH 34/35] Fix linting --- lib/core/util.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/core/util.js b/lib/core/util.js index d9e80964479..635ef2e15f2 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -26,10 +26,6 @@ function isBlobLike (object) { ) } -function isDate (val) { - return toString.call(val) === '[object Date]' -} - function isObject (val) { return val !== null && typeof val === 'object' } From 79610ddb4d73e54d3cdf23a89973d8082b604a1d Mon Sep 17 00:00:00 2001 From: Igor Savin Date: Thu, 19 May 2022 16:57:20 +0300 Subject: [PATCH 35/35] Clarify documentation --- docs/api/Dispatcher.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/Dispatcher.md b/docs/api/Dispatcher.md index 34f2bb8c715..68ed316a6c6 100644 --- a/docs/api/Dispatcher.md +++ b/docs/api/Dispatcher.md @@ -194,7 +194,7 @@ Returns: `Boolean` - `false` if dispatcher is busy and further dispatch calls wo * **method** `string` * **body** `string | Buffer | Uint8Array | stream.Readable | Iterable | AsyncIterable | null` (optional) - Default: `null` * **headers** `UndiciHeaders | string[]` (optional) - Default: `null`. -* **query** `Record | null` (optional) - Default: `null` - Query string params to be embedded in the request URL. +* **query** `Record | null` (optional) - Default: `null` - Query string params to be embedded in the request URL. Note that both keys and values of query are encoded using `encodeURIComponent`. If for some reason you need to send them unencoded, embed query params into path directly instead. * **idempotent** `boolean` (optional) - Default: `true` if `method` is `'HEAD'` or `'GET'` - Whether the requests can be safely retried or not. If `false` the request won't be sent until all preceding requests in the pipeline has completed. * **blocking** `boolean` (optional) - Default: `false` - Whether the response is expected to take a long time and would end up blocking the pipeline. When this is set to `true` further pipelining will be avoided on the same connection until headers have been received. * **upgrade** `string | null` (optional) - Default: `null` - Upgrade the request. Should be used to specify the kind of upgrade i.e. `'Websocket'`.