Skip to content

Commit

Permalink
fix: assume blocking unless HEAD (#3771)
Browse files Browse the repository at this point in the history
* disable failing test

* fix: assume blocking unless HEAD

This is a safer default.

* fixup

* fixup: benchmark
  • Loading branch information
ronag authored Nov 6, 2024
1 parent 3596e82 commit ae91a25
Show file tree
Hide file tree
Showing 14 changed files with 37 additions and 20 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions benchmarks/benchmark.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ const superagentAgent = new http.Agent({
const undiciOptions = {
path: '/',
method: 'GET',
blocking: false,
reset: false,
headersTimeout,
bodyTimeout
}
Expand Down
4 changes: 2 additions & 2 deletions docs/docs/api/Dispatcher.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ Returns: `Boolean` - `false` if dispatcher is busy and further dispatch calls wo
* **headers** `UndiciHeaders | string[]` (optional) - Default: `null`.
* **query** `Record<string, any> | 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.
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion lib/core/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion test/client-connect.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions test/client-idempotent-body.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ test('idempotent retry', async (t) => {
path: '/',
method: 'PUT',
idempotent: true,
blocking: false,
body
}, () => {
throw _err
Expand Down
5 changes: 3 additions & 2 deletions test/client-pipelining.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion test/client-upgrade.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
Expand Down
6 changes: 4 additions & 2 deletions test/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -1442,7 +1442,8 @@ test('increase pipelining', async (t) => {

client.request({
path: '/',
method: 'GET'
method: 'GET',
blocking: false
}, () => {
if (!client.destroyed) {
t.fail()
Expand All @@ -1451,7 +1452,8 @@ test('increase pipelining', async (t) => {

client.request({
path: '/',
method: 'GET'
method: 'GET',
blocking: false
}, () => {
if (!client.destroyed) {
t.fail()
Expand Down
6 changes: 4 additions & 2 deletions test/node-test/client-abort.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -151,7 +152,8 @@ test('abort pipelined', async (t) => {

client.dispatch({
method: 'GET',
path: '/'
path: '/',
blocking: false
}, {
onConnect (abort) {
abort()
Expand Down
3 changes: 2 additions & 1 deletion test/node-test/client-connect.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
Expand Down
15 changes: 10 additions & 5 deletions test/pipeline-pipelining.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,17 @@ 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)
t.deepStrictEqual(client[kPending], 1)

client.pipeline({
method: 'GET',
path: '/'
path: '/',
blocking: false
}, ({ body }) => body).end().resume()
t.equal(client[kBusy], true)
t.deepStrictEqual(client[kRunning], 0)
Expand Down Expand Up @@ -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)
Expand All @@ -85,15 +88,17 @@ 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)
t.deepStrictEqual(client[kPending], 2)

client.pipeline({
method: 'GET',
path: '/'
path: '/',
blocking: false
}, ({ body }) => body).end().resume()
t.equal(client[kBusy], true)
t.deepStrictEqual(client[kRunning], 0)
Expand Down
2 changes: 1 addition & 1 deletion test/promises.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
2 changes: 1 addition & 1 deletion types/dispatcher.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ declare namespace Dispatcher {
query?: Record<string, any>;
/** 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;
Expand Down

0 comments on commit ae91a25

Please sign in to comment.