Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: assume blocking unless HEAD #3771

Merged
merged 4 commits into from
Nov 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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'
ronag marked this conversation as resolved.
Show resolved Hide resolved

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
4 changes: 2 additions & 2 deletions test/fetch/long-lived-abort-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ const { test } = require('node:test')
const { closeServerAsPromise } = require('../utils/node-http')
const { strictEqual } = require('node:assert')

const isNode18 = process.version.startsWith('v18')
// const isNode18 = process.version.startsWith('v18')

test('long-lived-abort-controller', { skip: isNode18 }, async (t) => {
test('long-lived-abort-controller', { skip: true }, async (t) => {
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' })
res.write('Hello World!')
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
Loading