From ae91a25e4771778cac41f2fb2b0c0f1f60024c44 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Wed, 6 Nov 2024 16:24:25 +0000 Subject: [PATCH] fix: assume blocking unless HEAD (#3771) * disable failing test * fix: assume blocking unless HEAD This is a safer default. * fixup * fixup: benchmark --- README.md | 3 ++- benchmarks/benchmark.js | 2 ++ docs/docs/api/Dispatcher.md | 4 ++-- lib/core/request.js | 2 +- test/client-connect.js | 3 ++- test/client-idempotent-body.js | 1 + test/client-pipelining.js | 5 +++-- test/client-upgrade.js | 3 ++- test/client.js | 6 ++++-- test/node-test/client-abort.js | 6 ++++-- test/node-test/client-connect.js | 3 ++- test/pipeline-pipelining.js | 15 ++++++++++----- test/promises.js | 2 +- types/dispatcher.d.ts | 2 +- 14 files changed, 37 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 40e999b3fcb..ec30c4e2420 100644 --- a/README.md +++ b/README.md @@ -402,7 +402,8 @@ Refs: https://tools.ietf.org/html/rfc7231#section-5.1.1 ### Pipelining Undici will only use pipelining if configured with a `pipelining` factor -greater than `1`. +greater than `1`. Also it is important to pass `blocking: false` to the +request options to properly pipeline requests. Undici always assumes that connections are persistent and will immediately pipeline requests, without checking whether the connection is persistent. diff --git a/benchmarks/benchmark.js b/benchmarks/benchmark.js index 7e922015225..5c8f49ea5c8 100644 --- a/benchmarks/benchmark.js +++ b/benchmarks/benchmark.js @@ -89,6 +89,8 @@ const superagentAgent = new http.Agent({ const undiciOptions = { path: '/', method: 'GET', + blocking: false, + reset: false, headersTimeout, bodyTimeout } diff --git a/docs/docs/api/Dispatcher.md b/docs/docs/api/Dispatcher.md index d531efec07c..e77b7771114 100644 --- a/docs/docs/api/Dispatcher.md +++ b/docs/docs/api/Dispatcher.md @@ -197,7 +197,7 @@ Returns: `Boolean` - `false` if dispatcher is busy and further dispatch calls wo * **headers** `UndiciHeaders | string[]` (optional) - Default: `null`. * **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. +* **blocking** `boolean` (optional) - Default: `method !== 'HEAD'` - 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'`. * **bodyTimeout** `number | null` (optional) - The timeout after which a request will time out, in milliseconds. Monitors time between receiving body data. Use `0` to disable it entirely. Defaults to 300 seconds. * **headersTimeout** `number | null` (optional) - The amount of time, in milliseconds, the parser will wait to receive the complete HTTP headers while not sending the request. Defaults to 300 seconds. @@ -1021,7 +1021,7 @@ The `dns` interceptor enables you to cache DNS lookups for a given duration, per - It can be either `'4` or `6`. - It will only take effect if `dualStack` is `false`. - `lookup: (hostname: string, options: LookupOptions, callback: (err: NodeJS.ErrnoException | null, addresses: DNSInterceptorRecord[]) => void) => void` - Custom lookup function. Default: `dns.lookup`. - - For more info see [dns.lookup](https://nodejs.org/api/dns.html#dns_dns_lookup_hostname_options_callback). + - For more info see [dns.lookup](https://nodejs.org/api/dns.html#dns_dns_lookup_hostname_options_callback). - `pick: (origin: URL, records: DNSInterceptorRecords, affinity: 4 | 6) => DNSInterceptorRecord` - Custom pick function. Default: `RoundRobin`. - The function should return a single record from the records array. - By default a simplified version of Round Robin is used. diff --git a/lib/core/request.js b/lib/core/request.js index c9bf388c316..6cd9b2f8307 100644 --- a/lib/core/request.js +++ b/lib/core/request.js @@ -143,7 +143,7 @@ class Request { ? method === 'HEAD' || method === 'GET' : idempotent - this.blocking = blocking == null ? false : blocking + this.blocking = blocking ?? this.method !== 'HEAD' this.reset = reset == null ? null : reset diff --git a/test/client-connect.js b/test/client-connect.js index e002a42c571..72e575035ef 100644 --- a/test/client-connect.js +++ b/test/client-connect.js @@ -33,7 +33,8 @@ test('connect aborted after connect', async (t) => { client.connect({ path: '/', signal, - opaque: 'asd' + opaque: 'asd', + blocking: false }, (err, { opaque }) => { t.strictEqual(opaque, 'asd') t.ok(err instanceof errors.RequestAbortedError) diff --git a/test/client-idempotent-body.js b/test/client-idempotent-body.js index 07004cb8296..6f40efdd9e0 100644 --- a/test/client-idempotent-body.js +++ b/test/client-idempotent-body.js @@ -33,6 +33,7 @@ test('idempotent retry', async (t) => { path: '/', method: 'PUT', idempotent: true, + blocking: false, body }, () => { throw _err diff --git a/test/client-pipelining.js b/test/client-pipelining.js index 475b826c62d..b62357f03e0 100644 --- a/test/client-pipelining.js +++ b/test/client-pipelining.js @@ -61,7 +61,7 @@ test('20 times GET with pipelining 10', async (t) => { }) function makeRequestAndExpectUrl (client, i, t, cb) { - return client.request({ path: '/' + i, method: 'GET' }, (err, { statusCode, headers, body }) => { + return client.request({ path: '/' + i, method: 'GET', blocking: false }, (err, { statusCode, headers, body }) => { cb() t.ifError(err) t.strictEqual(statusCode, 200) @@ -587,7 +587,8 @@ test('pipelining empty pipeline before reset', async (t) => { client.request({ path: '/', - method: 'GET' + method: 'GET', + blocking: false }, (err, data) => { t.ifError(err) data.body diff --git a/test/client-upgrade.js b/test/client-upgrade.js index 5cf5e553ba7..7d76f8ee148 100644 --- a/test/client-upgrade.js +++ b/test/client-upgrade.js @@ -256,7 +256,8 @@ test('upgrade wait for empty pipeline', async (t) => { client.request({ path: '/', - method: 'GET' + method: 'GET', + blocking: false }, (err) => { t.ifError(err) }) diff --git a/test/client.js b/test/client.js index 62f2c10dc7a..680ff66be10 100644 --- a/test/client.js +++ b/test/client.js @@ -1442,7 +1442,8 @@ test('increase pipelining', async (t) => { client.request({ path: '/', - method: 'GET' + method: 'GET', + blocking: false }, () => { if (!client.destroyed) { t.fail() @@ -1451,7 +1452,8 @@ test('increase pipelining', async (t) => { client.request({ path: '/', - method: 'GET' + method: 'GET', + blocking: false }, () => { if (!client.destroyed) { t.fail() diff --git a/test/node-test/client-abort.js b/test/node-test/client-abort.js index a5337f16ae2..52395f0dbfb 100644 --- a/test/node-test/client-abort.js +++ b/test/node-test/client-abort.js @@ -126,7 +126,8 @@ test('abort pipelined', async (t) => { let counter = 0 client.dispatch({ method: 'GET', - path: '/' + path: '/', + blocking: false }, { onConnect (abort) { // This request will be retried @@ -151,7 +152,8 @@ test('abort pipelined', async (t) => { client.dispatch({ method: 'GET', - path: '/' + path: '/', + blocking: false }, { onConnect (abort) { abort() diff --git a/test/node-test/client-connect.js b/test/node-test/client-connect.js index 0bf65488d81..28271d348cf 100644 --- a/test/node-test/client-connect.js +++ b/test/node-test/client-connect.js @@ -142,7 +142,8 @@ test('connect wait for empty pipeline', async (t) => { client.request({ path: '/', - method: 'GET' + method: 'GET', + blocking: false }, (err) => { p.ifError(err) }) diff --git a/test/pipeline-pipelining.js b/test/pipeline-pipelining.js index b244925786a..27ea1bb21ed 100644 --- a/test/pipeline-pipelining.js +++ b/test/pipeline-pipelining.js @@ -26,7 +26,8 @@ test('pipeline pipelining', async (t) => { t.equal(client[kRunning], 0) client.pipeline({ method: 'GET', - path: '/' + path: '/', + blocking: false }, ({ body }) => body).end().resume() t.equal(client[kBusy], true) t.deepStrictEqual(client[kRunning], 0) @@ -34,7 +35,8 @@ test('pipeline pipelining', async (t) => { client.pipeline({ method: 'GET', - path: '/' + path: '/', + blocking: false }, ({ body }) => body).end().resume() t.equal(client[kBusy], true) t.deepStrictEqual(client[kRunning], 0) @@ -74,7 +76,8 @@ test('pipeline pipelining retry', async (t) => { client[kConnect](() => { client.pipeline({ method: 'GET', - path: '/' + path: '/', + blocking: false }, ({ body }) => body).end().resume() .on('error', (err) => { t.ok(err) @@ -85,7 +88,8 @@ test('pipeline pipelining retry', async (t) => { client.pipeline({ method: 'GET', - path: '/' + path: '/', + blocking: false }, ({ body }) => body).end().resume() t.equal(client[kBusy], true) t.deepStrictEqual(client[kRunning], 0) @@ -93,7 +97,8 @@ test('pipeline pipelining retry', async (t) => { client.pipeline({ method: 'GET', - path: '/' + path: '/', + blocking: false }, ({ body }) => body).end().resume() t.equal(client[kBusy], true) t.deepStrictEqual(client[kRunning], 0) diff --git a/test/promises.js b/test/promises.js index 5d47adc3640..1eddfc216f6 100644 --- a/test/promises.js +++ b/test/promises.js @@ -245,7 +245,7 @@ test('20 times GET with pipelining 10, async await support', async (t) => { async function makeRequestAndExpectUrl (client, i, t) { try { - const { statusCode, body } = await client.request({ path: '/' + i, method: 'GET' }) + const { statusCode, body } = await client.request({ path: '/' + i, method: 'GET', blocking: false }) t.strictEqual(statusCode, 200) const bufs = [] body.on('data', (buf) => { diff --git a/types/dispatcher.d.ts b/types/dispatcher.d.ts index 7a9810a2c47..8b9e633d730 100644 --- a/types/dispatcher.d.ts +++ b/types/dispatcher.d.ts @@ -108,7 +108,7 @@ declare namespace Dispatcher { 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; - /** 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. */ + /** 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. Defaults to `method !== 'HEAD'`. */ blocking?: boolean; /** Upgrade the request. Should be used to specify the kind of upgrade i.e. `'Websocket'`. Default: `method === 'CONNECT' || null`. */ upgrade?: boolean | string | null;