From 83efd5095f510925e2504c954ba0b9ae7b19e841 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Sun, 22 Sep 2024 19:28:01 +0200 Subject: [PATCH] remove some methods Remove methods and features that are not commonly used and can be re-implement in user land using the dispatch API. Simplifies undici-core so we can focus on the most important API's and features. --- README.md | 70 -- benchmarks/benchmark-http2.js | 30 - benchmarks/benchmark-https.js | 30 - benchmarks/benchmark.js | 30 - benchmarks/post-benchmark.js | 41 +- docs/docs/api/Agent.md | 18 - docs/docs/api/BalancedPool.md | 16 - docs/docs/api/Client.md | 18 +- docs/docs/api/Dispatcher.md | 343 +------ docs/docs/api/EnvHttpProxyAgent.md | 16 - docs/docs/api/Pool.md | 16 - docs/docs/api/api-lifecycle.md | 4 +- index.js | 4 - lib/api/api-connect.js | 110 -- lib/api/api-pipeline.js | 252 ----- lib/api/api-request.js | 19 +- lib/api/api-stream.js | 209 ---- lib/api/api-upgrade.js | 110 -- lib/api/index.js | 4 - test/client-connect.js | 44 - test/client-pipeline.js | 1102 --------------------- test/client-request.js | 135 --- test/client-stream.js | 834 ---------------- test/client-upgrade.js | 473 --------- test/client.js | 36 +- test/http2.js | 202 +--- test/issue-2349.js | 44 - test/node-test/agent.js | 47 - test/node-test/async_hooks.js | 29 - test/node-test/client-connect.js | 293 ------ test/node-test/client-errors.js | 17 +- test/pipeline-pipelining.js | 113 --- test/pool.js | 379 +------ test/redirect-pipeline.js | 54 - test/redirect-stream.js | 423 -------- test/request-timeout.js | 192 ---- test/types/agent.test-d.ts | 71 -- test/types/api.test-d.ts | 35 +- test/types/balanced-pool.test-d.ts | 57 -- test/types/client.test-d.ts | 63 -- test/types/dispatcher.test-d.ts | 103 -- test/types/env-http-proxy-agent.test-d.ts | 71 -- test/types/pool.test-d.ts | 57 -- test/utils/esm-wrapper.mjs | 12 +- types/api.d.ts | 33 +- types/dispatcher.d.ts | 84 +- types/index.d.ts | 8 +- 47 files changed, 24 insertions(+), 6327 deletions(-) delete mode 100644 lib/api/api-connect.js delete mode 100644 lib/api/api-pipeline.js delete mode 100644 lib/api/api-stream.js delete mode 100644 lib/api/api-upgrade.js delete mode 100644 test/client-connect.js delete mode 100644 test/client-pipeline.js delete mode 100644 test/client-stream.js delete mode 100644 test/client-upgrade.js delete mode 100644 test/issue-2349.js delete mode 100644 test/node-test/client-connect.js delete mode 100644 test/pipeline-pipelining.js delete mode 100644 test/redirect-pipeline.js delete mode 100644 test/redirect-stream.js diff --git a/README.md b/README.md index 7d509997e3b..1f7f27a4bee 100644 --- a/README.md +++ b/README.md @@ -34,9 +34,7 @@ The benchmark is a simple getting data [example](https://github.com/nodejs/undic | got | 10 | 5969.67 req/sec | ± 2.64 % | + 61.15 % | | superagent | 10 | 9471.48 req/sec | ± 1.50 % | + 155.68 % | | http - keepalive | 25 | 10327.49 req/sec | ± 2.95 % | + 178.79 % | -| undici - pipeline | 10 | 15053.41 req/sec | ± 1.63 % | + 306.36 % | | undici - request | 10 | 19264.24 req/sec | ± 1.74 % | + 420.03 % | -| undici - stream | 15 | 20317.29 req/sec | ± 2.13 % | + 448.46 % | | undici - dispatch | 10 | 24883.28 req/sec | ± 1.54 % | + 571.72 % | The benchmark is a simple sending data [example](https://github.com/nodejs/undici/blob/main/benchmarks/post-benchmark.js) using a @@ -52,9 +50,7 @@ The benchmark is a simple sending data [example](https://github.com/nodejs/undic | axios | 10 | 3040.45 req/sec | ± 1.72 % | + 54.46 % | | superagent | 20 | 3358.29 req/sec | ± 2.51 % | + 70.61 % | | http - keepalive | 20 | 3477.94 req/sec | ± 2.51 % | + 76.69 % | -| undici - pipeline | 25 | 3812.61 req/sec | ± 2.80 % | + 93.69 % | | undici - request | 10 | 6067.00 req/sec | ± 0.94 % | + 208.22 % | -| undici - stream | 10 | 6391.61 req/sec | ± 1.98 % | + 224.71 % | | undici - dispatch | 10 | 6397.00 req/sec | ± 1.48 % | + 224.98 % | @@ -134,55 +130,6 @@ Calls `options.dispatcher.request(options)`. See [Dispatcher.request](./docs/docs/api/Dispatcher.md#dispatcherrequestoptions-callback) for more details, and [request examples](./examples/README.md) for examples. -### `undici.stream([url, options, ]factory): Promise` - -Arguments: - -* **url** `string | URL | UrlObject` -* **options** [`StreamOptions`](./docs/docs/api/Dispatcher.md#parameter-streamoptions) - * **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher) - * **method** `String` - Default: `PUT` if `options.body`, otherwise `GET` -* **factory** `Dispatcher.stream.factory` - -Returns a promise with the result of the `Dispatcher.stream` method. - -Calls `options.dispatcher.stream(options, factory)`. - -See [Dispatcher.stream](./docs/docs/api/Dispatcher.md#dispatcherstreamoptions-factory-callback) for more details. - -### `undici.pipeline([url, options, ]handler): Duplex` - -Arguments: - -* **url** `string | URL | UrlObject` -* **options** [`PipelineOptions`](./docs/docs/api/Dispatcher.md#parameter-pipelineoptions) - * **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher) - * **method** `String` - Default: `PUT` if `options.body`, otherwise `GET` -* **handler** `Dispatcher.pipeline.handler` - -Returns: `stream.Duplex` - -Calls `options.dispatch.pipeline(options, handler)`. - -See [Dispatcher.pipeline](./docs/docs/api/Dispatcher.md#dispatcherpipelineoptions-handler) for more details. - -### `undici.connect([url, options]): Promise` - -Starts two-way communications with the requested resource using [HTTP CONNECT](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/CONNECT). - -Arguments: - -* **url** `string | URL | UrlObject` -* **options** [`ConnectOptions`](./docs/docs/api/Dispatcher.md#parameter-connectoptions) - * **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher) -* **callback** `(err: Error | null, data: ConnectData | null) => void` (optional) - -Returns a promise with the result of the `Dispatcher.connect` method. - -Calls `options.dispatch.connect(options)`. - -See [Dispatcher.connect](./docs/docs/api/Dispatcher.md#dispatcherconnectoptions-callback) for more details. - ### `undici.fetch(input[, init]): Promise` Implements [fetch](https://fetch.spec.whatwg.org/#fetch-method). @@ -325,23 +272,6 @@ const headers = await fetch(url, { method: 'HEAD' }) The [Fetch Standard](https://fetch.spec.whatwg.org) requires implementations to exclude certain headers from requests and responses. In browser environments, some headers are forbidden so the user agent remains in full control over them. In Undici, these constraints are removed to give more control to the user. -### `undici.upgrade([url, options]): Promise` - -Upgrade to a different protocol. See [MDN - HTTP - Protocol upgrade mechanism](https://developer.mozilla.org/en-US/docs/Web/HTTP/Protocol_upgrade_mechanism) for more details. - -Arguments: - -* **url** `string | URL | UrlObject` -* **options** [`UpgradeOptions`](./docs/docs/api/Dispatcher.md#parameter-upgradeoptions) - * **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher) -* **callback** `(error: Error | null, data: UpgradeData) => void` (optional) - -Returns a promise with the result of the `Dispatcher.upgrade` method. - -Calls `options.dispatcher.upgrade(options)`. - -See [Dispatcher.upgrade](./docs/docs/api/Dispatcher.md#dispatcherupgradeoptions-callback) for more details. - ### `undici.setGlobalDispatcher(dispatcher)` * dispatcher `Dispatcher` diff --git a/benchmarks/benchmark-http2.js b/benchmarks/benchmark-http2.js index 46ac89f97bf..67b7a715f00 100644 --- a/benchmarks/benchmark-http2.js +++ b/benchmarks/benchmark-http2.js @@ -182,23 +182,6 @@ const experiments = { }) }) }, - 'undici - pipeline' () { - return makeParallelRequests(resolve => { - dispatcher - .pipeline(undiciOptions, data => { - return data.body - }) - .end() - .pipe( - new Writable({ - write (chunk, encoding, callback) { - callback() - } - }) - ) - .on('finish', resolve) - }) - }, 'undici - request' () { return makeParallelRequests(resolve => { try { @@ -223,19 +206,6 @@ const experiments = { } }) }, - 'undici - stream' () { - return makeParallelRequests(resolve => { - return dispatcher - .stream(undiciOptions, () => { - return new Writable({ - write (chunk, encoding, callback) { - callback() - } - }) - }) - .then(resolve) - }) - }, 'undici - dispatch' () { return makeParallelRequests(resolve => { dispatcher.dispatch(undiciOptions, new SimpleRequest(resolve)) diff --git a/benchmarks/benchmark-https.js b/benchmarks/benchmark-https.js index ca0d5981bf0..c5f0f48485a 100644 --- a/benchmarks/benchmark-https.js +++ b/benchmarks/benchmark-https.js @@ -200,23 +200,6 @@ const experiments = { }) }) }, - 'undici - pipeline' () { - return makeParallelRequests(resolve => { - dispatcher - .pipeline(undiciOptions, data => { - return data.body - }) - .end() - .pipe( - new Writable({ - write (chunk, encoding, callback) { - callback() - } - }) - ) - .on('finish', resolve) - }) - }, 'undici - request' () { return makeParallelRequests(resolve => { dispatcher.request(undiciOptions).then(({ body }) => { @@ -232,19 +215,6 @@ const experiments = { }) }) }, - 'undici - stream' () { - return makeParallelRequests(resolve => { - return dispatcher - .stream(undiciOptions, () => { - return new Writable({ - write (chunk, encoding, callback) { - callback() - } - }) - }) - .then(resolve) - }) - }, 'undici - dispatch' () { return makeParallelRequests(resolve => { dispatcher.dispatch(undiciOptions, new SimpleRequest(resolve)) diff --git a/benchmarks/benchmark.js b/benchmarks/benchmark.js index 7e922015225..441385102e0 100644 --- a/benchmarks/benchmark.js +++ b/benchmarks/benchmark.js @@ -167,23 +167,6 @@ const experiments = { }) }) }, - 'undici - pipeline' () { - return makeParallelRequests(resolve => { - dispatcher - .pipeline(undiciOptions, ({ body }) => { - return body - }) - .end() - .pipe( - new Writable({ - write (chunk, encoding, callback) { - callback() - } - }) - ) - .on('finish', resolve) - }) - }, 'undici - request' () { return makeParallelRequests(resolve => { dispatcher.request(undiciOptions).then(({ body }) => { @@ -199,19 +182,6 @@ const experiments = { }) }) }, - 'undici - stream' () { - return makeParallelRequests(resolve => { - return dispatcher - .stream(undiciOptions, () => { - return new Writable({ - write (chunk, encoding, callback) { - callback() - } - }) - }) - .then(resolve) - }) - }, 'undici - dispatch' () { return makeParallelRequests(resolve => { dispatcher.dispatch(undiciOptions, new SimpleRequest(resolve)) diff --git a/benchmarks/post-benchmark.js b/benchmarks/post-benchmark.js index 041c136dcdd..5d0387470a7 100644 --- a/benchmarks/post-benchmark.js +++ b/benchmarks/post-benchmark.js @@ -3,7 +3,7 @@ const http = require('node:http') const os = require('node:os') const path = require('node:path') -const { Writable, Readable, pipeline } = require('node:stream') +const { Writable } = require('node:stream') const { isMainThread } = require('node:worker_threads') const { Pool, Client, fetch, Agent, setGlobalDispatcher } = require('..') @@ -181,32 +181,6 @@ const experiments = { request.end(data) }) }, - 'undici - pipeline' () { - return makeParallelRequests(resolve => { - pipeline( - new Readable({ - read () { - this.push(data) - this.push(null) - } - }), - dispatcher.pipeline(undiciOptions, ({ body }) => { - return body - }), - new Writable({ - write (chunk, encoding, callback) { - callback() - } - }), - (err) => { - if (err != null) { - console.log(err) - } - resolve() - } - ) - }) - }, 'undici - request' () { return makeParallelRequests(resolve => { dispatcher.request(undiciOptions).then(({ body }) => { @@ -222,19 +196,6 @@ const experiments = { }) }) }, - 'undici - stream' () { - return makeParallelRequests(resolve => { - return dispatcher - .stream(undiciOptions, () => { - return new Writable({ - write (chunk, encoding, callback) { - callback() - } - }) - }) - .then(resolve) - }) - }, 'undici - dispatch' () { return makeParallelRequests(resolve => { dispatcher.dispatch(undiciOptions, new SimpleRequest(resolve)) diff --git a/docs/docs/api/Agent.md b/docs/docs/api/Agent.md index e3e3e26a93b..d11aecb7791 100644 --- a/docs/docs/api/Agent.md +++ b/docs/docs/api/Agent.md @@ -52,26 +52,8 @@ Extends: [`DispatchOptions`](Dispatcher.md#parameter-dispatchoptions) Implements [`Dispatcher.destroy([error, callback])`](Dispatcher.md#dispatcherdestroyerror-callback-promise). -### `Agent.connect(options[, callback])` - -See [`Dispatcher.connect(options[, callback])`](Dispatcher.md#dispatcherconnectoptions-callback). - ### `Agent.dispatch(options, handler)` Implements [`Dispatcher.dispatch(options, handler)`](Dispatcher.md#dispatcherdispatchoptions-handler). -### `Agent.pipeline(options, handler)` - -See [`Dispatcher.pipeline(options, handler)`](Dispatcher.md#dispatcherpipelineoptions-handler). - ### `Agent.request(options[, callback])` - -See [`Dispatcher.request(options [, callback])`](Dispatcher.md#dispatcherrequestoptions-callback). - -### `Agent.stream(options, factory[, callback])` - -See [`Dispatcher.stream(options, factory[, callback])`](Dispatcher.md#dispatcherstreamoptions-factory-callback). - -### `Agent.upgrade(options[, callback])` - -See [`Dispatcher.upgrade(options[, callback])`](Dispatcher.md#dispatcherupgradeoptions-callback). diff --git a/docs/docs/api/BalancedPool.md b/docs/docs/api/BalancedPool.md index 183ef523185..69aeaeab2a2 100644 --- a/docs/docs/api/BalancedPool.md +++ b/docs/docs/api/BalancedPool.md @@ -60,30 +60,14 @@ Implements [`Dispatcher.close([callback])`](Dispatcher.md#dispatcherclosecallbac Implements [`Dispatcher.destroy([error, callback])`](Dispatcher.md#dispatcherdestroyerror-callback-promise). -### `BalancedPool.connect(options[, callback])` - -See [`Dispatcher.connect(options[, callback])`](Dispatcher.md#dispatcherconnectoptions-callback). - ### `BalancedPool.dispatch(options, handlers)` Implements [`Dispatcher.dispatch(options, handlers)`](Dispatcher.md#dispatcherdispatchoptions-handler). -### `BalancedPool.pipeline(options, handler)` - -See [`Dispatcher.pipeline(options, handler)`](Dispatcher.md#dispatcherpipelineoptions-handler). - ### `BalancedPool.request(options[, callback])` See [`Dispatcher.request(options [, callback])`](Dispatcher.md#dispatcherrequestoptions-callback). -### `BalancedPool.stream(options, factory[, callback])` - -See [`Dispatcher.stream(options, factory[, callback])`](Dispatcher.md#dispatcherstreamoptions-factory-callback). - -### `BalancedPool.upgrade(options[, callback])` - -See [`Dispatcher.upgrade(options[, callback])`](Dispatcher.md#dispatcherupgradeoptions-callback). - ## Instance Events ### Event: `'connect'` diff --git a/docs/docs/api/Client.md b/docs/docs/api/Client.md index 0be99625d2e..bcd9fda8322 100644 --- a/docs/docs/api/Client.md +++ b/docs/docs/api/Client.md @@ -48,7 +48,7 @@ Furthermore, the following options can be passed: ### Example - Basic Client instantiation -This will instantiate the undici Client, but it will not connect to the origin until something is queued. Consider using `client.connect` to prematurely connect to the origin, or just call `client.request`. +This will instantiate the undici Client, but it will not connect to the origin until something is queued. ```js 'use strict' @@ -94,30 +94,14 @@ Implements [`Dispatcher.destroy([error, callback])`](Dispatcher.md#dispatcherdes Waits until socket is closed before invoking the callback (or returning a promise if no callback is provided). -### `Client.connect(options[, callback])` - -See [`Dispatcher.connect(options[, callback])`](Dispatcher.md#dispatcherconnectoptions-callback). - ### `Client.dispatch(options, handlers)` Implements [`Dispatcher.dispatch(options, handlers)`](Dispatcher.md#dispatcherdispatchoptions-handler). -### `Client.pipeline(options, handler)` - -See [`Dispatcher.pipeline(options, handler)`](Dispatcher.md#dispatcherpipelineoptions-handler). - ### `Client.request(options[, callback])` See [`Dispatcher.request(options [, callback])`](Dispatcher.md#dispatcherrequestoptions-callback). -### `Client.stream(options, factory[, callback])` - -See [`Dispatcher.stream(options, factory[, callback])`](Dispatcher.md#dispatcherstreamoptions-factory-callback). - -### `Client.upgrade(options[, callback])` - -See [`Dispatcher.upgrade(options[, callback])`](Dispatcher.md#dispatcherupgradeoptions-callback). - ## Instance Properties ### `Client.closed` diff --git a/docs/docs/api/Dispatcher.md b/docs/docs/api/Dispatcher.md index 933f4a730f8..d0be593270f 100644 --- a/docs/docs/api/Dispatcher.md +++ b/docs/docs/api/Dispatcher.md @@ -53,76 +53,6 @@ console.log('Client closed') server.close() ``` -### `Dispatcher.connect(options[, callback])` - -Starts two-way communications with the requested resource using [HTTP CONNECT](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/CONNECT). - -Arguments: - -* **options** `ConnectOptions` -* **callback** `(err: Error | null, data: ConnectData | null) => void` (optional) - -Returns: `void | Promise` - Only returns a `Promise` if no `callback` argument was passed - -#### Parameter: `ConnectOptions` - -* **path** `string` -* **headers** `UndiciHeaders` (optional) - Default: `null` -* **signal** `AbortSignal | events.EventEmitter | null` (optional) - Default: `null` -* **opaque** `unknown` (optional) - This argument parameter is passed through to `ConnectData` - -#### Parameter: `ConnectData` - -* **statusCode** `number` -* **headers** `Record` -* **socket** `stream.Duplex` -* **opaque** `unknown` - -#### Example - Connect request with echo - -```js -import { createServer } from 'http' -import { Client } from 'undici' -import { once } from 'events' - -const server = createServer((request, response) => { - throw Error('should never get here') -}).listen() - -server.on('connect', (req, socket, head) => { - socket.write('HTTP/1.1 200 Connection established\r\n\r\n') - - let data = head.toString() - socket.on('data', (buf) => { - data += buf.toString() - }) - - socket.on('end', () => { - socket.end(data) - }) -}) - -await once(server, 'listening') - -const client = new Client(`http://localhost:${server.address().port}`) - -try { - const { socket } = await client.connect({ - path: '/' - }) - const wanted = 'Body' - let data = '' - socket.on('data', d => { data += d }) - socket.on('end', () => { - console.log(`Data received: ${data.toString()} | Data wanted: ${wanted}`) - client.close() - server.close() - }) - socket.write(wanted) - socket.end() -} catch (error) { } -``` - ### `Dispatcher.destroy([error, callback]): Promise` Destroy the dispatcher abruptly with the given error. All the pending and running requests will be asynchronously aborted and error. Since this operation is asynchronously dispatched there might still be some progress on dispatched requests. @@ -207,7 +137,7 @@ Returns: `Boolean` - `false` if dispatcher is busy and further dispatch calls wo * **onConnect** `(abort: () => void, context: object) => void` - Invoked before request is dispatched on socket. May be invoked multiple times when a request is retried when the request at the head of the pipeline fails. * **onError** `(error: Error) => void` - Invoked when an error has occurred. May not throw. -* **onUpgrade** `(statusCode: number, headers: Buffer[], socket: Duplex) => void` (optional) - Invoked when request is upgraded. Required if `DispatchOptions.upgrade` is defined or `DispatchOptions.method === 'CONNECT'`. +* **onUpgrade** `(statusCode: number, headers: Buffer[], socket: Duplex) => void` (optional) - Invoked when request is upgraded. * **onResponseStarted** `() => void` (optional) - Invoked when response is received, before headers have been read. * **onHeaders** `(statusCode: number, headers: Buffer[], resume: () => void, statusText: string) => boolean` - Invoked when statusCode and headers have been received. May be invoked multiple times due to 1xx informational headers. Not required for `upgrade` requests. * **onData** `(chunk: Buffer) => boolean` - Invoked when response payload data is received. Not required for `upgrade` requests. @@ -364,86 +294,6 @@ client.dispatch({ }) ``` -### `Dispatcher.pipeline(options, handler)` - -For easy use with [stream.pipeline](https://nodejs.org/api/stream.html#stream_stream_pipeline_source_transforms_destination_callback). The `handler` argument should return a `Readable` from which the result will be read. Usually it should just return the `body` argument unless some kind of transformation needs to be performed based on e.g. `headers` or `statusCode`. The `handler` should validate the response and save any required state. If there is an error, it should be thrown. The function returns a `Duplex` which writes to the request and reads from the response. - -Arguments: - -* **options** `PipelineOptions` -* **handler** `(data: PipelineHandlerData) => stream.Readable` - -Returns: `stream.Duplex` - -#### Parameter: PipelineOptions - -Extends: [`RequestOptions`](#parameter-requestoptions) - -* **objectMode** `boolean` (optional) - Default: `false` - Set to `true` if the `handler` will return an object stream. - -#### Parameter: PipelineHandlerData - -* **statusCode** `number` -* **headers** `Record` -* **opaque** `unknown` -* **body** `stream.Readable` -* **context** `object` -* **onInfo** `({statusCode: number, headers: Record}) => void | null` (optional) - Default: `null` - Callback collecting all the info headers (HTTP 100-199) received. - -#### Example 1 - Pipeline Echo - -```js -import { Readable, Writable, PassThrough, pipeline } from 'stream' -import { createServer } from 'http' -import { Client } from 'undici' -import { once } from 'events' - -const server = createServer((request, response) => { - request.pipe(response) -}).listen() - -await once(server, 'listening') - -const client = new Client(`http://localhost:${server.address().port}`) - -let res = '' - -pipeline( - new Readable({ - read () { - this.push(Buffer.from('undici')) - this.push(null) - } - }), - client.pipeline({ - path: '/', - method: 'GET' - }, ({ statusCode, headers, body }) => { - console.log(`response received ${statusCode}`) - console.log('headers', headers) - return pipeline(body, new PassThrough(), () => {}) - }), - new Writable({ - write (chunk, _, callback) { - res += chunk.toString() - callback() - }, - final (callback) { - console.log(`Response pipelined to writable: ${res}`) - callback() - } - }), - error => { - if (error) { - console.error(error) - } - - client.close() - server.close() - } -) -``` - ### `Dispatcher.request(options[, callback])` Performs a HTTP request. @@ -471,7 +321,6 @@ Extends: [`DispatchOptions`](#parameter-dispatchoptions) * **opaque** `unknown` (optional) - Default: `null` - Used for passing through context to `ResponseData`. * **signal** `AbortSignal | events.EventEmitter | null` (optional) - Default: `null`. -* **onInfo** `({statusCode: number, headers: Record}) => void | null` (optional) - Default: `null` - Callback collecting all the info headers (HTTP 100-199) received. The `RequestOptions.method` property should not be value `'CONNECT'`. @@ -630,194 +479,6 @@ try { } ``` -### `Dispatcher.stream(options, factory[, callback])` - -A faster version of `Dispatcher.request`. This method expects the second argument `factory` to return a [`stream.Writable`](https://nodejs.org/api/stream.html#stream_class_stream_writable) stream which the response will be written to. This improves performance by avoiding creating an intermediate [`stream.Readable`](https://nodejs.org/api/stream.html#stream_readable_streams) stream when the user expects to directly pipe the response body to a [`stream.Writable`](https://nodejs.org/api/stream.html#stream_class_stream_writable) stream. - -As demonstrated in [Example 1 - Basic GET stream request](#example-1---basic-get-stream-request), it is recommended to use the `option.opaque` property to avoid creating a closure for the `factory` method. This pattern works well with Node.js Web Frameworks such as [Fastify](https://fastify.io). See [Example 2 - Stream to Fastify Response](#example-2---stream-to-fastify-response) for more details. - -Arguments: - -* **options** `RequestOptions` -* **factory** `(data: StreamFactoryData) => stream.Writable` -* **callback** `(error: Error | null, data: StreamData) => void` (optional) - -Returns: `void | Promise` - Only returns a `Promise` if no `callback` argument was passed - -#### Parameter: `StreamFactoryData` - -* **statusCode** `number` -* **headers** `Record` -* **opaque** `unknown` -* **onInfo** `({statusCode: number, headers: Record}) => void | null` (optional) - Default: `null` - Callback collecting all the info headers (HTTP 100-199) received. - -#### Parameter: `StreamData` - -* **opaque** `unknown` -* **trailers** `Record` -* **context** `object` - -#### Example 1 - Basic GET stream request - -```js -import { createServer } from 'http' -import { Client } from 'undici' -import { once } from 'events' -import { Writable } from 'stream' - -const server = createServer((request, response) => { - response.end('Hello, World!') -}).listen() - -await once(server, 'listening') - -const client = new Client(`http://localhost:${server.address().port}`) - -const bufs = [] - -try { - await client.stream({ - path: '/', - method: 'GET', - opaque: { bufs } - }, ({ statusCode, headers, opaque: { bufs } }) => { - console.log(`response received ${statusCode}`) - console.log('headers', headers) - return new Writable({ - write (chunk, encoding, callback) { - bufs.push(chunk) - callback() - } - }) - }) - - console.log(Buffer.concat(bufs).toString('utf-8')) - - client.close() - server.close() -} catch (error) { - console.error(error) -} -``` - -#### Example 2 - Stream to Fastify Response - -In this example, a (fake) request is made to the fastify server using `fastify.inject()`. This request then executes the fastify route handler which makes a subsequent request to the raw Node.js http server using `undici.dispatcher.stream()`. The fastify response is passed to the `opaque` option so that undici can tap into the underlying writable stream using `response.raw`. This methodology demonstrates how one could use undici and fastify together to create fast-as-possible requests from one backend server to another. - -```js -import { createServer } from 'http' -import { Client } from 'undici' -import { once } from 'events' -import fastify from 'fastify' - -const nodeServer = createServer((request, response) => { - response.end('Hello, World! From Node.js HTTP Server') -}).listen() - -await once(nodeServer, 'listening') - -console.log('Node Server listening') - -const nodeServerUndiciClient = new Client(`http://localhost:${nodeServer.address().port}`) - -const fastifyServer = fastify() - -fastifyServer.route({ - url: '/', - method: 'GET', - handler: (request, response) => { - nodeServerUndiciClient.stream({ - path: '/', - method: 'GET', - opaque: response - }, ({ opaque }) => opaque.raw) - } -}) - -await fastifyServer.listen() - -console.log('Fastify Server listening') - -const fastifyServerUndiciClient = new Client(`http://localhost:${fastifyServer.server.address().port}`) - -try { - const { statusCode, body } = await fastifyServerUndiciClient.request({ - path: '/', - method: 'GET' - }) - - console.log(`response received ${statusCode}`) - body.setEncoding('utf8') - body.on('data', console.log) - - nodeServerUndiciClient.close() - fastifyServerUndiciClient.close() - fastifyServer.close() - nodeServer.close() -} catch (error) { } -``` - -### `Dispatcher.upgrade(options[, callback])` - -Upgrade to a different protocol. Visit [MDN - HTTP - Protocol upgrade mechanism](https://developer.mozilla.org/en-US/docs/Web/HTTP/Protocol_upgrade_mechanism) for more details. - -Arguments: - -* **options** `UpgradeOptions` - -* **callback** `(error: Error | null, data: UpgradeData) => void` (optional) - -Returns: `void | Promise` - Only returns a `Promise` if no `callback` argument was passed - -#### Parameter: `UpgradeOptions` - -* **path** `string` -* **method** `string` (optional) - Default: `'GET'` -* **headers** `UndiciHeaders` (optional) - Default: `null` -* **protocol** `string` (optional) - Default: `'Websocket'` - A string of comma separated protocols, in descending preference order. -* **signal** `AbortSignal | EventEmitter | null` (optional) - Default: `null` - -#### Parameter: `UpgradeData` - -* **headers** `http.IncomingHeaders` -* **socket** `stream.Duplex` -* **opaque** `unknown` - -#### Example 1 - Basic Upgrade Request - -```js -import { createServer } from 'http' -import { Client } from 'undici' -import { once } from 'events' - -const server = createServer((request, response) => { - response.statusCode = 101 - response.setHeader('connection', 'upgrade') - response.setHeader('upgrade', request.headers.upgrade) - response.end() -}).listen() - -await once(server, 'listening') - -const client = new Client(`http://localhost:${server.address().port}`) - -try { - const { headers, socket } = await client.upgrade({ - path: '/', - }) - socket.on('end', () => { - console.log(`upgrade: ${headers.upgrade}`) // upgrade: Websocket - client.close() - server.close() - }) - socket.end() -} catch (error) { - console.error(error) - client.close() - server.close() -} -``` - ### `Dispatcher.compose(interceptors[, interceptor])` Compose a new dispatcher from the current dispatcher and the given interceptors. @@ -974,7 +635,7 @@ const client = new Client("http://example.com").compose( }) ); -// or +// or client.dispatch( { path: "/", diff --git a/docs/docs/api/EnvHttpProxyAgent.md b/docs/docs/api/EnvHttpProxyAgent.md index 7d431030f67..12912533aa1 100644 --- a/docs/docs/api/EnvHttpProxyAgent.md +++ b/docs/docs/api/EnvHttpProxyAgent.md @@ -136,26 +136,10 @@ Extends: [`DispatchOptions`](Dispatcher.md#parameter-dispatchoptions) Implements [`Dispatcher.destroy([error, callback])`](Dispatcher.md#dispatcherdestroyerror-callback-promise). -### `EnvHttpProxyAgent.connect(options[, callback])` - -See [`Dispatcher.connect(options[, callback])`](Dispatcher.md#dispatcherconnectoptions-callback). - ### `EnvHttpProxyAgent.dispatch(options, handler)` Implements [`Dispatcher.dispatch(options, handler)`](Dispatcher.md#dispatcherdispatchoptions-handler). -### `EnvHttpProxyAgent.pipeline(options, handler)` - -See [`Dispatcher.pipeline(options, handler)`](Dispatcher.md#dispatcherpipelineoptions-handler). - ### `EnvHttpProxyAgent.request(options[, callback])` See [`Dispatcher.request(options [, callback])`](Dispatcher.md#dispatcherrequestoptions-callback). - -### `EnvHttpProxyAgent.stream(options, factory[, callback])` - -See [`Dispatcher.stream(options, factory[, callback])`](Dispatcher.md#dispatcherstreamoptions-factory-callback). - -### `EnvHttpProxyAgent.upgrade(options[, callback])` - -See [`Dispatcher.upgrade(options[, callback])`](Dispatcher.md#dispatcherupgradeoptions-callback). diff --git a/docs/docs/api/Pool.md b/docs/docs/api/Pool.md index 6b08294b61c..e728a49e36e 100644 --- a/docs/docs/api/Pool.md +++ b/docs/docs/api/Pool.md @@ -44,30 +44,14 @@ Implements [`Dispatcher.close([callback])`](Dispatcher.md#dispatcherclosecallbac Implements [`Dispatcher.destroy([error, callback])`](Dispatcher.md#dispatcherdestroyerror-callback-promise). -### `Pool.connect(options[, callback])` - -See [`Dispatcher.connect(options[, callback])`](Dispatcher.md#dispatcherconnectoptions-callback). - ### `Pool.dispatch(options, handler)` Implements [`Dispatcher.dispatch(options, handler)`](Dispatcher.md#dispatcherdispatchoptions-handler). -### `Pool.pipeline(options, handler)` - -See [`Dispatcher.pipeline(options, handler)`](Dispatcher.md#dispatcherpipelineoptions-handler). - ### `Pool.request(options[, callback])` See [`Dispatcher.request(options [, callback])`](Dispatcher.md#dispatcherrequestoptions-callback). -### `Pool.stream(options, factory[, callback])` - -See [`Dispatcher.stream(options, factory[, callback])`](Dispatcher.md#dispatcherstreamoptions-factory-callback). - -### `Pool.upgrade(options[, callback])` - -See [`Dispatcher.upgrade(options[, callback])`](Dispatcher.md#dispatcherupgradeoptions-callback). - ## Instance Events ### Event: `'connect'` diff --git a/docs/docs/api/api-lifecycle.md b/docs/docs/api/api-lifecycle.md index 2e7db25d132..2d1bca8f6fb 100644 --- a/docs/docs/api/api-lifecycle.md +++ b/docs/docs/api/api-lifecycle.md @@ -28,7 +28,7 @@ stateDiagram-v2 [*] --> idle idle --> pending : connect idle --> destroyed : destroy/close - + pending --> idle : timeout pending --> destroyed : destroy @@ -58,7 +58,7 @@ stateDiagram-v2 ### idle -The **idle** state is the initial state of a `Client` instance. While an `origin` is required for instantiating a `Client` instance, the underlying socket connection will not be established until a request is queued using [`Client.dispatch()`](Client.md#clientdispatchoptions-handlers). By calling `Client.dispatch()` directly or using one of the multiple implementations ([`Client.connect()`](Client.md#clientconnectoptions-callback), [`Client.pipeline()`](Client.md#clientpipelineoptions-handler), [`Client.request()`](Client.md#clientrequestoptions-callback), [`Client.stream()`](Client.md#clientstreamoptions-factory-callback), and [`Client.upgrade()`](Client.md#clientupgradeoptions-callback)), the `Client` instance will transition from **idle** to [**pending**](#pending) and then most likely directly to [**processing**](#processing). +The **idle** state is the initial state of a `Client` instance. While an `origin` is required for instantiating a `Client` instance, the underlying socket connection will not be established until a request is queued using [`Client.dispatch()`](Client.md#clientdispatchoptions-handlers). By calling `Client.dispatch()` directly or using [`Client.request()`](Client.md#clientrequestoptions-callback), the `Client` instance will transition from **idle** to [**pending**](#pending) and then most likely directly to [**processing**](#processing). Calling [`Client.close()`](Client.md#clientclosecallback) or [`Client.destroy()`](Client.md#clientdestroyerror-callback) transitions directly to the [**destroyed**](#destroyed) state since the `Client` instance will have no queued requests in this state. diff --git a/index.js b/index.js index 444706560ae..3344aaa0ae3 100644 --- a/index.js +++ b/index.js @@ -149,10 +149,6 @@ module.exports.ErrorEvent = ErrorEvent module.exports.MessageEvent = MessageEvent module.exports.request = makeDispatcher(api.request) -module.exports.stream = makeDispatcher(api.stream) -module.exports.pipeline = makeDispatcher(api.pipeline) -module.exports.connect = makeDispatcher(api.connect) -module.exports.upgrade = makeDispatcher(api.upgrade) module.exports.MockClient = MockClient module.exports.MockPool = MockPool diff --git a/lib/api/api-connect.js b/lib/api/api-connect.js deleted file mode 100644 index c8b86dd7d53..00000000000 --- a/lib/api/api-connect.js +++ /dev/null @@ -1,110 +0,0 @@ -'use strict' - -const assert = require('node:assert') -const { AsyncResource } = require('node:async_hooks') -const { InvalidArgumentError, SocketError } = require('../core/errors') -const util = require('../core/util') -const { addSignal, removeSignal } = require('./abort-signal') - -class ConnectHandler extends AsyncResource { - constructor (opts, callback) { - if (!opts || typeof opts !== 'object') { - throw new InvalidArgumentError('invalid opts') - } - - if (typeof callback !== 'function') { - throw new InvalidArgumentError('invalid callback') - } - - const { signal, opaque, responseHeaders } = opts - - if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') { - throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget') - } - - super('UNDICI_CONNECT') - - this.opaque = opaque || null - this.responseHeaders = responseHeaders || null - this.callback = callback - this.abort = null - - addSignal(this, signal) - } - - onConnect (abort, context) { - if (this.reason) { - abort(this.reason) - return - } - - assert(this.callback) - - this.abort = abort - this.context = context - } - - onHeaders () { - throw new SocketError('bad connect', null) - } - - onUpgrade (statusCode, rawHeaders, socket) { - const { callback, opaque, context } = this - - removeSignal(this) - - this.callback = null - - let headers = rawHeaders - // Indicates is an HTTP2Session - if (headers != null) { - headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) - } - - this.runInAsyncScope(callback, null, null, { - statusCode, - headers, - socket, - opaque, - context - }) - } - - onError (err) { - const { callback, opaque } = this - - removeSignal(this) - - if (callback) { - this.callback = null - queueMicrotask(() => { - this.runInAsyncScope(callback, null, err, { opaque }) - }) - } - } -} - -function connect (opts, callback) { - if (callback === undefined) { - return new Promise((resolve, reject) => { - connect.call(this, opts, (err, data) => { - return err ? reject(err) : resolve(data) - }) - }) - } - - try { - const connectHandler = new ConnectHandler(opts, callback) - const connectOptions = { ...opts, method: 'CONNECT' } - - this.dispatch(connectOptions, connectHandler) - } catch (err) { - if (typeof callback !== 'function') { - throw err - } - const opaque = opts?.opaque - queueMicrotask(() => callback(err, { opaque })) - } -} - -module.exports = connect diff --git a/lib/api/api-pipeline.js b/lib/api/api-pipeline.js deleted file mode 100644 index 77f3520a83f..00000000000 --- a/lib/api/api-pipeline.js +++ /dev/null @@ -1,252 +0,0 @@ -'use strict' - -const { - Readable, - Duplex, - PassThrough -} = require('node:stream') -const assert = require('node:assert') -const { AsyncResource } = require('node:async_hooks') -const { - InvalidArgumentError, - InvalidReturnValueError, - RequestAbortedError -} = require('../core/errors') -const util = require('../core/util') -const { addSignal, removeSignal } = require('./abort-signal') - -function noop () {} - -const kResume = Symbol('resume') - -class PipelineRequest extends Readable { - constructor () { - super({ autoDestroy: true }) - - this[kResume] = null - } - - _read () { - const { [kResume]: resume } = this - - if (resume) { - this[kResume] = null - resume() - } - } - - _destroy (err, callback) { - this._read() - - callback(err) - } -} - -class PipelineResponse extends Readable { - constructor (resume) { - super({ autoDestroy: true }) - this[kResume] = resume - } - - _read () { - this[kResume]() - } - - _destroy (err, callback) { - if (!err && !this._readableState.endEmitted) { - err = new RequestAbortedError() - } - - callback(err) - } -} - -class PipelineHandler extends AsyncResource { - constructor (opts, handler) { - if (!opts || typeof opts !== 'object') { - throw new InvalidArgumentError('invalid opts') - } - - if (typeof handler !== 'function') { - throw new InvalidArgumentError('invalid handler') - } - - const { signal, method, opaque, onInfo, responseHeaders } = opts - - if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') { - throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget') - } - - if (method === 'CONNECT') { - throw new InvalidArgumentError('invalid method') - } - - if (onInfo && typeof onInfo !== 'function') { - throw new InvalidArgumentError('invalid onInfo callback') - } - - super('UNDICI_PIPELINE') - - this.opaque = opaque || null - this.responseHeaders = responseHeaders || null - this.handler = handler - this.abort = null - this.context = null - this.onInfo = onInfo || null - - this.req = new PipelineRequest().on('error', noop) - - this.ret = new Duplex({ - readableObjectMode: opts.objectMode, - autoDestroy: true, - read: () => { - const { body } = this - - if (body?.resume) { - body.resume() - } - }, - write: (chunk, encoding, callback) => { - const { req } = this - - if (req.push(chunk, encoding) || req._readableState.destroyed) { - callback() - } else { - req[kResume] = callback - } - }, - destroy: (err, callback) => { - const { body, req, res, ret, abort } = this - - if (!err && !ret._readableState.endEmitted) { - err = new RequestAbortedError() - } - - if (abort && err) { - abort() - } - - util.destroy(body, err) - util.destroy(req, err) - util.destroy(res, err) - - removeSignal(this) - - callback(err) - } - }).on('prefinish', () => { - const { req } = this - - // Node < 15 does not call _final in same tick. - req.push(null) - }) - - this.res = null - - addSignal(this, signal) - } - - onConnect (abort, context) { - const { res } = this - - if (this.reason) { - abort(this.reason) - return - } - - assert(!res, 'pipeline cannot be retried') - - this.abort = abort - this.context = context - } - - onHeaders (statusCode, rawHeaders, resume) { - const { opaque, handler, context } = this - - if (statusCode < 200) { - if (this.onInfo) { - const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) - this.onInfo({ statusCode, headers }) - } - return - } - - this.res = new PipelineResponse(resume) - - let body - try { - this.handler = null - const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) - body = this.runInAsyncScope(handler, null, { - statusCode, - headers, - opaque, - body: this.res, - context - }) - } catch (err) { - this.res.on('error', noop) - throw err - } - - if (!body || typeof body.on !== 'function') { - throw new InvalidReturnValueError('expected Readable') - } - - body - .on('data', (chunk) => { - const { ret, body } = this - - if (!ret.push(chunk) && body.pause) { - body.pause() - } - }) - .on('error', (err) => { - const { ret } = this - - util.destroy(ret, err) - }) - .on('end', () => { - const { ret } = this - - ret.push(null) - }) - .on('close', () => { - const { ret } = this - - if (!ret._readableState.ended) { - util.destroy(ret, new RequestAbortedError()) - } - }) - - this.body = body - } - - onData (chunk) { - const { res } = this - return res.push(chunk) - } - - onComplete (trailers) { - const { res } = this - res.push(null) - } - - onError (err) { - const { ret } = this - this.handler = null - util.destroy(ret, err) - } -} - -function pipeline (opts, handler) { - try { - const pipelineHandler = new PipelineHandler(opts, handler) - this.dispatch({ ...opts, body: pipelineHandler.req }, pipelineHandler) - return pipelineHandler.ret - } catch (err) { - return new PassThrough().destroy(err) - } -} - -module.exports = pipeline diff --git a/lib/api/api-request.js b/lib/api/api-request.js index 2c86dd07c58..193e8ccef79 100644 --- a/lib/api/api-request.js +++ b/lib/api/api-request.js @@ -14,7 +14,7 @@ class RequestHandler extends AsyncResource { throw new InvalidArgumentError('invalid opts') } - const { signal, method, opaque, body, onInfo, responseHeaders, highWaterMark } = opts + const { signal, method, opaque, body, responseHeaders, highWaterMark } = opts try { if (typeof callback !== 'function') { @@ -33,10 +33,6 @@ class RequestHandler extends AsyncResource { throw new InvalidArgumentError('invalid method') } - if (onInfo && typeof onInfo !== 'function') { - throw new InvalidArgumentError('invalid onInfo callback') - } - super('UNDICI_REQUEST') } catch (err) { if (util.isStream(body)) { @@ -54,7 +50,6 @@ class RequestHandler extends AsyncResource { this.body = body this.trailers = {} this.context = null - this.onInfo = onInfo || null this.highWaterMark = highWaterMark this.reason = null this.removeAbortListener = null @@ -86,20 +81,16 @@ class RequestHandler extends AsyncResource { } onHeaders (statusCode, rawHeaders, resume, statusMessage) { - const { callback, opaque, abort, context, responseHeaders, highWaterMark } = this + const { callback, opaque, abort, context, highWaterMark } = this - const headers = responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) + const headers = util.parseHeaders(rawHeaders) if (statusCode < 200) { - if (this.onInfo) { - this.onInfo({ statusCode, headers }) - } return } - const parsedHeaders = responseHeaders === 'raw' ? util.parseHeaders(rawHeaders) : headers - const contentType = parsedHeaders['content-type'] - const contentLength = parsedHeaders['content-length'] + const contentType = headers['content-type'] + const contentLength = headers['content-length'] const res = new Readable({ resume, abort, diff --git a/lib/api/api-stream.js b/lib/api/api-stream.js deleted file mode 100644 index eb927336a3f..00000000000 --- a/lib/api/api-stream.js +++ /dev/null @@ -1,209 +0,0 @@ -'use strict' - -const assert = require('node:assert') -const { finished } = require('node:stream') -const { AsyncResource } = require('node:async_hooks') -const { InvalidArgumentError, InvalidReturnValueError } = require('../core/errors') -const util = require('../core/util') -const { addSignal, removeSignal } = require('./abort-signal') - -function noop () {} - -class StreamHandler extends AsyncResource { - constructor (opts, factory, callback) { - if (!opts || typeof opts !== 'object') { - throw new InvalidArgumentError('invalid opts') - } - - const { signal, method, opaque, body, onInfo, responseHeaders } = opts - - try { - if (typeof callback !== 'function') { - throw new InvalidArgumentError('invalid callback') - } - - if (typeof factory !== 'function') { - throw new InvalidArgumentError('invalid factory') - } - - if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') { - throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget') - } - - if (method === 'CONNECT') { - throw new InvalidArgumentError('invalid method') - } - - if (onInfo && typeof onInfo !== 'function') { - throw new InvalidArgumentError('invalid onInfo callback') - } - - super('UNDICI_STREAM') - } catch (err) { - if (util.isStream(body)) { - util.destroy(body.on('error', noop), err) - } - throw err - } - - this.responseHeaders = responseHeaders || null - this.opaque = opaque || null - this.factory = factory - this.callback = callback - this.res = null - this.abort = null - this.context = null - this.trailers = null - this.body = body - this.onInfo = onInfo || null - - if (util.isStream(body)) { - body.on('error', (err) => { - this.onError(err) - }) - } - - addSignal(this, signal) - } - - onConnect (abort, context) { - if (this.reason) { - abort(this.reason) - return - } - - assert(this.callback) - - this.abort = abort - this.context = context - } - - onHeaders (statusCode, rawHeaders, resume, statusMessage) { - const { factory, opaque, context, responseHeaders } = this - - const headers = responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) - - if (statusCode < 200) { - if (this.onInfo) { - this.onInfo({ statusCode, headers }) - } - return - } - - this.factory = null - - if (factory === null) { - return - } - - const res = this.runInAsyncScope(factory, null, { - statusCode, - headers, - opaque, - context - }) - - if ( - !res || - typeof res.write !== 'function' || - typeof res.end !== 'function' || - typeof res.on !== 'function' - ) { - throw new InvalidReturnValueError('expected Writable') - } - - // TODO: Avoid finished. It registers an unnecessary amount of listeners. - finished(res, { readable: false }, (err) => { - const { callback, res, opaque, trailers, abort } = this - - this.res = null - if (err || !res.readable) { - util.destroy(res, err) - } - - this.callback = null - this.runInAsyncScope(callback, null, err || null, { opaque, trailers }) - - if (err) { - abort() - } - }) - - res.on('drain', resume) - - this.res = res - - const needDrain = res.writableNeedDrain !== undefined - ? res.writableNeedDrain - : res._writableState?.needDrain - - return needDrain !== true - } - - onData (chunk) { - const { res } = this - - return res ? res.write(chunk) : true - } - - onComplete (trailers) { - const { res } = this - - removeSignal(this) - - if (!res) { - return - } - - this.trailers = util.parseHeaders(trailers) - - res.end() - } - - onError (err) { - const { res, callback, opaque, body } = this - - removeSignal(this) - - this.factory = null - - if (res) { - this.res = null - util.destroy(res, err) - } else if (callback) { - this.callback = null - queueMicrotask(() => { - this.runInAsyncScope(callback, null, err, { opaque }) - }) - } - - if (body) { - this.body = null - util.destroy(body, err) - } - } -} - -function stream (opts, factory, callback) { - if (callback === undefined) { - return new Promise((resolve, reject) => { - stream.call(this, opts, factory, (err, data) => { - return err ? reject(err) : resolve(data) - }) - }) - } - - try { - const handler = new StreamHandler(opts, factory, callback) - - this.dispatch(opts, handler) - } catch (err) { - if (typeof callback !== 'function') { - throw err - } - const opaque = opts?.opaque - queueMicrotask(() => callback(err, { opaque })) - } -} - -module.exports = stream diff --git a/lib/api/api-upgrade.js b/lib/api/api-upgrade.js deleted file mode 100644 index f6efdc98626..00000000000 --- a/lib/api/api-upgrade.js +++ /dev/null @@ -1,110 +0,0 @@ -'use strict' - -const { InvalidArgumentError, SocketError } = require('../core/errors') -const { AsyncResource } = require('node:async_hooks') -const assert = require('node:assert') -const util = require('../core/util') -const { addSignal, removeSignal } = require('./abort-signal') - -class UpgradeHandler extends AsyncResource { - constructor (opts, callback) { - if (!opts || typeof opts !== 'object') { - throw new InvalidArgumentError('invalid opts') - } - - if (typeof callback !== 'function') { - throw new InvalidArgumentError('invalid callback') - } - - const { signal, opaque, responseHeaders } = opts - - if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') { - throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget') - } - - super('UNDICI_UPGRADE') - - this.responseHeaders = responseHeaders || null - this.opaque = opaque || null - this.callback = callback - this.abort = null - this.context = null - - addSignal(this, signal) - } - - onConnect (abort, context) { - if (this.reason) { - abort(this.reason) - return - } - - assert(this.callback) - - this.abort = abort - this.context = null - } - - onHeaders () { - throw new SocketError('bad upgrade', null) - } - - onUpgrade (statusCode, rawHeaders, socket) { - assert(statusCode === 101) - - const { callback, opaque, context } = this - - removeSignal(this) - - this.callback = null - const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) - this.runInAsyncScope(callback, null, null, { - headers, - socket, - opaque, - context - }) - } - - onError (err) { - const { callback, opaque } = this - - removeSignal(this) - - if (callback) { - this.callback = null - queueMicrotask(() => { - this.runInAsyncScope(callback, null, err, { opaque }) - }) - } - } -} - -function upgrade (opts, callback) { - if (callback === undefined) { - return new Promise((resolve, reject) => { - upgrade.call(this, opts, (err, data) => { - return err ? reject(err) : resolve(data) - }) - }) - } - - try { - const upgradeHandler = new UpgradeHandler(opts, callback) - const upgradeOpts = { - ...opts, - method: opts.method || 'GET', - upgrade: opts.protocol || 'Websocket' - } - - this.dispatch(upgradeOpts, upgradeHandler) - } catch (err) { - if (typeof callback !== 'function') { - throw err - } - const opaque = opts?.opaque - queueMicrotask(() => callback(err, { opaque })) - } -} - -module.exports = upgrade diff --git a/lib/api/index.js b/lib/api/index.js index 8983a5e746f..82763aac71c 100644 --- a/lib/api/index.js +++ b/lib/api/index.js @@ -1,7 +1,3 @@ 'use strict' module.exports.request = require('./api-request') -module.exports.stream = require('./api-stream') -module.exports.pipeline = require('./api-pipeline') -module.exports.upgrade = require('./api-upgrade') -module.exports.connect = require('./api-connect') diff --git a/test/client-connect.js b/test/client-connect.js deleted file mode 100644 index e002a42c571..00000000000 --- a/test/client-connect.js +++ /dev/null @@ -1,44 +0,0 @@ -'use strict' - -const { tspl } = require('@matteo.collina/tspl') -const { test, after } = require('node:test') -const { once } = require('node:events') -const { Client, errors } = require('..') -const http = require('node:http') -const EE = require('node:events') -const { kBusy } = require('../lib/core/symbols') - -// TODO: move to test/node-test/client-connect.js -test('connect aborted after connect', async (t) => { - t = tspl(t, { plan: 3 }) - - const signal = new EE() - const server = http.createServer((req, res) => { - t.fail() - }) - server.on('connect', (req, c, firstBodyChunk) => { - signal.emit('abort') - }) - after(() => server.close()) - - server.listen(0) - - await once(server, 'listening') - - const client = new Client(`http://localhost:${server.address().port}`, { - pipelining: 3 - }) - after(() => client.close()) - - client.connect({ - path: '/', - signal, - opaque: 'asd' - }, (err, { opaque }) => { - t.strictEqual(opaque, 'asd') - t.ok(err instanceof errors.RequestAbortedError) - }) - t.strictEqual(client[kBusy], true) - - await t.completed -}) diff --git a/test/client-pipeline.js b/test/client-pipeline.js deleted file mode 100644 index bc2cd1d3a95..00000000000 --- a/test/client-pipeline.js +++ /dev/null @@ -1,1102 +0,0 @@ -'use strict' - -const { tspl } = require('@matteo.collina/tspl') -const { test, after } = require('node:test') -const { Client, errors } = require('..') -const EE = require('node:events') -const { createServer } = require('node:http') -const { - pipeline, - Readable, - Transform, - Writable, - PassThrough -} = require('node:stream') - -test('pipeline get', async (t) => { - t = tspl(t, { plan: 17 }) - - const server = createServer((req, res) => { - t.strictEqual('/', req.url) - t.strictEqual('GET', req.method) - t.strictEqual(`localhost:${server.address().port}`, req.headers.host) - t.strictEqual(undefined, req.headers['content-length']) - res.setHeader('Content-Type', 'text/plain') - res.end('hello') - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.close()) - - { - const bufs = [] - const signal = new EE() - client.pipeline({ signal, path: '/', method: 'GET' }, ({ statusCode, headers, body }) => { - t.strictEqual(statusCode, 200) - t.strictEqual(headers['content-type'], 'text/plain') - t.strictEqual(signal.listenerCount('abort'), 1) - return body - }) - .end() - .on('data', (buf) => { - bufs.push(buf) - }) - .on('end', () => { - t.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) - }) - .on('close', () => { - t.strictEqual(signal.listenerCount('abort'), 0) - }) - t.strictEqual(signal.listenerCount('abort'), 1) - } - - { - const bufs = [] - client.pipeline({ path: '/', method: 'GET' }, ({ statusCode, headers, body }) => { - t.strictEqual(statusCode, 200) - t.strictEqual(headers['content-type'], 'text/plain') - return body - }) - .end() - .on('data', (buf) => { - bufs.push(buf) - }) - .on('end', () => { - t.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) - }) - } - }) - - await t.completed -}) - -test('pipeline echo', async (t) => { - t = tspl(t, { plan: 2 }) - - const server = createServer((req, res) => { - req.pipe(res) - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.close()) - - let res = '' - const buf1 = Buffer.alloc(1e3).toString() - const buf2 = Buffer.alloc(1e6).toString() - pipeline( - new Readable({ - read () { - this.push(buf1) - this.push(buf2) - this.push(null) - } - }), - client.pipeline({ - path: '/', - method: 'PUT' - }, ({ body }) => { - return pipeline(body, new PassThrough(), () => {}) - }), - new Writable({ - write (chunk, encoding, callback) { - res += chunk.toString() - callback() - }, - final (callback) { - t.strictEqual(res, buf1 + buf2) - callback() - } - }), - (err) => { - t.ifError(err) - } - ) - }) - - await t.completed -}) - -test('pipeline ignore request body', async (t) => { - t = tspl(t, { plan: 2 }) - - let done - const server = createServer((req, res) => { - res.write('asd') - res.end() - done() - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.close()) - - let res = '' - const buf1 = Buffer.alloc(1e3).toString() - const buf2 = Buffer.alloc(1e6).toString() - pipeline( - new Readable({ - read () { - this.push(buf1) - this.push(buf2) - done = () => this.push(null) - } - }), - client.pipeline({ - path: '/', - method: 'PUT' - }, ({ body }) => { - return pipeline(body, new PassThrough(), () => {}) - }), - new Writable({ - write (chunk, encoding, callback) { - res += chunk.toString() - callback() - }, - final (callback) { - t.strictEqual(res, 'asd') - callback() - } - }), - (err) => { - t.ifError(err) - } - ) - }) - - await t.completed -}) - -test('pipeline invalid handler', async (t) => { - t = tspl(t, { plan: 1 }) - - const client = new Client('http://localhost:5000') - client.pipeline({}, null).on('error', (err) => { - t.ok(/handler/.test(err)) - }) - - await t.completed -}) - -test('pipeline invalid handler return after destroy should not error', async (t) => { - t = tspl(t, { plan: 3 }) - - const server = createServer((req, res) => { - req.pipe(res) - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`, { - pipelining: 3 - }) - after(() => client.destroy()) - - const dup = client.pipeline({ - path: '/', - method: 'GET' - }, ({ body }) => { - body.on('error', (err) => { - t.strictEqual(err.message, 'asd') - }) - dup.destroy(new Error('asd')) - return {} - }) - .on('error', (err) => { - t.strictEqual(err.message, 'asd') - }) - .on('close', () => { - t.ok(true, 'pass') - }) - .end() - }) - - await t.completed -}) - -test('pipeline error body', async (t) => { - t = tspl(t, { plan: 2 }) - - const server = createServer((req, res) => { - req.pipe(res) - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.close()) - - const buf = Buffer.alloc(1e6).toString() - pipeline( - new Readable({ - read () { - this.push(buf) - } - }), - client.pipeline({ - path: '/', - method: 'PUT' - }, ({ body }) => { - const pt = new PassThrough() - process.nextTick(() => { - pt.destroy(new Error('asd')) - }) - body.on('error', (err) => { - t.ok(err) - }) - return pipeline(body, pt, () => {}) - }), - new PassThrough(), - (err) => { - t.ok(err) - } - ) - }) - - await t.completed -}) - -test('pipeline destroy body', async (t) => { - t = tspl(t, { plan: 2 }) - - const server = createServer((req, res) => { - req.pipe(res) - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.close()) - - const buf = Buffer.alloc(1e6).toString() - pipeline( - new Readable({ - read () { - this.push(buf) - } - }), - client.pipeline({ - path: '/', - method: 'PUT' - }, ({ body }) => { - const pt = new PassThrough() - process.nextTick(() => { - pt.destroy() - }) - body.on('error', (err) => { - t.ok(err) - }) - return pipeline(body, pt, () => {}) - }), - new PassThrough(), - (err) => { - t.ok(err) - } - ) - }) - - await t.completed -}) - -test('pipeline backpressure', async (t) => { - t = tspl(t, { plan: 1 }) - - const server = createServer((req, res) => { - req.pipe(res) - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.destroy()) - - const buf = Buffer.alloc(1e6).toString() - const duplex = client.pipeline({ - path: '/', - method: 'PUT' - }, ({ body }) => { - const pt = new PassThrough() - return pipeline(body, pt, () => {}) - }) - - duplex.end(buf) - duplex.on('data', () => { - duplex.pause() - setImmediate(() => { - duplex.resume() - }) - }).on('end', () => { - t.ok(true, 'pass') - }) - }) - - await t.completed -}) - -test('pipeline invalid handler return', async (t) => { - t = tspl(t, { plan: 2 }) - - const server = createServer((req, res) => { - req.pipe(res) - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.destroy()) - - client.pipeline({ - path: '/', - method: 'GET' - }, ({ body }) => { - // TODO: Should body cause unhandled exception? - body.on('error', () => {}) - }) - .on('error', (err) => { - t.ok(err instanceof errors.InvalidReturnValueError) - }) - .end() - - client.pipeline({ - path: '/', - method: 'GET' - }, ({ body }) => { - // TODO: Should body cause unhandled exception? - body.on('error', () => {}) - return {} - }) - .on('error', (err) => { - t.ok(err instanceof errors.InvalidReturnValueError) - }) - .end() - }) - - await t.completed -}) - -test('pipeline throw handler', async (t) => { - t = tspl(t, { plan: 1 }) - - const server = createServer((req, res) => { - req.pipe(res) - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.destroy()) - - client.pipeline({ - path: '/', - method: 'GET' - }, ({ body }) => { - // TODO: Should body cause unhandled exception? - body.on('error', () => {}) - throw new Error('asd') - }) - .on('error', (err) => { - t.strictEqual(err.message, 'asd') - }) - .end() - }) - - await t.completed -}) - -test('pipeline destroy and throw handler', async (t) => { - t = tspl(t, { plan: 2 }) - - const server = createServer((req, res) => { - req.pipe(res) - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.destroy()) - - const dup = client.pipeline({ - path: '/', - method: 'GET' - }, ({ body }) => { - dup.destroy() - // TODO: Should body cause unhandled exception? - body.on('error', () => {}) - throw new Error('asd') - }) - .end() - .on('error', (err) => { - t.ok(err instanceof errors.RequestAbortedError) - }) - .on('close', () => { - t.ok(true, 'pass') - }) - }) - - await t.completed -}) - -test('pipeline abort res', async (t) => { - t = tspl(t, { plan: 2 }) - - let _res - const server = createServer((req, res) => { - res.write('asd') - _res = res - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.destroy()) - - client.pipeline({ - path: '/', - method: 'GET' - }, ({ body }) => { - setImmediate(() => { - body.destroy() - _res.write('asdasdadasd') - const timeout = setTimeout(() => { - t.fail() - }, 100) - client.on('disconnect', () => { - clearTimeout(timeout) - t.ok(true, 'pass') - }) - }) - return body - }) - .on('error', (err) => { - t.ok(err instanceof errors.RequestAbortedError) - }) - .end() - }) - - await t.completed -}) - -test('pipeline abort server res', async (t) => { - t = tspl(t, { plan: 1 }) - - const server = createServer((req, res) => { - res.destroy() - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.destroy()) - - client.pipeline({ - path: '/', - method: 'GET' - }, () => { - t.fail() - }) - .on('error', (err) => { - t.ok(err instanceof errors.SocketError) - }) - .end() - }) - - await t.completed -}) - -test('pipeline abort duplex', async (t) => { - t = tspl(t, { plan: 2 }) - - const server = createServer((req, res) => { - res.end() - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.destroy()) - - client.request({ - path: '/', - method: 'PUT' - }, (err, data) => { - t.ifError(err) - data.body.resume() - - client.pipeline({ - path: '/', - method: 'PUT' - }, () => { - t.fail() - }).destroy().on('error', (err) => { - t.ok(err instanceof errors.RequestAbortedError) - }) - }) - }) - - await t.completed -}) - -test('pipeline abort piped res', async (t) => { - t = tspl(t, { plan: 1 }) - - const server = createServer((req, res) => { - res.write('asd') - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.destroy()) - - client.pipeline({ - path: '/', - method: 'GET' - }, ({ body }) => { - const pt = new PassThrough() - setImmediate(() => { - pt.destroy() - }) - return pipeline(body, pt, () => {}) - }) - .on('error', (err) => { - t.strictEqual(err.code, 'UND_ERR_ABORTED') - }) - .end() - }) - - await t.completed -}) - -test('pipeline abort piped res 2', async (t) => { - t = tspl(t, { plan: 2 }) - - const server = createServer((req, res) => { - res.write('asd') - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.destroy()) - - client.pipeline({ - path: '/', - method: 'GET' - }, ({ body }) => { - const pt = new PassThrough() - body.on('error', (err) => { - t.ok(err instanceof errors.RequestAbortedError) - }) - setImmediate(() => { - pt.destroy() - }) - body.pipe(pt) - return pt - }) - .on('error', (err) => { - t.ok(err instanceof errors.RequestAbortedError) - }) - .end() - }) - - await t.completed -}) - -test('pipeline abort piped res 3', async (t) => { - t = tspl(t, { plan: 2 }) - - const server = createServer((req, res) => { - res.write('asd') - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.destroy()) - - client.pipeline({ - path: '/', - method: 'GET' - }, ({ body }) => { - const pt = new PassThrough() - body.on('error', (err) => { - t.strictEqual(err.message, 'asd') - }) - setImmediate(() => { - pt.destroy(new Error('asd')) - }) - body.pipe(pt) - return pt - }) - .on('error', (err) => { - t.strictEqual(err.message, 'asd') - }) - .end() - }) - - await t.completed -}) - -test('pipeline abort server res after headers', async (t) => { - t = tspl(t, { plan: 1 }) - - let _res - const server = createServer((req, res) => { - res.write('asd') - _res = res - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.destroy()) - - client.pipeline({ - path: '/', - method: 'GET' - }, (data) => { - _res.destroy() - return data.body - }) - .on('error', (err) => { - t.ok(err instanceof errors.SocketError) - }) - .end() - }) - - await t.completed -}) - -test('pipeline w/ write abort server res after headers', async (t) => { - t = tspl(t, { plan: 1 }) - - let _res - const server = createServer((req, res) => { - req.pipe(res) - _res = res - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.destroy()) - - client.pipeline({ - path: '/', - method: 'PUT' - }, (data) => { - _res.destroy() - return data.body - }) - .on('error', (err) => { - t.ok(err instanceof errors.SocketError) - }) - .resume() - .write('asd') - }) - - await t.completed -}) - -test('destroy in push', async (t) => { - t = tspl(t, { plan: 3 }) - - let _res - const server = createServer((req, res) => { - res.write('asd') - _res = res - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.close()) - - client.pipeline({ path: '/', method: 'GET' }, ({ body }) => { - body.once('data', () => { - _res.write('asd') - body.on('data', (buf) => { - body.destroy() - _res.end() - }).on('error', (err) => { - t.ok(err) - }) - }) - return body - }).on('error', (err) => { - t.ok(err) - }).resume().end() - - client.pipeline({ path: '/', method: 'GET' }, ({ body }) => { - let buf = '' - body.on('data', (chunk) => { - buf = chunk.toString() - _res.end() - }).on('end', () => { - t.strictEqual('asd', buf) - }) - return body - }).resume().end() - }) - - await t.completed -}) - -test('pipeline args validation', async (t) => { - t = tspl(t, { plan: 2 }) - - const client = new Client('http://localhost:5000') - - const ret = client.pipeline(null, () => {}) - ret.on('error', (err) => { - t.ok(/opts/.test(err.message)) - t.ok(err instanceof errors.InvalidArgumentError) - }) - - await t.completed -}) - -test('pipeline factory throw not unhandled', async (t) => { - t = tspl(t, { plan: 1 }) - - const server = createServer((req, res) => { - res.write('asd') - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.destroy()) - - client.pipeline({ - path: '/', - method: 'GET' - }, (data) => { - throw new Error('asd') - }) - .on('error', (err) => { - t.ok(err) - }) - .end() - }) - - await t.completed -}) - -test('pipeline destroy before dispatch', async (t) => { - t = tspl(t, { plan: 1 }) - - const server = createServer((req, res) => { - res.end('hello') - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.destroy()) - - client - .pipeline({ path: '/', method: 'GET' }, ({ body }) => { - return body - }) - .on('error', (err) => { - t.ok(err) - }) - .end() - .destroy() - }) - - await t.completed -}) - -test('pipeline legacy stream', async (t) => { - t = tspl(t, { plan: 1 }) - - const server = createServer((req, res) => { - res.write(Buffer.alloc(16e3)) - setImmediate(() => { - res.end(Buffer.alloc(16e3)) - }) - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.close()) - - client - .pipeline({ path: '/', method: 'GET' }, ({ body }) => { - const pt = new PassThrough() - pt.pause = null - return body.pipe(pt) - }) - .resume() - .on('end', () => { - t.ok(true, 'pass') - }) - .end() - }) - - await t.completed -}) - -test('pipeline objectMode', async (t) => { - t = tspl(t, { plan: 1 }) - - const server = createServer((req, res) => { - res.end(JSON.stringify({ asd: 1 })) - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.close()) - - client - .pipeline({ path: '/', method: 'GET', objectMode: true }, ({ body }) => { - return pipeline(body, new Transform({ - readableObjectMode: true, - transform (chunk, encoding, callback) { - callback(null, JSON.parse(chunk)) - } - }), () => {}) - }) - .on('data', data => { - t.deepStrictEqual(data, { asd: 1 }) - }) - .end() - }) - - await t.completed -}) - -test('pipeline invalid opts', async (t) => { - t = tspl(t, { plan: 2 }) - - const server = createServer((req, res) => { - res.end(JSON.stringify({ asd: 1 })) - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.destroy()) - - client.close((err) => { - t.ifError(err) - }) - client - .pipeline({ path: '/', method: 'GET', objectMode: true }, ({ body }) => { - t.fail() - }) - .on('error', (err) => { - t.ok(err) - }) - }) - - await t.completed -}) - -test('pipeline CONNECT throw', async (t) => { - t = tspl(t, { plan: 1 }) - - const server = createServer((req, res) => { - res.end('asd') - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.destroy()) - - client.pipeline({ - path: '/', - method: 'CONNECT' - }, () => { - t.fail() - }).on('error', (err) => { - t.ok(err instanceof errors.InvalidArgumentError) - }) - client.on('disconnect', () => { - t.fail() - }) - }) - - await t.completed -}) - -test('pipeline body without destroy', async (t) => { - t = tspl(t, { plan: 1 }) - - const server = createServer((req, res) => { - res.end('asd') - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.destroy()) - - client.pipeline({ - path: '/', - method: 'GET' - }, ({ body }) => { - const pt = new PassThrough({ autoDestroy: false }) - pt.destroy = null - return body.pipe(pt) - }) - .end() - .on('end', () => { - t.ok(true, 'pass') - }) - .resume() - }) - - await t.completed -}) - -test('pipeline ignore 1xx', async (t) => { - t = tspl(t, { plan: 1 }) - - const server = createServer((req, res) => { - res.writeProcessing() - res.end('hello') - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.close()) - - let buf = '' - client.pipeline({ - path: '/', - method: 'GET' - }, ({ body }) => body) - .on('data', (chunk) => { - buf += chunk - }) - .on('end', () => { - t.strictEqual(buf, 'hello') - }) - .end() - }) - - await t.completed -}) - -test('pipeline ignore 1xx and use onInfo', async (t) => { - t = tspl(t, { plan: 3 }) - - const infos = [] - const server = createServer((req, res) => { - res.writeProcessing() - res.end('hello') - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.close()) - - let buf = '' - client.pipeline({ - path: '/', - method: 'GET', - onInfo: (x) => { - infos.push(x) - } - }, ({ body }) => body) - .on('data', (chunk) => { - buf += chunk - }) - .on('end', () => { - t.strictEqual(buf, 'hello') - t.strictEqual(infos.length, 1) - t.strictEqual(infos[0].statusCode, 102) - }) - .end() - }) - - await t.completed -}) - -test('pipeline backpressure', async (t) => { - t = tspl(t, { plan: 1 }) - - const expected = Buffer.alloc(1e6).toString() - - const server = createServer((req, res) => { - res.writeProcessing() - res.end(expected) - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.close()) - - let buf = '' - client.pipeline({ - path: '/', - method: 'GET' - }, ({ body }) => body) - .end() - .pipe(new Transform({ - highWaterMark: 1, - transform (chunk, encoding, callback) { - setImmediate(() => { - callback(null, chunk) - }) - } - })) - .on('data', chunk => { - buf += chunk - }) - .on('end', () => { - t.strictEqual(buf, expected) - }) - }) - - await t.completed -}) - -test('pipeline abort after headers', async (t) => { - t = tspl(t, { plan: 1 }) - - const server = createServer((req, res) => { - res.writeProcessing() - res.write('asd') - setImmediate(() => { - res.write('asd') - }) - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.destroy()) - - const signal = new EE() - client.pipeline({ - path: '/', - method: 'GET', - signal - }, ({ body }) => { - process.nextTick(() => { - signal.emit('abort') - }) - return body - }) - .end() - .on('error', (err) => { - t.ok(err instanceof errors.RequestAbortedError) - }) - }) - - await t.completed -}) diff --git a/test/client-request.js b/test/client-request.js index 27555d5186a..f8aafb36e33 100644 --- a/test/client-request.js +++ b/test/client-request.js @@ -783,141 +783,6 @@ test('request post body no extra data handler', async (t) => { await t.completed }) -test('request with onInfo callback', async (t) => { - t = tspl(t, { plan: 3 }) - const infos = [] - const server = createServer((req, res) => { - res.writeProcessing() - res.setHeader('Content-Type', 'application/json') - res.end(JSON.stringify({ foo: 'bar' })) - }) - after(() => server.close()) - - server.listen(0, async () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.destroy()) - - await client.request({ - path: '/', - method: 'GET', - onInfo: (x) => { infos.push(x) } - }) - t.strictEqual(infos.length, 1) - t.strictEqual(infos[0].statusCode, 102) - t.ok(true, 'pass') - }) - - await t.completed -}) - -test('request with onInfo callback but socket is destroyed before end of response', async (t) => { - t = tspl(t, { plan: 5 }) - const infos = [] - let response - const server = createServer((req, res) => { - response = res - res.writeProcessing() - }) - after(() => server.close()) - - server.listen(0, async () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.destroy()) - try { - await client.request({ - path: '/', - method: 'GET', - onInfo: (x) => { - infos.push(x) - response.destroy() - } - }) - t.fail() - } catch (e) { - t.ok(e) - t.strictEqual(e.message, 'other side closed') - } - t.strictEqual(infos.length, 1) - t.strictEqual(infos[0].statusCode, 102) - t.ok(true, 'pass') - }) - - await t.completed -}) - -test('request onInfo callback headers parsing', async (t) => { - t = tspl(t, { plan: 4 }) - const infos = [] - - const server = net.createServer((socket) => { - const lines = [ - 'HTTP/1.1 103 Early Hints', - 'Link: ; rel=preload; as=style', - '', - 'HTTP/1.1 200 OK', - 'Date: Sat, 09 Oct 2010 14:28:02 GMT', - 'Connection: close', - '', - 'the body' - ] - socket.end(lines.join('\r\n')) - }) - after(() => server.close()) - - await promisify(server.listen.bind(server))(0) - - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.close()) - - const { body } = await client.request({ - path: '/', - method: 'GET', - onInfo: (x) => { infos.push(x) } - }) - await body.dump() - t.strictEqual(infos.length, 1) - t.strictEqual(infos[0].statusCode, 103) - t.deepStrictEqual(infos[0].headers, { link: '; rel=preload; as=style' }) - t.ok(true, 'pass') -}) - -test('request raw responseHeaders', async (t) => { - t = tspl(t, { plan: 4 }) - const infos = [] - - const server = net.createServer((socket) => { - const lines = [ - 'HTTP/1.1 103 Early Hints', - 'Link: ; rel=preload; as=style', - '', - 'HTTP/1.1 200 OK', - 'Date: Sat, 09 Oct 2010 14:28:02 GMT', - 'Connection: close', - '', - 'the body' - ] - socket.end(lines.join('\r\n')) - }) - after(() => server.close()) - - await promisify(server.listen.bind(server))(0) - - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.close()) - - const { body, headers } = await client.request({ - path: '/', - method: 'GET', - responseHeaders: 'raw', - onInfo: (x) => { infos.push(x) } - }) - await body.dump() - t.strictEqual(infos.length, 1) - t.deepStrictEqual(infos[0].headers, ['Link', '; rel=preload; as=style']) - t.deepStrictEqual(headers, ['Date', 'Sat, 09 Oct 2010 14:28:02 GMT', 'Connection', 'close']) - t.ok(true, 'pass') -}) - test('request formData', async (t) => { t = tspl(t, { plan: 1 }) diff --git a/test/client-stream.js b/test/client-stream.js deleted file mode 100644 index ccdbedf1b09..00000000000 --- a/test/client-stream.js +++ /dev/null @@ -1,834 +0,0 @@ -'use strict' - -const { tspl } = require('@matteo.collina/tspl') -const { test, after } = require('node:test') -const { Client, errors } = require('..') -const { createServer } = require('node:http') -const { PassThrough, Writable, Readable } = require('node:stream') -const EE = require('node:events') - -test('stream get', async (t) => { - t = tspl(t, { plan: 9 }) - - const server = createServer((req, res) => { - t.strictEqual('/', req.url) - t.strictEqual('GET', req.method) - t.strictEqual(`localhost:${server.address().port}`, req.headers.host) - res.setHeader('content-type', 'text/plain') - res.end('hello') - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.close()) - - const signal = new EE() - client.stream({ - signal, - path: '/', - method: 'GET', - opaque: new PassThrough() - }, ({ statusCode, headers, opaque: pt }) => { - t.strictEqual(statusCode, 200) - t.strictEqual(headers['content-type'], 'text/plain') - const bufs = [] - pt.on('data', (buf) => { - bufs.push(buf) - }) - pt.on('end', () => { - t.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) - }) - return pt - }, (err) => { - t.strictEqual(signal.listenerCount('abort'), 0) - t.ifError(err) - }) - t.strictEqual(signal.listenerCount('abort'), 1) - }) - - await t.completed -}) - -test('stream promise get', async (t) => { - t = tspl(t, { plan: 6 }) - - const server = createServer((req, res) => { - t.strictEqual('/', req.url) - t.strictEqual('GET', req.method) - t.strictEqual(`localhost:${server.address().port}`, req.headers.host) - res.setHeader('content-type', 'text/plain') - res.end('hello') - }) - after(() => server.close()) - - server.listen(0, async () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.close()) - - await client.stream({ - path: '/', - method: 'GET', - opaque: new PassThrough() - }, ({ statusCode, headers, opaque: pt }) => { - t.strictEqual(statusCode, 200) - t.strictEqual(headers['content-type'], 'text/plain') - const bufs = [] - pt.on('data', (buf) => { - bufs.push(buf) - }) - pt.on('end', () => { - t.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) - }) - return pt - }) - }) - - await t.completed -}) - -test('stream GET destroy res', async (t) => { - t = tspl(t, { plan: 14 }) - - const server = createServer((req, res) => { - t.strictEqual('/', req.url) - t.strictEqual('GET', req.method) - t.strictEqual(`localhost:${server.address().port}`, req.headers.host) - res.setHeader('content-type', 'text/plain') - res.end('hello') - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.close()) - - client.stream({ - path: '/', - method: 'GET' - }, ({ statusCode, headers }) => { - t.strictEqual(statusCode, 200) - t.strictEqual(headers['content-type'], 'text/plain') - - const pt = new PassThrough() - .on('error', (err) => { - t.ok(err) - }) - .on('data', () => { - pt.destroy(new Error('kaboom')) - }) - - return pt - }, (err) => { - t.ok(err) - }) - - client.stream({ - path: '/', - method: 'GET' - }, ({ statusCode, headers }) => { - t.strictEqual(statusCode, 200) - t.strictEqual(headers['content-type'], 'text/plain') - - let ret = '' - const pt = new PassThrough() - pt.on('data', chunk => { - ret += chunk - }).on('end', () => { - t.strictEqual(ret, 'hello') - }) - - return pt - }, (err) => { - t.ifError(err) - }) - }) - - await t.completed -}) - -test('stream GET remote destroy', async (t) => { - t = tspl(t, { plan: 4 }) - - const server = createServer((req, res) => { - res.write('asd') - setImmediate(() => { - res.destroy() - }) - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.close()) - - client.stream({ - path: '/', - method: 'GET' - }, () => { - const pt = new PassThrough() - pt.on('error', (err) => { - t.ok(err) - }) - return pt - }, (err) => { - t.ok(err) - }) - - client.stream({ - path: '/', - method: 'GET' - }, () => { - const pt = new PassThrough() - pt.on('error', (err) => { - t.ok(err) - }) - return pt - }).catch((err) => { - t.ok(err) - }) - }) - - await t.completed -}) - -test('stream response resume back pressure and non standard error', async (t) => { - t = tspl(t, { plan: 5 }) - - const server = createServer((req, res) => { - res.write(Buffer.alloc(1e3)) - setImmediate(() => { - res.write(Buffer.alloc(1e7)) - res.end() - }) - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.close()) - - const pt = new PassThrough() - client.stream({ - path: '/', - method: 'GET' - }, () => { - pt.on('data', () => { - pt.emit('error', new Error('kaboom')) - }).once('error', (err) => { - t.strictEqual(err.message, 'kaboom') - }) - return pt - }, (err) => { - t.ok(err) - t.strictEqual(pt.destroyed, true) - }) - - client.once('disconnect', (err) => { - t.ok(err) - }) - - client.stream({ - path: '/', - method: 'GET' - }, () => { - const pt = new PassThrough() - pt.resume() - return pt - }, (err) => { - t.ifError(err) - }) - }) - - await t.completed -}) - -test('stream waits only for writable side', async (t) => { - t = tspl(t, { plan: 2 }) - - const server = createServer((req, res) => { - res.end(Buffer.alloc(1e3)) - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.close()) - - const pt = new PassThrough({ autoDestroy: false }) - client.stream({ - path: '/', - method: 'GET' - }, () => pt, (err) => { - t.ifError(err) - t.strictEqual(pt.destroyed, false) - }) - }) - - await t.completed -}) - -test('stream args validation', async (t) => { - t = tspl(t, { plan: 3 }) - - const client = new Client('http://localhost:5000') - client.stream({ - path: '/', - method: 'GET' - }, null, (err) => { - t.ok(err instanceof errors.InvalidArgumentError) - }) - - client.stream(null, null, (err) => { - t.ok(err instanceof errors.InvalidArgumentError) - }) - - try { - client.stream(null, null, 'asd') - } catch (err) { - t.ok(err instanceof errors.InvalidArgumentError) - } -}) - -test('stream args validation promise', async (t) => { - t = tspl(t, { plan: 2 }) - - const client = new Client('http://localhost:5000') - client.stream({ - path: '/', - method: 'GET' - }, null).catch((err) => { - t.ok(err instanceof errors.InvalidArgumentError) - }) - - client.stream(null, null).catch((err) => { - t.ok(err instanceof errors.InvalidArgumentError) - }) - - await t.completed -}) - -test('stream destroy if not readable', async (t) => { - t = tspl(t, { plan: 2 }) - - const server = createServer((req, res) => { - res.end() - }) - after(() => server.close()) - - const pt = new PassThrough() - pt.readable = false - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(client.destroy.bind(client)) - - client.stream({ - path: '/', - method: 'GET' - }, () => { - return pt - }, (err) => { - t.ifError(err) - t.strictEqual(pt.destroyed, true) - }) - }) - - await t.completed -}) - -test('stream server side destroy', async (t) => { - t = tspl(t, { plan: 1 }) - - const server = createServer((req, res) => { - res.destroy() - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(client.destroy.bind(client)) - - client.stream({ - path: '/', - method: 'GET' - }, () => { - t.fail() - }, (err) => { - t.ok(err instanceof errors.SocketError) - }) - }) - - await t.completed -}) - -test('stream invalid return', async (t) => { - t = tspl(t, { plan: 1 }) - - const server = createServer((req, res) => { - res.write('asd') - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(client.destroy.bind(client)) - - client.stream({ - path: '/', - method: 'GET' - }, () => { - return {} - }, (err) => { - t.ok(err instanceof errors.InvalidReturnValueError) - }) - }) - - await t.completed -}) - -test('stream body without destroy', async (t) => { - t = tspl(t, { plan: 1 }) - - const server = createServer((req, res) => { - res.end('asd') - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(client.destroy.bind(client)) - - client.stream({ - path: '/', - method: 'GET' - }, () => { - const pt = new PassThrough({ autoDestroy: false }) - pt.destroy = null - pt.resume() - return pt - }, (err) => { - t.ifError(err) - }) - }) - - await t.completed -}) - -test('stream factory abort', async (t) => { - t = tspl(t, { plan: 3 }) - - const server = createServer((req, res) => { - res.end('asd') - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(client.destroy.bind(client)) - - const signal = new EE() - client.stream({ - path: '/', - method: 'GET', - signal - }, () => { - signal.emit('abort') - return new PassThrough() - }, (err) => { - t.strictEqual(signal.listenerCount('abort'), 0) - t.ok(err instanceof errors.RequestAbortedError) - }) - t.strictEqual(signal.listenerCount('abort'), 1) - }) - - await t.completed -}) - -test('stream factory throw', async (t) => { - t = tspl(t, { plan: 3 }) - - const server = createServer((req, res) => { - res.end('asd') - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(client.destroy.bind(client)) - - client.stream({ - path: '/', - method: 'GET' - }, () => { - throw new Error('asd') - }, (err) => { - t.strictEqual(err.message, 'asd') - }) - client.stream({ - path: '/', - method: 'GET' - }, () => { - throw new Error('asd') - }, (err) => { - t.strictEqual(err.message, 'asd') - }) - client.stream({ - path: '/', - method: 'GET' - }, () => { - return new PassThrough() - }, (err) => { - t.ifError(err) - }) - }) - - await t.completed -}) - -test('stream CONNECT throw', async (t) => { - t = tspl(t, { plan: 1 }) - - const server = createServer((req, res) => { - res.end('asd') - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(client.destroy.bind(client)) - - client.stream({ - path: '/', - method: 'CONNECT' - }, () => { - }, (err) => { - t.ok(err instanceof errors.InvalidArgumentError) - }) - }) - - await t.completed -}) - -test('stream abort after complete', async (t) => { - t = tspl(t, { plan: 1 }) - - const server = createServer((req, res) => { - res.end('asd') - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(client.destroy.bind(client)) - - const pt = new PassThrough() - const signal = new EE() - client.stream({ - path: '/', - method: 'GET', - signal - }, () => { - return pt - }, (err) => { - t.ifError(err) - signal.emit('abort') - }) - }) - - await t.completed -}) - -test('stream abort before dispatch', async (t) => { - t = tspl(t, { plan: 1 }) - - const server = createServer((req, res) => { - res.end('asd') - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(client.destroy.bind(client)) - - const pt = new PassThrough() - const signal = new EE() - client.stream({ - path: '/', - method: 'GET', - signal - }, () => { - return pt - }, (err) => { - t.ok(err instanceof errors.RequestAbortedError) - }) - signal.emit('abort') - }) - - await t.completed -}) - -test('trailers', async (t) => { - t = tspl(t, { plan: 2 }) - - const server = createServer((req, res) => { - res.writeHead(200, { Trailer: 'Content-MD5' }) - res.addTrailers({ 'Content-MD5': 'test' }) - res.end() - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.close()) - - client.stream({ - path: '/', - method: 'GET' - }, () => new PassThrough(), (err, data) => { - t.ifError(err) - t.deepStrictEqual(data.trailers, { 'content-md5': 'test' }) - }) - }) - - await t.completed -}) - -test('stream ignore 1xx', async (t) => { - t = tspl(t, { plan: 2 }) - - const server = createServer((req, res) => { - res.writeProcessing() - res.end('hello') - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.close()) - - let buf = '' - client.stream({ - path: '/', - method: 'GET' - }, () => new Writable({ - write (chunk, encoding, callback) { - buf += chunk - callback() - } - }), (err, data) => { - t.ifError(err) - t.strictEqual(buf, 'hello') - }) - }) - - await t.completed -}) - -test('stream ignore 1xx and use onInfo', async (t) => { - t = tspl(t, { plan: 4 }) - - const infos = [] - const server = createServer((req, res) => { - res.writeProcessing() - res.end('hello') - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.close()) - - let buf = '' - client.stream({ - path: '/', - method: 'GET', - onInfo: (x) => { - infos.push(x) - } - }, () => new Writable({ - write (chunk, encoding, callback) { - buf += chunk - callback() - } - }), (err, data) => { - t.ifError(err) - t.strictEqual(buf, 'hello') - t.strictEqual(infos.length, 1) - t.strictEqual(infos[0].statusCode, 102) - }) - }) - - await t.completed -}) - -test('stream backpressure', async (t) => { - t = tspl(t, { plan: 2 }) - - const expected = Buffer.alloc(1e6).toString() - - const server = createServer((req, res) => { - res.writeProcessing() - res.end(expected) - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.close()) - - let buf = '' - client.stream({ - path: '/', - method: 'GET' - }, () => new Writable({ - highWaterMark: 1, - write (chunk, encoding, callback) { - buf += chunk - process.nextTick(callback) - } - }), (err, data) => { - t.ifError(err) - t.strictEqual(buf, expected) - }) - }) - - await t.completed -}) - -test('stream body destroyed on invalid callback', async (t) => { - t = tspl(t, { plan: 1 }) - - const server = createServer((req, res) => { - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(client.destroy.bind(client)) - - const body = new Readable({ - read () { } - }) - try { - client.stream({ - path: '/', - method: 'GET', - body - }, () => { }, null) - } catch (err) { - t.strictEqual(body.destroyed, true) - } - }) - - await t.completed -}) - -test('stream needDrain', async (t) => { - t = tspl(t, { plan: 3 }) - - const server = createServer((req, res) => { - res.end(Buffer.alloc(4096)) - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => { - client.destroy() - }) - - const dst = new PassThrough() - dst.pause() - - if (dst.writableNeedDrain === undefined) { - Object.defineProperty(dst, 'writableNeedDrain', { - get () { - return this._writableState.needDrain - } - }) - } - - while (dst.write(Buffer.alloc(4096))) { - // Do nothing. - } - - const orgWrite = dst.write - dst.write = () => t.fail() - const p = client.stream({ - path: '/', - method: 'GET' - }, () => { - t.strictEqual(dst._writableState.needDrain, true) - t.strictEqual(dst.writableNeedDrain, true) - - setImmediate(() => { - dst.write = (...args) => { - orgWrite.call(dst, ...args) - } - dst.resume() - }) - - return dst - }) - - p.then(() => { - t.ok(true, 'pass') - }) - }) - - await t.completed -}) - -test('stream legacy needDrain', async (t) => { - t = tspl(t, { plan: 3 }) - - const server = createServer((req, res) => { - res.end(Buffer.alloc(4096)) - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => { - client.destroy() - }) - - const dst = new PassThrough() - dst.pause() - - if (dst.writableNeedDrain !== undefined) { - Object.defineProperty(dst, 'writableNeedDrain', { - get () { - } - }) - } - - while (dst.write(Buffer.alloc(4096))) { - // Do nothing - } - - const orgWrite = dst.write - dst.write = () => t.fail() - const p = client.stream({ - path: '/', - method: 'GET' - }, () => { - t.strictEqual(dst._writableState.needDrain, true) - t.strictEqual(dst.writableNeedDrain, undefined) - - setImmediate(() => { - dst.write = (...args) => { - orgWrite.call(dst, ...args) - } - dst.resume() - }) - - return dst - }) - - p.then(() => { - t.ok(true, 'pass') - }) - }) - await t.completed -}) diff --git a/test/client-upgrade.js b/test/client-upgrade.js deleted file mode 100644 index 5cf5e553ba7..00000000000 --- a/test/client-upgrade.js +++ /dev/null @@ -1,473 +0,0 @@ -'use strict' - -const { tspl } = require('@matteo.collina/tspl') -const { test, after } = require('node:test') -const { Client, errors } = require('..') -const net = require('node:net') -const http = require('node:http') -const EE = require('node:events') -const { kBusy } = require('../lib/core/symbols') - -test('basic upgrade', async (t) => { - t = tspl(t, { plan: 6 }) - - const server = net.createServer((c) => { - c.on('data', (d) => { - t.ok(/upgrade: websocket/i.test(d)) - c.write('HTTP/1.1 101\r\n') - c.write('hello: world\r\n') - c.write('connection: upgrade\r\n') - c.write('upgrade: websocket\r\n') - c.write('\r\n') - c.write('Body') - }) - - c.on('end', () => { - c.end() - }) - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.close()) - - const signal = new EE() - client.upgrade({ - signal, - path: '/', - method: 'GET', - protocol: 'Websocket' - }, (err, data) => { - t.ifError(err) - - t.strictEqual(signal.listenerCount('abort'), 0) - - const { headers, socket } = data - - let recvData = '' - data.socket.on('data', (d) => { - recvData += d - }) - - socket.on('close', () => { - t.strictEqual(recvData.toString(), 'Body') - }) - - t.deepStrictEqual(headers, { - hello: 'world', - connection: 'upgrade', - upgrade: 'websocket' - }) - socket.end() - }) - t.strictEqual(signal.listenerCount('abort'), 1) - }) - - await t.completed -}) - -test('basic upgrade promise', async (t) => { - t = tspl(t, { plan: 2 }) - - const server = net.createServer((c) => { - c.on('data', (d) => { - c.write('HTTP/1.1 101\r\n') - c.write('hello: world\r\n') - c.write('connection: upgrade\r\n') - c.write('upgrade: websocket\r\n') - c.write('\r\n') - c.write('Body') - }) - - c.on('end', () => { - c.end() - }) - }) - after(() => server.close()) - - server.listen(0, async () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.close()) - - const { headers, socket } = await client.upgrade({ - path: '/', - method: 'GET', - protocol: 'Websocket' - }) - - let recvData = '' - socket.on('data', (d) => { - recvData += d - }) - - socket.on('close', () => { - t.strictEqual(recvData.toString(), 'Body') - }) - - t.deepStrictEqual(headers, { - hello: 'world', - connection: 'upgrade', - upgrade: 'websocket' - }) - socket.end() - }) - - await t.completed -}) - -test('upgrade error', async (t) => { - t = tspl(t, { plan: 1 }) - - const server = net.createServer((c) => { - c.on('data', (d) => { - c.write('HTTP/1.1 101\r\n') - c.write('hello: world\r\n') - c.write('connection: upgrade\r\n') - c.write('\r\n') - c.write('Body') - }) - c.on('error', () => { - // Whether we get an error, end or close is undefined. - // Ignore error. - }) - }) - after(() => server.close()) - - server.listen(0, async () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.close()) - - try { - await client.upgrade({ - path: '/', - method: 'GET', - protocol: 'Websocket' - }) - } catch (err) { - t.ok(err) - } - }) - - await t.completed -}) - -test('upgrade invalid opts', async (t) => { - t = tspl(t, { plan: 6 }) - - const client = new Client('http://localhost:5432') - - client.upgrade(null, err => { - t.ok(err instanceof errors.InvalidArgumentError) - t.strictEqual(err.message, 'invalid opts') - }) - - try { - client.upgrade(null, null) - t.fail() - } catch (err) { - t.ok(err instanceof errors.InvalidArgumentError) - t.strictEqual(err.message, 'invalid opts') - } - - try { - client.upgrade({ path: '/' }, null) - t.fail() - } catch (err) { - t.ok(err instanceof errors.InvalidArgumentError) - t.strictEqual(err.message, 'invalid callback') - } -}) - -test('basic upgrade2', async (t) => { - t = tspl(t, { plan: 3 }) - - const server = http.createServer() - server.on('upgrade', (req, c, head) => { - c.write('HTTP/1.1 101\r\n') - c.write('hello: world\r\n') - c.write('connection: upgrade\r\n') - c.write('upgrade: websocket\r\n') - c.write('\r\n') - c.write('Body') - c.end() - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.close()) - - client.upgrade({ - path: '/', - method: 'GET', - protocol: 'Websocket' - }, (err, data) => { - t.ifError(err) - - const { headers, socket } = data - - let recvData = '' - data.socket.on('data', (d) => { - recvData += d - }) - - socket.on('close', () => { - t.strictEqual(recvData.toString(), 'Body') - }) - - t.deepStrictEqual(headers, { - hello: 'world', - connection: 'upgrade', - upgrade: 'websocket' - }) - socket.end() - }) - }) - - await t.completed -}) - -test('upgrade wait for empty pipeline', async (t) => { - t = tspl(t, { plan: 7 }) - - let canConnect = false - const server = http.createServer((req, res) => { - res.end() - canConnect = true - }) - server.on('upgrade', (req, c, firstBodyChunk) => { - t.strictEqual(canConnect, true) - c.write('HTTP/1.1 101\r\n') - c.write('hello: world\r\n') - c.write('connection: upgrade\r\n') - c.write('upgrade: websocket\r\n') - c.write('\r\n') - c.write('Body') - c.end() - }) - after(() => server.close()) - - server.listen(0, async () => { - const client = new Client(`http://localhost:${server.address().port}`, { - pipelining: 3 - }) - after(() => client.close()) - - client.request({ - path: '/', - method: 'GET' - }, (err) => { - t.ifError(err) - }) - client.once('connect', () => { - process.nextTick(() => { - t.strictEqual(client[kBusy], false) - - client.upgrade({ - path: '/' - }, (err, { socket }) => { - t.ifError(err) - let recvData = '' - socket.on('data', (d) => { - recvData += d - }) - - socket.on('end', () => { - t.strictEqual(recvData.toString(), 'Body') - }) - - socket.write('Body') - socket.end() - }) - t.strictEqual(client[kBusy], true) - - client.request({ - path: '/', - method: 'GET' - }, (err) => { - t.ifError(err) - }) - }) - }) - }) - - await t.completed -}) - -test('upgrade aborted', async (t) => { - t = tspl(t, { plan: 6 }) - - const server = http.createServer((req, res) => { - t.fail() - }) - server.on('upgrade', (req, c, firstBodyChunk) => { - t.fail() - }) - after(() => server.close()) - - server.listen(0, async () => { - const client = new Client(`http://localhost:${server.address().port}`, { - pipelining: 3 - }) - after(() => client.destroy()) - - const signal = new EE() - client.upgrade({ - path: '/', - signal, - opaque: 'asd' - }, (err, { opaque }) => { - t.strictEqual(opaque, 'asd') - t.ok(err instanceof errors.RequestAbortedError) - t.strictEqual(signal.listenerCount('abort'), 0) - }) - t.strictEqual(client[kBusy], true) - t.strictEqual(signal.listenerCount('abort'), 1) - signal.emit('abort') - - client.close(() => { - t.ok(true, 'pass') - }) - }) - - await t.completed -}) - -test('basic aborted after res', async (t) => { - t = tspl(t, { plan: 1 }) - - const signal = new EE() - const server = http.createServer() - server.on('upgrade', (req, c, head) => { - c.write('HTTP/1.1 101\r\n') - c.write('hello: world\r\n') - c.write('connection: upgrade\r\n') - c.write('upgrade: websocket\r\n') - c.write('\r\n') - c.write('Body') - c.end() - c.on('error', () => { - - }) - signal.emit('abort') - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.close()) - - client.upgrade({ - path: '/', - method: 'GET', - protocol: 'Websocket', - signal - }, (err) => { - t.ok(err instanceof errors.RequestAbortedError) - }) - }) - - await t.completed -}) - -test('basic upgrade error', async (t) => { - t = tspl(t, { plan: 2 }) - - const server = net.createServer((c) => { - c.on('data', (d) => { - c.write('HTTP/1.1 101\r\n') - c.write('hello: world\r\n') - c.write('connection: upgrade\r\n') - c.write('upgrade: websocket\r\n') - c.write('\r\n') - c.write('Body') - }) - c.on('error', () => { - - }) - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.close()) - - const _err = new Error() - client.upgrade({ - path: '/', - method: 'GET', - protocol: 'Websocket' - }, (err, data) => { - t.ifError(err) - data.socket.on('error', (err) => { - t.strictEqual(err, _err) - }) - throw _err - }) - }) - - await t.completed -}) - -test('upgrade disconnect', async (t) => { - t = tspl(t, { plan: 3 }) - - const server = net.createServer(connection => { - connection.destroy() - }) - - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.close()) - - client.on('disconnect', (origin, [self], error) => { - t.strictEqual(client, self) - t.ok(error instanceof Error) - }) - - client - .upgrade({ path: '/', method: 'GET' }) - .then(() => { - t.fail() - }) - .catch(error => { - t.ok(error instanceof Error) - }) - }) - - await t.completed -}) - -test('upgrade invalid signal', async (t) => { - t = tspl(t, { plan: 2 }) - - const server = net.createServer(() => { - t.fail() - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.destroy()) - - client.on('disconnect', () => { - t.fail() - }) - - client.upgrade({ - path: '/', - method: 'GET', - protocol: 'Websocket', - signal: 'error', - opaque: 'asd' - }, (err, { opaque }) => { - t.strictEqual(opaque, 'asd') - t.ok(err instanceof errors.InvalidArgumentError) - }) - }) - - await t.completed -}) diff --git a/test/client.js b/test/client.js index 62f2c10dc7a..7155e503eef 100644 --- a/test/client.js +++ b/test/client.js @@ -3,7 +3,7 @@ const { tspl } = require('@matteo.collina/tspl') const { readFileSync, createReadStream } = require('node:fs') const { createServer } = require('node:http') -const { Readable, PassThrough } = require('node:stream') +const { Readable } = require('node:stream') const { test, after } = require('node:test') const { Client, errors } = require('..') const { kSocket } = require('../lib/core/symbols') @@ -350,40 +350,6 @@ test('using throwOnError should throw (request)', async (t) => { await t.completed }) -test('using throwOnError should throw (stream)', async (t) => { - t = tspl(t, { plan: 2 }) - - const server = createServer((req, res) => { - res.statusCode = 400 - res.end('hello') - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`, { - keepAliveTimeout: 300e3 - }) - after(() => client.close()) - - client.stream({ - path: '/', - method: 'GET', - throwOnError: true, - opaque: new PassThrough() - }, ({ opaque: pt }) => { - pt.on('data', () => { - t.fail() - }) - return pt - }, err => { - t.strictEqual(err.message, 'invalid throwOnError') - t.strictEqual(err.code, 'UND_ERR_INVALID_ARG') - }) - }) - - await t.completed -}) - test('basic head', async (t) => { t = tspl(t, { plan: 14 }) diff --git a/test/http2.js b/test/http2.js index a43700574b8..82d37dd1b26 100644 --- a/test/http2.js +++ b/test/http2.js @@ -6,7 +6,7 @@ const { createSecureServer } = require('node:http2') const { createReadStream, readFileSync } = require('node:fs') const { once } = require('node:events') const { Blob } = require('node:buffer') -const { Writable, pipeline, PassThrough, Readable } = require('node:stream') +const { Readable } = require('node:stream') const pem = require('https-pem') @@ -471,206 +471,6 @@ test('Should handle h2 continue', async t => { t.strictEqual(Buffer.concat(responseBody).toString('utf-8'), 'hello h2!') }) -test('Dispatcher#Stream', async t => { - const server = createSecureServer(pem) - const expectedBody = 'hello from client!' - const bufs = [] - let requestBody = '' - - server.on('stream', async (stream, headers) => { - stream.setEncoding('utf-8') - stream.on('data', chunk => { - requestBody += chunk - }) - - stream.respond({ ':status': 200, 'x-custom': 'custom-header' }) - stream.end('hello h2!') - }) - - t = tspl(t, { plan: 4 }) - - server.listen(0, async () => { - const client = new Client(`https://localhost:${server.address().port}`, { - connect: { - rejectUnauthorized: false - }, - allowH2: true - }) - - after(() => server.close()) - after(() => client.close()) - - await client.stream( - { path: '/', opaque: { bufs }, method: 'POST', body: expectedBody }, - ({ statusCode, headers, opaque: { bufs } }) => { - t.strictEqual(statusCode, 200) - t.strictEqual(headers['x-custom'], 'custom-header') - - return new Writable({ - write (chunk, _encoding, cb) { - bufs.push(chunk) - cb() - } - }) - } - ) - - t.strictEqual(Buffer.concat(bufs).toString('utf-8'), 'hello h2!') - t.strictEqual(requestBody, expectedBody) - }) - - await t.completed -}) - -test('Dispatcher#Pipeline', async t => { - const server = createSecureServer(pem) - const expectedBody = 'hello from client!' - const bufs = [] - let requestBody = '' - - server.on('stream', async (stream, headers) => { - stream.setEncoding('utf-8') - stream.on('data', chunk => { - requestBody += chunk - }) - - stream.respond({ ':status': 200, 'x-custom': 'custom-header' }) - stream.end('hello h2!') - }) - - t = tspl(t, { plan: 5 }) - - server.listen(0, () => { - const client = new Client(`https://localhost:${server.address().port}`, { - connect: { - rejectUnauthorized: false - }, - allowH2: true - }) - - after(() => server.close()) - after(() => client.close()) - - pipeline( - new Readable({ - read () { - this.push(Buffer.from(expectedBody)) - this.push(null) - } - }), - client.pipeline( - { path: '/', method: 'POST', body: expectedBody }, - ({ statusCode, headers, body }) => { - t.strictEqual(statusCode, 200) - t.strictEqual(headers['x-custom'], 'custom-header') - - return pipeline(body, new PassThrough(), () => {}) - } - ), - new Writable({ - write (chunk, _, cb) { - bufs.push(chunk) - cb() - } - }), - err => { - t.ifError(err) - t.strictEqual(Buffer.concat(bufs).toString('utf-8'), 'hello h2!') - t.strictEqual(requestBody, expectedBody) - } - ) - }) - - await t.completed -}) - -test('Dispatcher#Connect', async t => { - const server = createSecureServer(pem) - const expectedBody = 'hello from client!' - let requestBody = '' - - server.on('stream', async (stream, headers) => { - stream.setEncoding('utf-8') - stream.on('data', chunk => { - requestBody += chunk - }) - - stream.respond({ ':status': 200, 'x-custom': 'custom-header' }) - stream.end('hello h2!') - }) - - t = tspl(t, { plan: 6 }) - - server.listen(0, () => { - const client = new Client(`https://localhost:${server.address().port}`, { - connect: { - rejectUnauthorized: false - }, - allowH2: true - }) - - after(() => server.close()) - after(() => client.close()) - - let result = '' - client.connect({ path: '/' }, (err, { socket }) => { - t.ifError(err) - socket.on('data', chunk => { - result += chunk - }) - socket.on('response', headers => { - t.strictEqual(headers[':status'], 200) - t.strictEqual(headers['x-custom'], 'custom-header') - t.strictEqual(socket.closed, false) - }) - - // We need to handle the error event although - // is not controlled by Undici, the fact that a session - // is destroyed and destroys subsequent streams, causes - // unhandled errors to surface if not handling this event. - socket.on('error', () => {}) - - socket.once('end', () => { - t.strictEqual(requestBody, expectedBody) - t.strictEqual(result, 'hello h2!') - }) - socket.end(expectedBody) - }) - }) - - await t.completed -}) - -test('Dispatcher#Upgrade', async t => { - const server = createSecureServer(pem) - - server.on('stream', async (stream, headers) => { - stream.end() - }) - - t = tspl(t, { plan: 1 }) - - server.listen(0, async () => { - const client = new Client(`https://localhost:${server.address().port}`, { - connect: { - rejectUnauthorized: false - }, - allowH2: true - }) - - after(() => server.close()) - after(() => client.close()) - - try { - await client.upgrade({ path: '/' }) - } catch (error) { - t.strictEqual(error.message, 'Upgrade not supported for H2') - } - }) - - await t.completed -}) - test('Dispatcher#destroy', async t => { const promises = [] const server = createSecureServer(pem) diff --git a/test/issue-2349.js b/test/issue-2349.js deleted file mode 100644 index fa308227849..00000000000 --- a/test/issue-2349.js +++ /dev/null @@ -1,44 +0,0 @@ -'use strict' - -const { test } = require('node:test') -const { rejects } = require('node:assert') -const { Writable } = require('node:stream') -const { MockAgent, stream } = require('..') - -test('stream() does not fail after request has been aborted', () => { - const mockAgent = new MockAgent() - - mockAgent.disableNetConnect() - mockAgent - .get('http://localhost:3333') - .intercept({ - path: '/' - }) - .reply(200, 'ok') - .delay(10) - - const parts = [] - const ac = new AbortController() - - setTimeout(() => ac.abort(), 5) - - rejects( - stream( - 'http://localhost:3333/', - { - opaque: { parts }, - signal: ac.signal, - dispatcher: mockAgent - }, - ({ opaque: { parts } }) => { - return new Writable({ - write (chunk, _encoding, callback) { - parts.push(chunk) - callback() - } - }) - } - ), - new DOMException('This operation was aborted', 'AbortError') - ) -}) diff --git a/test/node-test/agent.js b/test/node-test/agent.js index c5238d7b570..b99e5a86f5d 100644 --- a/test/node-test/agent.js +++ b/test/node-test/agent.js @@ -9,8 +9,6 @@ const { Agent, errors, request, - stream, - pipeline, Pool, setGlobalDispatcher, getGlobalDispatcher @@ -532,14 +530,6 @@ test('with a local agent', async t => { await p.completed }) -test('stream: fails with invalid URL', t => { - const p = tspl(t, { plan: 4 }) - p.throws(() => stream(), errors.InvalidArgumentError, 'throws on missing url argument') - p.throws(() => stream(''), errors.InvalidArgumentError, 'throws on invalid url') - p.throws(() => stream({}), errors.InvalidArgumentError, 'throws on missing url.origin argument') - p.throws(() => stream({ origin: '' }), errors.InvalidArgumentError, 'throws on invalid url.origin argument') -}) - test('with globalAgent', async t => { const p = tspl(t, { plan: 6 }) const wanted = 'payload' @@ -624,43 +614,6 @@ test('with a local agent', async t => { await p.completed }) -test('pipeline: fails with invalid URL', t => { - const p = tspl(t, { plan: 4 }) - p.throws(() => pipeline(), errors.InvalidArgumentError, 'throws on missing url argument') - p.throws(() => pipeline(''), errors.InvalidArgumentError, 'throws on invalid url') - p.throws(() => pipeline({}), errors.InvalidArgumentError, 'throws on missing url.origin argument') - p.throws(() => pipeline({ origin: '' }), errors.InvalidArgumentError, 'throws on invalid url.origin argument') -}) - -test('pipeline: fails with invalid onInfo', async (t) => { - const p = tspl(t, { plan: 2 }) - pipeline({ origin: 'http://localhost', path: '/', onInfo: 'foo' }, () => {}).on('error', (err) => { - p.ok(err instanceof errors.InvalidArgumentError) - p.equal(err.message, 'invalid onInfo callback') - }) - await p.completed -}) - -test('request: fails with invalid onInfo', async (t) => { - try { - await request({ origin: 'http://localhost', path: '/', onInfo: 'foo' }) - assert.fail('should throw') - } catch (e) { - assert.ok(e) - assert.strictEqual(e.message, 'invalid onInfo callback') - } -}) - -test('stream: fails with invalid onInfo', async (t) => { - try { - await stream({ origin: 'http://localhost', path: '/', onInfo: 'foo' }, () => new PassThrough()) - assert.fail('should throw') - } catch (e) { - assert.ok(e) - assert.strictEqual(e.message, 'invalid onInfo callback') - } -}) - test('constructor validations', t => { const p = tspl(t, { plan: 1 }) p.throws(() => new Agent({ factory: 'ASD' }), errors.InvalidArgumentError, 'throws on invalid opts argument') diff --git a/test/node-test/async_hooks.js b/test/node-test/async_hooks.js index 63d42dd1082..9147b3b10b6 100644 --- a/test/node-test/async_hooks.js +++ b/test/node-test/async_hooks.js @@ -183,32 +183,3 @@ test('async hooks client is destroyed', async (t) => { await p.completed }) - -test('async hooks pipeline handler', async (t) => { - const p = tspl(t, { plan: 2 }) - - const server = createServer((req, res) => { - res.end('hello') - }) - t.after(closeServerAsPromise(server)) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - t.after(() => { return client.close() }) - - setCurrentTransaction({ hello: 'world2' }) - - client - .pipeline({ path: '/', method: 'GET' }, ({ body }) => { - p.deepStrictEqual(getCurrentTransaction(), { hello: 'world2' }) - return body - }) - .on('close', () => { - p.ok(1) - }) - .resume() - .end() - }) - - await p.completed -}) diff --git a/test/node-test/client-connect.js b/test/node-test/client-connect.js deleted file mode 100644 index 0bf65488d81..00000000000 --- a/test/node-test/client-connect.js +++ /dev/null @@ -1,293 +0,0 @@ -'use strict' - -const { test } = require('node:test') -const { Client, errors } = require('../..') -const http = require('node:http') -const EE = require('node:events') -const { kBusy } = require('../../lib/core/symbols') -const { tspl } = require('@matteo.collina/tspl') -const { closeServerAsPromise } = require('../utils/node-http') - -test('basic connect', async (t) => { - const p = tspl(t, { plan: 3 }) - - const server = http.createServer((c) => { - p.ok(0) - }) - server.on('connect', (req, socket, firstBodyChunk) => { - socket.write('HTTP/1.1 200 Connection established\r\n\r\n') - - let data = firstBodyChunk.toString() - socket.on('data', (buf) => { - data += buf.toString() - }) - - socket.on('end', () => { - socket.end(data) - }) - }) - t.after(closeServerAsPromise(server)) - - server.listen(0, async () => { - const client = new Client(`http://localhost:${server.address().port}`) - t.after(() => { return client.close() }) - - const signal = new EE() - const promise = client.connect({ - signal, - path: '/' - }) - p.strictEqual(signal.listenerCount('abort'), 1) - const { socket } = await promise - p.strictEqual(signal.listenerCount('abort'), 0) - - let recvData = '' - socket.on('data', (d) => { - recvData += d - }) - - socket.on('end', () => { - p.strictEqual(recvData.toString(), 'Body') - }) - - socket.write('Body') - socket.end() - }) - - await p.completed -}) - -test('connect error', async (t) => { - const p = tspl(t, { plan: 1 }) - - const server = http.createServer((c) => { - p.ok(0) - }) - server.on('connect', (req, socket, firstBodyChunk) => { - socket.destroy() - }) - t.after(closeServerAsPromise(server)) - - server.listen(0, async () => { - const client = new Client(`http://localhost:${server.address().port}`) - t.after(() => { return client.close() }) - - try { - await client.connect({ - path: '/' - }) - } catch (err) { - p.ok(err) - } - }) - - await p.completed -}) - -test('connect invalid opts', (t) => { - const p = tspl(t, { plan: 6 }) - - const client = new Client('http://localhost:5432') - - client.connect(null, err => { - p.ok(err instanceof errors.InvalidArgumentError) - p.strictEqual(err.message, 'invalid opts') - }) - - try { - client.connect(null, null) - p.ok(0) - } catch (err) { - p.ok(err instanceof errors.InvalidArgumentError) - p.strictEqual(err.message, 'invalid opts') - } - - try { - client.connect({ path: '/' }, null) - p.ok(0) - } catch (err) { - p.ok(err instanceof errors.InvalidArgumentError) - p.strictEqual(err.message, 'invalid callback') - } -}) - -test('connect wait for empty pipeline', async (t) => { - const p = tspl(t, { plan: 7 }) - - let canConnect = false - const server = http.createServer((req, res) => { - res.end() - canConnect = true - }) - server.on('connect', (req, socket, firstBodyChunk) => { - p.strictEqual(canConnect, true) - socket.write('HTTP/1.1 200 Connection established\r\n\r\n') - - let data = firstBodyChunk.toString() - socket.on('data', (buf) => { - data += buf.toString() - }) - - socket.on('end', () => { - socket.end(data) - }) - }) - t.after(closeServerAsPromise(server)) - - server.listen(0, async () => { - const client = new Client(`http://localhost:${server.address().port}`, { - pipelining: 3 - }) - t.after(() => { return client.close() }) - - client.request({ - path: '/', - method: 'GET' - }, (err) => { - p.ifError(err) - }) - client.once('connect', () => { - process.nextTick(() => { - p.strictEqual(client[kBusy], false) - - client.connect({ - path: '/' - }, (err, { socket }) => { - p.ifError(err) - let recvData = '' - socket.on('data', (d) => { - recvData += d - }) - - socket.on('end', () => { - p.strictEqual(recvData.toString(), 'Body') - }) - - socket.write('Body') - socket.end() - }) - p.strictEqual(client[kBusy], true) - - client.request({ - path: '/', - method: 'GET' - }, (err) => { - p.ifError(err) - }) - }) - }) - }) - await p.completed -}) - -test('connect aborted', async (t) => { - const p = tspl(t, { plan: 6 }) - - const server = http.createServer((req, res) => { - p.ok(0) - }) - server.on('connect', (req, c, firstBodyChunk) => { - p.ok(0) - }) - t.after(closeServerAsPromise(server)) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`, { - pipelining: 3 - }) - t.after(() => { - client.destroy() - }) - - const signal = new EE() - client.connect({ - path: '/', - signal, - opaque: 'asd' - }, (err, { opaque }) => { - p.strictEqual(opaque, 'asd') - p.strictEqual(signal.listenerCount('abort'), 0) - p.ok(err instanceof errors.RequestAbortedError) - }) - p.strictEqual(client[kBusy], true) - p.strictEqual(signal.listenerCount('abort'), 1) - signal.emit('abort') - - client.close(() => { - p.ok(1) - }) - }) - - await p.completed -}) - -test('basic connect error', async (t) => { - const p = tspl(t, { plan: 2 }) - - const server = http.createServer((c) => { - p.ok(0) - }) - server.on('connect', (req, socket, firstBodyChunk) => { - socket.write('HTTP/1.1 200 Connection established\r\n\r\n') - - let data = firstBodyChunk.toString() - socket.on('data', (buf) => { - data += buf.toString() - }) - - socket.on('end', () => { - socket.end(data) - }) - }) - t.after(closeServerAsPromise(server)) - - server.listen(0, async () => { - const client = new Client(`http://localhost:${server.address().port}`) - t.after(() => { return client.close() }) - - const _err = new Error() - client.connect({ - path: '/' - }, (err, { socket }) => { - p.ifError(err) - socket.on('error', (err) => { - p.strictEqual(err, _err) - }) - throw _err - }) - }) - - await p.completed -}) - -test('connect invalid signal', async (t) => { - const p = tspl(t, { plan: 2 }) - - const server = http.createServer((req, res) => { - p.ok(0) - }) - server.on('connect', (req, c, firstBodyChunk) => { - p.ok(0) - }) - t.after(closeServerAsPromise(server)) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - t.after(client.destroy.bind(client)) - - client.on('disconnect', () => { - p.ok(0) - }) - - client.connect({ - path: '/', - signal: 'error', - opaque: 'asd' - }, (err, { opaque }) => { - p.strictEqual(opaque, 'asd') - p.ok(err instanceof errors.InvalidArgumentError) - }) - }) - - await p.completed -}) diff --git a/test/node-test/client-errors.js b/test/node-test/client-errors.js index d53e9ffc146..f58c9a5c4e8 100644 --- a/test/node-test/client-errors.js +++ b/test/node-test/client-errors.js @@ -1056,15 +1056,12 @@ test('retry idempotent inflight', async (t) => { }) test('invalid opts', async (t) => { - const p = tspl(t, { plan: 5 }) + const p = tspl(t, { plan: 4 }) const client = new Client('http://localhost:5000') client.request(null, (err) => { p.ok(err instanceof errors.InvalidArgumentError) }) - client.pipeline(null).on('error', (err) => { - p.ok(err instanceof errors.InvalidArgumentError) - }) client.request({ path: '/', method: 'GET', @@ -1155,7 +1152,7 @@ test('CONNECT throws in next tick', async (t) => { }) test('invalid signal', async (t) => { - const p = tspl(t, { plan: 8 }) + const p = tspl(t, { plan: 3 }) const client = new Client('http://localhost:3333') t.after(client.destroy.bind(client)) @@ -1166,16 +1163,6 @@ test('invalid signal', async (t) => { p.strictEqual(opaque, 'asd') p.ok(err instanceof errors.InvalidArgumentError) }) - client.pipeline({ path: '/', method: 'GET', signal: {} }, () => {}) - .on('error', (err) => { - p.strictEqual(ticked, true) - p.ok(err instanceof errors.InvalidArgumentError) - }) - client.stream({ path: '/', method: 'GET', signal: {}, opaque: 'asd' }, () => {}, (err, { opaque }) => { - p.strictEqual(ticked, true) - p.strictEqual(opaque, 'asd') - p.ok(err instanceof errors.InvalidArgumentError) - }) ticked = true await p.completed diff --git a/test/pipeline-pipelining.js b/test/pipeline-pipelining.js deleted file mode 100644 index b244925786a..00000000000 --- a/test/pipeline-pipelining.js +++ /dev/null @@ -1,113 +0,0 @@ -'use strict' - -const { tspl } = require('@matteo.collina/tspl') -const { test, after } = require('node:test') -const { Client } = require('..') -const { createServer } = require('node:http') -const { kConnect } = require('../lib/core/symbols') -const { kBusy, kPending, kRunning } = require('../lib/core/symbols') - -test('pipeline pipelining', async (t) => { - t = tspl(t, { plan: 10 }) - - const server = createServer((req, res) => { - t.deepStrictEqual(req.headers['transfer-encoding'], undefined) - res.end() - }) - - after(() => server.close()) - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`, { - pipelining: 2 - }) - after(() => client.close()) - - client[kConnect](() => { - t.equal(client[kRunning], 0) - client.pipeline({ - method: 'GET', - path: '/' - }, ({ body }) => body).end().resume() - t.equal(client[kBusy], true) - t.deepStrictEqual(client[kRunning], 0) - t.deepStrictEqual(client[kPending], 1) - - client.pipeline({ - method: 'GET', - path: '/' - }, ({ body }) => body).end().resume() - t.equal(client[kBusy], true) - t.deepStrictEqual(client[kRunning], 0) - t.deepStrictEqual(client[kPending], 2) - process.nextTick(() => { - t.equal(client[kRunning], 2) - }) - }) - }) - - await t.completed -}) - -test('pipeline pipelining retry', async (t) => { - t = tspl(t, { plan: 13 }) - - let count = 0 - const server = createServer((req, res) => { - if (count++ === 0) { - res.destroy() - } else { - res.end() - } - }) - - after(() => server.close()) - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`, { - pipelining: 3 - }) - after(() => client.destroy()) - - client.once('disconnect', () => { - t.ok(true, 'pass') - }) - - client[kConnect](() => { - client.pipeline({ - method: 'GET', - path: '/' - }, ({ body }) => body).end().resume() - .on('error', (err) => { - t.ok(err) - }) - t.equal(client[kBusy], true) - t.deepStrictEqual(client[kRunning], 0) - t.deepStrictEqual(client[kPending], 1) - - client.pipeline({ - method: 'GET', - path: '/' - }, ({ body }) => body).end().resume() - t.equal(client[kBusy], true) - t.deepStrictEqual(client[kRunning], 0) - t.deepStrictEqual(client[kPending], 2) - - client.pipeline({ - method: 'GET', - path: '/' - }, ({ body }) => body).end().resume() - t.equal(client[kBusy], true) - t.deepStrictEqual(client[kRunning], 0) - t.deepStrictEqual(client[kPending], 3) - - process.nextTick(() => { - t.equal(client[kRunning], 3) - }) - - client.close(() => { - t.ok(true, 'pass') - }) - }) - }) - - await t.completed -}) diff --git a/test/pool.js b/test/pool.js index b75cd530d43..488009ef020 100644 --- a/test/pool.js +++ b/test/pool.js @@ -5,11 +5,7 @@ const { test, after } = require('node:test') const { EventEmitter } = require('node:events') const { createServer } = require('node:http') const net = require('node:net') -const { - finished, - PassThrough, - Readable -} = require('node:stream') +const { finished, Readable } = require('node:stream') const { promisify } = require('node:util') const { kBusy, @@ -264,86 +260,6 @@ test('basic get with async/await', async (t) => { await client.destroy() }) -test('stream get async/await', async (t) => { - t = tspl(t, { plan: 4 }) - - const server = createServer((req, res) => { - t.strictEqual('/', req.url) - t.strictEqual('GET', req.method) - res.setHeader('content-type', 'text/plain') - res.end('hello') - }) - after(() => server.close()) - - await promisify(server.listen.bind(server))(0) - const client = new Pool(`http://localhost:${server.address().port}`) - after(() => client.destroy()) - - await client.stream({ path: '/', method: 'GET' }, ({ statusCode, headers }) => { - t.strictEqual(statusCode, 200) - t.strictEqual(headers['content-type'], 'text/plain') - return new PassThrough() - }) - - await t.completed -}) - -test('stream get error async/await', async (t) => { - t = tspl(t, { plan: 1 }) - - const server = createServer((req, res) => { - res.destroy() - }) - after(() => server.close()) - - server.listen(0, async () => { - const client = new Pool(`http://localhost:${server.address().port}`) - after(() => client.destroy()) - - await client.stream({ path: '/', method: 'GET' }, () => { - - }) - .catch((err) => { - t.ok(err) - }) - }) - - await t.completed -}) - -test('pipeline get', async (t) => { - t = tspl(t, { plan: 5 }) - - const server = createServer((req, res) => { - t.strictEqual('/', req.url) - t.strictEqual('GET', req.method) - res.setHeader('content-type', 'text/plain') - res.end('hello') - }) - after(() => server.close()) - - server.listen(0, async () => { - const client = new Pool(`http://localhost:${server.address().port}`) - after(() => client.destroy()) - - const bufs = [] - client.pipeline({ path: '/', method: 'GET' }, ({ statusCode, headers, body }) => { - t.strictEqual(statusCode, 200) - t.strictEqual(headers['content-type'], 'text/plain') - return body - }) - .end() - .on('data', (buf) => { - bufs.push(buf) - }) - .on('end', () => { - t.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) - }) - }) - - await t.completed -}) - test('backpressure algorithm', async (t) => { t = tspl(t, { plan: 12 }) @@ -485,99 +401,6 @@ test('invalid pool dispatch options', async (t) => { t.throws(() => pool.dispatch({}, {}), errors.InvalidArgumentError, 'throws on invalid handler') }) -test('pool upgrade promise', async (t) => { - t = tspl(t, { plan: 2 }) - - const server = net.createServer((c) => { - c.on('data', (d) => { - c.write('HTTP/1.1 101\r\n') - c.write('hello: world\r\n') - c.write('connection: upgrade\r\n') - c.write('upgrade: websocket\r\n') - c.write('\r\n') - c.write('Body') - }) - - c.on('end', () => { - c.end() - }) - }) - after(() => server.close()) - - server.listen(0, async () => { - const client = new Pool(`http://localhost:${server.address().port}`) - after(() => client.close()) - - const { headers, socket } = await client.upgrade({ - path: '/', - method: 'GET', - protocol: 'Websocket' - }) - - let recvData = '' - socket.on('data', (d) => { - recvData += d - }) - - socket.on('close', () => { - t.strictEqual(recvData.toString(), 'Body') - }) - - t.deepStrictEqual(headers, { - hello: 'world', - connection: 'upgrade', - upgrade: 'websocket' - }) - socket.end() - }) - - await t.completed -}) - -test('pool connect', async (t) => { - t = tspl(t, { plan: 1 }) - - const server = createServer((c) => { - t.fail() - }) - server.on('connect', (req, socket, firstBodyChunk) => { - socket.write('HTTP/1.1 200 Connection established\r\n\r\n') - - let data = firstBodyChunk.toString() - socket.on('data', (buf) => { - data += buf.toString() - }) - - socket.on('end', () => { - socket.end(data) - }) - }) - after(() => server.close()) - - server.listen(0, async () => { - const client = new Pool(`http://localhost:${server.address().port}`) - after(() => client.close()) - - const { socket } = await client.connect({ - path: '/' - }) - - let recvData = '' - socket.on('data', (d) => { - recvData += d - }) - - socket.on('end', () => { - t.strictEqual(recvData.toString(), 'Body') - }) - - socket.write('Body') - socket.end() - }) - - await t.completed -}) - test('pool dispatch', async (t) => { t = tspl(t, { plan: 2 }) @@ -614,20 +437,6 @@ test('pool dispatch', async (t) => { await t.completed }) -test('pool pipeline args validation', async (t) => { - t = tspl(t, { plan: 2 }) - - const client = new Pool('http://localhost:5000') - - const ret = client.pipeline(null, () => {}) - ret.on('error', (err) => { - t.ok(/opts/.test(err.message)) - t.ok(err instanceof errors.InvalidArgumentError) - }) - - await t.completed -}) - test('300 requests succeed', async (t) => { t = tspl(t, { plan: 300 * 3 }) @@ -687,42 +496,6 @@ test('pool connect error', async (t) => { await t.completed }) -test('pool upgrade error', async (t) => { - t = tspl(t, { plan: 1 }) - - const server = net.createServer((c) => { - c.on('data', (d) => { - c.write('HTTP/1.1 101\r\n') - c.write('hello: world\r\n') - c.write('connection: upgrade\r\n') - c.write('\r\n') - c.write('Body') - }) - c.on('error', () => { - // Whether we get an error, end or close is undefined. - // Ignore error. - }) - }) - after(() => server.close()) - - server.listen(0, async () => { - const client = new Pool(`http://localhost:${server.address().port}`) - after(() => client.close()) - - try { - await client.upgrade({ - path: '/', - method: 'GET', - protocol: 'Websocket' - }) - } catch (err) { - t.ok(err) - } - }) - - await t.completed -}) - test('pool dispatch error', async (t) => { t = tspl(t, { plan: 3 }) @@ -828,156 +601,6 @@ test('pool request abort in queue', async (t) => { await t.completed }) -test('pool stream abort in queue', async (t) => { - t = tspl(t, { plan: 3 }) - - const server = createServer((req, res) => { - res.end('asd') - }) - after(() => server.close()) - - server.listen(0, async () => { - const client = new Pool(`http://localhost:${server.address().port}`, { - connections: 1, - pipelining: 1 - }) - after(() => client.close()) - - client.dispatch({ - path: '/', - method: 'GET' - }, { - onConnect () { - }, - onHeaders (statusCode, headers) { - t.strictEqual(statusCode, 200) - }, - onData (chunk) { - }, - onComplete () { - t.ok(true, 'pass') - }, - onError () { - } - }) - - const signal = new EventEmitter() - client.stream({ - path: '/', - method: 'GET', - signal - }, ({ body }) => body, (err) => { - t.strictEqual(err.code, 'UND_ERR_ABORTED') - }) - signal.emit('abort') - }) - - await t.completed -}) - -test('pool pipeline abort in queue', async (t) => { - t = tspl(t, { plan: 3 }) - - const server = createServer((req, res) => { - res.end('asd') - }) - after(() => server.close()) - - server.listen(0, async () => { - const client = new Pool(`http://localhost:${server.address().port}`, { - connections: 1, - pipelining: 1 - }) - after(() => client.close()) - - client.dispatch({ - path: '/', - method: 'GET' - }, { - onConnect () { - }, - onHeaders (statusCode, headers) { - t.strictEqual(statusCode, 200) - }, - onData (chunk) { - }, - onComplete () { - t.ok(true, 'pass') - }, - onError () { - } - }) - - const signal = new EventEmitter() - client.pipeline({ - path: '/', - method: 'GET', - signal - }, ({ body }) => body).end().on('error', (err) => { - t.strictEqual(err.code, 'UND_ERR_ABORTED') - }) - signal.emit('abort') - }) - - await t.completed -}) - -test('pool stream constructor error destroy body', async (t) => { - t = tspl(t, { plan: 4 }) - - const server = createServer((req, res) => { - res.end('asd') - }) - after(() => server.close()) - - server.listen(0, async () => { - const client = new Pool(`http://localhost:${server.address().port}`, { - connections: 1, - pipelining: 1 - }) - after(() => client.close()) - - { - const body = new Readable({ - read () { - } - }) - client.stream({ - path: '/', - method: 'GET', - body, - headers: { - 'transfer-encoding': 'fail' - } - }, () => { - t.fail() - }, (err) => { - t.strictEqual(err.code, 'UND_ERR_INVALID_ARG') - t.strictEqual(body.destroyed, true) - }) - } - - { - const body = new Readable({ - read () { - } - }) - client.stream({ - path: '/', - method: 'CONNECT', - body - }, () => { - t.fail() - }, (err) => { - t.strictEqual(err.code, 'UND_ERR_INVALID_ARG') - t.strictEqual(body.destroyed, true) - }) - } - }) - - await t.completed -}) - test('pool request constructor error destroy body', async (t) => { t = tspl(t, { plan: 4 }) diff --git a/test/redirect-pipeline.js b/test/redirect-pipeline.js deleted file mode 100644 index 52e4545e0d6..00000000000 --- a/test/redirect-pipeline.js +++ /dev/null @@ -1,54 +0,0 @@ -'use strict' - -const { tspl } = require('@matteo.collina/tspl') -const { test } = require('node:test') -const { pipeline: undiciPipeline, Client, interceptors } = require('..') -const { pipeline: streamPipelineCb } = require('node:stream') -const { promisify } = require('node:util') -const { createReadable, createWritable } = require('./utils/stream') -const { startRedirectingServer } = require('./utils/redirecting-servers') - -const streamPipeline = promisify(streamPipelineCb) -const redirect = interceptors.redirect - -test('should not follow redirection by default if not using RedirectAgent', async t => { - t = tspl(t, { plan: 3 }) - - const body = [] - const serverRoot = await startRedirectingServer() - - await streamPipeline( - createReadable('REQUEST'), - undiciPipeline(`http://${serverRoot}/`, { - dispatcher: new Client(`http://${serverRoot}/`).compose(redirect({ maxRedirections: null })) - }, ({ statusCode, headers, body }) => { - t.strictEqual(statusCode, 302) - t.strictEqual(headers.location, `http://${serverRoot}/302/1`) - - return body - }), - createWritable(body) - ) - - t.strictEqual(body.length, 0) -}) - -test('should not follow redirects when using RedirectAgent within pipeline', async t => { - t = tspl(t, { plan: 3 }) - - const body = [] - const serverRoot = await startRedirectingServer() - - await streamPipeline( - createReadable('REQUEST'), - undiciPipeline(`http://${serverRoot}/`, { dispatcher: new Client(`http://${serverRoot}/`).compose(redirect({ maxRedirections: 1 })) }, ({ statusCode, headers, body }) => { - t.strictEqual(statusCode, 302) - t.strictEqual(headers.location, `http://${serverRoot}/302/1`) - - return body - }), - createWritable(body) - ) - - t.strictEqual(body.length, 0) -}) diff --git a/test/redirect-stream.js b/test/redirect-stream.js deleted file mode 100644 index f45456d331e..00000000000 --- a/test/redirect-stream.js +++ /dev/null @@ -1,423 +0,0 @@ -'use strict' - -const { tspl } = require('@matteo.collina/tspl') -const { test, describe } = require('node:test') -const { stream, Agent, Client, interceptors: { redirect } } = require('..') -const { - startRedirectingServer, - startRedirectingWithBodyServer, - startRedirectingChainServers, - startRedirectingWithoutLocationServer, - startRedirectingWithAuthorization, - startRedirectingWithCookie -} = require('./utils/redirecting-servers') -const { createReadable, createWritable } = require('./utils/stream') - -test('should always have a history with the final URL even if no redirections were followed', async t => { - t = tspl(t, { plan: 4 }) - - const body = [] - const server = await startRedirectingServer() - - await stream( - `http://${server}/200?key=value`, - { opaque: body, dispatcher: new Client(`http://${server}/`).compose(redirect({ maxRedirections: 10 })) }, - ({ statusCode, headers, opaque, context: { history } }) => { - t.strictEqual(statusCode, 200) - t.strictEqual(headers.location, undefined) - t.deepStrictEqual(history.map(x => x.toString()), [ - `http://${server}/200?key=value` - ]) - - return createWritable(opaque) - } - ) - - t.strictEqual(body.join(''), `GET /5 key=value :: host@${server} connection@keep-alive`) -}) - -test('should not follow redirection by default if max redirect = 0', async t => { - t = tspl(t, { plan: 3 }) - - const body = [] - const server = await startRedirectingServer() - - await stream(`http://${server}`, { opaque: body, dispatcher: new Agent({}).compose(redirect({ maxRedirections: 0 })) }, ({ statusCode, headers, opaque }) => { - t.strictEqual(statusCode, 302) - t.strictEqual(headers.location, `http://${server}/302/1`) - - return createWritable(opaque) - }) - - t.strictEqual(body.length, 0) -}) - -test('should follow redirection after a HTTP 300', async t => { - t = tspl(t, { plan: 4 }) - - const body = [] - const server = await startRedirectingServer() - - await stream( - `http://${server}/300?key=value`, - { opaque: body, dispatcher: new Client(`http://${server}/`).compose(redirect({ maxRedirections: 10 })) }, - ({ statusCode, headers, opaque, context: { history } }) => { - t.strictEqual(statusCode, 200) - t.strictEqual(headers.location, undefined) - t.deepStrictEqual(history.map(x => x.toString()), [ - `http://${server}/300?key=value`, - `http://${server}/300/1?key=value`, - `http://${server}/300/2?key=value`, - `http://${server}/300/3?key=value`, - `http://${server}/300/4?key=value`, - `http://${server}/300/5?key=value` - ]) - - return createWritable(opaque) - } - ) - - t.strictEqual(body.join(''), `GET /5 key=value :: host@${server} connection@keep-alive`) -}) - -test('should follow redirection after a HTTP 301', async t => { - t = tspl(t, { plan: 3 }) - - const body = [] - const server = await startRedirectingServer() - - await stream( - `http://${server}/301`, - { method: 'POST', body: 'REQUEST', opaque: body, dispatcher: new Client(`http://${server}/`).compose(redirect({ maxRedirections: 10 })) }, - ({ statusCode, headers, opaque }) => { - t.strictEqual(statusCode, 200) - t.strictEqual(headers.location, undefined) - - return createWritable(opaque) - } - ) - - t.strictEqual(body.join(''), `POST /5 :: host@${server} connection@keep-alive content-length@7 :: REQUEST`) -}) - -test('should follow redirection after a HTTP 302', async t => { - t = tspl(t, { plan: 3 }) - - const body = [] - const server = await startRedirectingServer() - - await stream( - `http://${server}/302`, - { method: 'PUT', body: Buffer.from('REQUEST'), opaque: body, dispatcher: new Client(`http://${server}/`).compose(redirect({ maxRedirections: 10 })) }, - ({ statusCode, headers, opaque }) => { - t.strictEqual(statusCode, 200) - t.strictEqual(headers.location, undefined) - - return createWritable(opaque) - } - ) - - t.strictEqual(body.join(''), `PUT /5 :: host@${server} connection@keep-alive content-length@7 :: REQUEST`) -}) - -test('should follow redirection after a HTTP 303 changing method to GET', async t => { - t = tspl(t, { plan: 3 }) - - const body = [] - const server = await startRedirectingServer() - - await stream(`http://${server}/303`, { opaque: body, dispatcher: new Client(`http://${server}/`).compose(redirect({ maxRedirections: 10 })) }, ({ statusCode, headers, opaque }) => { - t.strictEqual(statusCode, 200) - t.strictEqual(headers.location, undefined) - - return createWritable(opaque) - }) - - t.strictEqual(body.join(''), `GET /5 :: host@${server} connection@keep-alive`) -}) - -test('should remove Host and request body related headers when following HTTP 303 (array)', async t => { - t = tspl(t, { plan: 3 }) - - const body = [] - const server = await startRedirectingServer() - - await stream( - `http://${server}/303`, - { - method: 'PATCH', - headers: [ - 'Content-Encoding', - 'gzip', - 'X-Foo1', - '1', - 'X-Foo2', - '2', - 'Content-Type', - 'application/json', - 'X-Foo3', - '3', - 'Host', - 'localhost', - 'X-Bar', - '4' - ], - opaque: body, - dispatcher: new Client(`http://${server}/`).compose(redirect({ maxRedirections: 10 })) - }, - ({ statusCode, headers, opaque }) => { - t.strictEqual(statusCode, 200) - t.strictEqual(headers.location, undefined) - - return createWritable(opaque) - } - ) - - t.strictEqual(body.join(''), `GET /5 :: host@${server} connection@keep-alive x-foo1@1 x-foo2@2 x-foo3@3 x-bar@4`) -}) - -test('should remove Host and request body related headers when following HTTP 303 (object)', async t => { - t = tspl(t, { plan: 3 }) - - const body = [] - const server = await startRedirectingServer() - - await stream( - `http://${server}/303`, - { - method: 'PATCH', - headers: { - 'Content-Encoding': 'gzip', - 'X-Foo1': '1', - 'X-Foo2': '2', - 'Content-Type': 'application/json', - 'X-Foo3': '3', - Host: 'localhost', - 'X-Bar': '4' - }, - opaque: body, - dispatcher: new Client(`http://${server}/`).compose(redirect({ maxRedirections: 10 })) - }, - ({ statusCode, headers, opaque }) => { - t.strictEqual(statusCode, 200) - t.strictEqual(headers.location, undefined) - - return createWritable(opaque) - } - ) - - t.strictEqual(body.join(''), `GET /5 :: host@${server} connection@keep-alive x-foo1@1 x-foo2@2 x-foo3@3 x-bar@4`) -}) - -test('should follow redirection after a HTTP 307', async t => { - t = tspl(t, { plan: 3 }) - - const body = [] - const server = await startRedirectingServer() - - await stream( - `http://${server}/307`, - { method: 'DELETE', opaque: body, dispatcher: new Client(`http://${server}/`).compose(redirect({ maxRedirections: 10 })) }, - ({ statusCode, headers, opaque }) => { - t.strictEqual(statusCode, 200) - t.strictEqual(headers.location, undefined) - - return createWritable(opaque) - } - ) - - t.strictEqual(body.join(''), `DELETE /5 :: host@${server} connection@keep-alive`) -}) - -test('should follow redirection after a HTTP 308', async t => { - t = tspl(t, { plan: 3 }) - - const body = [] - const server = await startRedirectingServer() - - await stream( - `http://${server}/308`, - { method: 'OPTIONS', opaque: body, dispatcher: new Client(`http://${server}/`).compose(redirect({ maxRedirections: 10 })) }, - ({ statusCode, headers, opaque }) => { - t.strictEqual(statusCode, 200) - t.strictEqual(headers.location, undefined) - - return createWritable(opaque) - } - ) - - t.strictEqual(body.join(''), `OPTIONS /5 :: host@${server} connection@keep-alive`) -}) - -test('should ignore HTTP 3xx response bodies', async t => { - t = tspl(t, { plan: 4 }) - - const body = [] - const server = await startRedirectingWithBodyServer() - - await stream( - `http://${server}/`, - { opaque: body, dispatcher: new Client(`http://${server}/`).compose(redirect({ maxRedirections: 10 })) }, - ({ statusCode, headers, opaque, context: { history } }) => { - t.strictEqual(statusCode, 200) - t.strictEqual(headers.location, undefined) - t.deepStrictEqual(history.map(x => x.toString()), [`http://${server}/`, `http://${server}/end`]) - - return createWritable(opaque) - } - ) - - t.strictEqual(body.join(''), 'FINAL') -}) - -test('should follow a redirect chain up to the allowed number of times', async t => { - t = tspl(t, { plan: 4 }) - - const body = [] - const server = await startRedirectingServer() - - await stream( - `http://${server}/300`, - { opaque: body, dispatcher: new Client(`http://${server}/`).compose(redirect({ maxRedirections: 2 })) }, - ({ statusCode, headers, opaque, context: { history } }) => { - t.strictEqual(statusCode, 300) - t.strictEqual(headers.location, `http://${server}/300/3`) - t.deepStrictEqual(history.map(x => x.toString()), [`http://${server}/300`, `http://${server}/300/1`, `http://${server}/300/2`]) - - return createWritable(opaque) - } - ) - - t.strictEqual(body.length, 0) -}) - -test('should follow redirections when going cross origin', async t => { - t = tspl(t, { plan: 4 }) - - const [server1, server2, server3] = await startRedirectingChainServers() - const body = [] - - await stream( - `http://${server1}`, - { method: 'POST', opaque: body, dispatcher: new Agent({}).compose(redirect({ maxRedirections: 10 })) }, - ({ statusCode, headers, opaque, context: { history } }) => { - t.strictEqual(statusCode, 200) - t.strictEqual(headers.location, undefined) - t.deepStrictEqual(history.map(x => x.toString()), [ - `http://${server1}/`, - `http://${server2}/`, - `http://${server3}/`, - `http://${server2}/end`, - `http://${server3}/end`, - `http://${server1}/end` - ]) - - return createWritable(opaque) - } - ) - - t.strictEqual(body.join(''), 'POST') -}) - -describe('when a Location response header is NOT present', async () => { - const redirectCodes = [300, 301, 302, 303, 307, 308] - const server = await startRedirectingWithoutLocationServer() - - for (const code of redirectCodes) { - test(`should return the original response after a HTTP ${code}`, async t => { - t = tspl(t, { plan: 3 }) - - const body = [] - - await stream( - `http://${server}/${code}`, - { opaque: body, maxRedirections: 10 }, - ({ statusCode, headers, opaque }) => { - t.strictEqual(statusCode, code) - t.strictEqual(headers.location, undefined) - - return createWritable(opaque) - } - ) - - t.strictEqual(body.length, 0) - await t.completed - }) - } -}) - -test('should not follow redirects when using Readable request bodies', async t => { - t = tspl(t, { plan: 3 }) - - const body = [] - const server = await startRedirectingServer() - - await stream( - `http://${server}`, - { - method: 'POST', - body: createReadable('REQUEST'), - opaque: body, - maxRedirections: 10 - }, - ({ statusCode, headers, opaque }) => { - t.strictEqual(statusCode, 302) - t.strictEqual(headers.location, `http://${server}/302/1`) - - return createWritable(opaque) - } - ) - - t.strictEqual(body.length, 0) -}) - -test('should handle errors', async t => { - t = tspl(t, { plan: 2 }) - - const body = [] - - try { - await stream('http://localhost:0', { opaque: body, maxRedirections: 10 }, ({ statusCode, headers, opaque }) => { - return createWritable(opaque) - }) - - throw new Error('Did not throw') - } catch (error) { - t.match(error.code, /EADDRNOTAVAIL|ECONNREFUSED/) - t.strictEqual(body.length, 0) - } -}) - -test('removes authorization header on third party origin', async t => { - t = tspl(t, { plan: 1 }) - - const body = [] - - const [server1] = await startRedirectingWithAuthorization('secret') - await stream(`http://${server1}`, { - maxRedirections: 10, - opaque: body, - headers: { - authorization: 'secret' - } - }, ({ statusCode, headers, opaque }) => createWritable(opaque)) - - t.strictEqual(body.length, 0) -}) - -test('removes cookie header on third party origin', async t => { - t = tspl(t, { plan: 1 }) - - const body = [] - - const [server1] = await startRedirectingWithCookie('a=b') - await stream(`http://${server1}`, { - maxRedirections: 10, - opaque: body, - headers: { - cookie: 'a=b' - } - }, ({ statusCode, headers, opaque }) => createWritable(opaque)) - - t.strictEqual(body.length, 0) -}) diff --git a/test/request-timeout.js b/test/request-timeout.js index 19f602fff8b..842729f1adc 100644 --- a/test/request-timeout.js +++ b/test/request-timeout.js @@ -10,12 +10,6 @@ const { createServer } = require('node:http') const EventEmitter = require('node:events') const FakeTimers = require('@sinonjs/fake-timers') const { AbortController } = require('abort-controller') -const { - pipeline, - Readable, - Writable, - PassThrough -} = require('node:stream') const { tick: fastTimersTick, reset: resetFastTimers @@ -620,192 +614,6 @@ test('Disable request timeout for a single request', async (t) => { await t.completed }) -test('stream timeout', async (t) => { - t = tspl(t, { plan: 1 }) - - const clock = FakeTimers.install({ - shouldClearNativeTimers: true, - toFake: ['setTimeout', 'clearTimeout'] - }) - after(() => clock.uninstall()) - - const server = createServer((req, res) => { - setTimeout(() => { - res.end('hello') - }, 301e3) - clock.tick(301e3) - fastTimersTick(301e3) - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`, { connectTimeout: 0 }) - after(() => client.destroy()) - - client.stream({ - path: '/', - method: 'GET', - opaque: new PassThrough() - }, (result) => { - t.fail('Should not be called') - }, (err) => { - t.ok(err instanceof errors.HeadersTimeoutError) - }) - }) - - await t.completed -}) - -test('stream custom timeout', async (t) => { - t = tspl(t, { plan: 1 }) - - const clock = FakeTimers.install({ - shouldClearNativeTimers: true, - toFake: ['setTimeout', 'clearTimeout'] - }) - after(() => clock.uninstall()) - - const server = createServer((req, res) => { - setTimeout(() => { - res.end('hello') - }, 31e3) - clock.tick(31e3) - fastTimersTick(31e3) - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`, { - headersTimeout: 30e3 - }) - after(() => client.destroy()) - - client.stream({ - path: '/', - method: 'GET', - opaque: new PassThrough() - }, (result) => { - t.fail('Should not be called') - }, (err) => { - t.ok(err instanceof errors.HeadersTimeoutError) - }) - }) - - await t.completed -}) - -test('pipeline timeout', async (t) => { - t = tspl(t, { plan: 1 }) - - const clock = FakeTimers.install({ - shouldClearNativeTimers: true, - toFake: ['setTimeout', 'clearTimeout'] - }) - after(() => clock.uninstall()) - - const server = createServer((req, res) => { - setTimeout(() => { - req.pipe(res) - }, 301e3) - clock.tick(301e3) - fastTimersTick(301e3) - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - after(() => client.destroy()) - - const buf = Buffer.alloc(1e6).toString() - pipeline( - new Readable({ - read () { - this.push(buf) - this.push(null) - } - }), - client.pipeline({ - path: '/', - method: 'PUT' - }, (result) => { - t.fail('Should not be called') - }, (e) => { - t.fail('Should not be called') - }), - new Writable({ - write (chunk, encoding, callback) { - callback() - }, - final (callback) { - callback() - } - }), - (err) => { - t.ok(err instanceof errors.HeadersTimeoutError) - } - ) - }) - - await t.completed -}) - -test('pipeline timeout', async (t) => { - t = tspl(t, { plan: 1 }) - - const clock = FakeTimers.install({ - shouldClearNativeTimers: true, - toFake: ['setTimeout', 'clearTimeout'] - }) - after(() => clock.uninstall()) - - const server = createServer((req, res) => { - setTimeout(() => { - req.pipe(res) - }, 31e3) - clock.tick(31e3) - fastTimersTick(31e3) - }) - after(() => server.close()) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`, { - headersTimeout: 30e3 - }) - after(() => client.destroy()) - - const buf = Buffer.alloc(1e6).toString() - pipeline( - new Readable({ - read () { - this.push(buf) - this.push(null) - } - }), - client.pipeline({ - path: '/', - method: 'PUT' - }, (result) => { - t.fail('Should not be called') - }, (e) => { - t.fail('Should not be called') - }), - new Writable({ - write (chunk, encoding, callback) { - callback() - }, - final (callback) { - callback() - } - }), - (err) => { - t.ok(err instanceof errors.HeadersTimeoutError) - } - ) - }) - - await t.completed -}) - test('client.close should not deadlock', async (t) => { t = tspl(t, { plan: 2 }) diff --git a/test/types/agent.test-d.ts b/test/types/agent.test-d.ts index 72e27293418..94ab3cac268 100644 --- a/test/types/agent.test-d.ts +++ b/test/types/agent.test-d.ts @@ -1,4 +1,3 @@ -import { Duplex, Readable, Writable } from 'stream' import { expectAssignable } from 'tsd' import { Agent, Dispatcher } from '../..' import { URL } from 'url' @@ -17,7 +16,6 @@ expectAssignable(new Agent({ factory: () => new Dispatcher() })) // request expectAssignable>(agent.request({ origin: '', path: '', method: 'GET' })) - expectAssignable>(agent.request({ origin: '', path: '', method: 'GET', onInfo: (info) => {} })) expectAssignable>(agent.request({ origin: new URL('http://localhost'), path: '', method: 'GET' })) expectAssignable(agent.request({ origin: '', path: '', method: 'GET' }, (err, data) => { expectAssignable(err) @@ -28,75 +26,6 @@ expectAssignable(new Agent({ factory: () => new Dispatcher() })) expectAssignable(data) })) - // stream - expectAssignable>(agent.stream({ origin: '', path: '', method: 'GET' }, data => { - expectAssignable(data) - return new Writable() - })) - expectAssignable>(agent.stream({ origin: '', path: '', method: 'GET', onInfo: (info) => {} }, data => { - expectAssignable(data) - return new Writable() - })) - expectAssignable>(agent.stream({ origin: new URL('http://localhost'), path: '', method: 'GET' }, data => { - expectAssignable(data) - return new Writable() - })) - expectAssignable(agent.stream( - { origin: '', path: '', method: 'GET' }, - data => { - expectAssignable(data) - return new Writable() - }, - (err, data) => { - expectAssignable(err) - expectAssignable(data) - } - )) - expectAssignable(agent.stream( - { origin: new URL('http://localhost'), path: '', method: 'GET' }, - data => { - expectAssignable(data) - return new Writable() - }, - (err, data) => { - expectAssignable(err) - expectAssignable(data) - } - )) - - // pipeline - expectAssignable(agent.pipeline({ origin: '', path: '', method: 'GET' }, data => { - expectAssignable(data) - return new Readable() - })) - expectAssignable(agent.pipeline({ origin: '', path: '', method: 'GET', onInfo: (info) => {} }, data => { - expectAssignable(data) - return new Readable() - })) - expectAssignable(agent.pipeline({ origin: new URL('http://localhost'), path: '', method: 'GET' }, data => { - expectAssignable(data) - return new Readable() - })) - - // upgrade - expectAssignable>(agent.upgrade({ path: '' })) - expectAssignable(agent.upgrade({ path: '' }, (err, data) => { - expectAssignable(err) - expectAssignable(data) - })) - - // connect - expectAssignable>(agent.connect({ origin: '', path: '' })) - expectAssignable>(agent.connect({ origin: new URL('http://localhost'), path: '' })) - expectAssignable(agent.connect({ origin: '', path: '' }, (err, data) => { - expectAssignable(err) - expectAssignable(data) - })) - expectAssignable(agent.connect({ origin: new URL('http://localhost'), path: '' }, (err, data) => { - expectAssignable(err) - expectAssignable(data) - })) - // dispatch expectAssignable(agent.dispatch({ origin: '', path: '', method: 'GET' }, {})) expectAssignable(agent.dispatch({ origin: '', path: '', method: 'GET', maxRedirections: 1 }, {})) diff --git a/test/types/api.test-d.ts b/test/types/api.test-d.ts index 85d5e6723a5..44c41e8d93a 100644 --- a/test/types/api.test-d.ts +++ b/test/types/api.test-d.ts @@ -1,38 +1,7 @@ -import { Duplex, Readable, Writable } from 'stream' -import { expectAssignable, expectType } from 'tsd' -import { Dispatcher, request, stream, pipeline, connect, upgrade } from '../..' +import { expectAssignable } from 'tsd' +import { Dispatcher, request } from '../..' // request expectAssignable>(request('')) expectAssignable>(request('', { })) expectAssignable>(request('', { method: 'GET', reset: false })) - -// stream -expectAssignable>(stream('', { method: 'GET' }, data => { - expectAssignable(data) - expectType(data.opaque) - return new Writable() -})) -expectAssignable>>(stream('', { method: 'GET', opaque: { example: '' } }, data => { - expectType<{ example: string }>(data.opaque) - return new Writable() -})) - -// pipeline -expectAssignable(pipeline('', { method: 'GET' }, data => { - expectAssignable(data) - expectType(data.opaque) - return new Readable() -})) -expectAssignable(pipeline('', { method: 'GET', opaque: { example: '' } }, data => { - expectType<{ example: string }>(data.opaque) - return new Readable() -})) - -// connect -expectAssignable>(connect('')) -expectAssignable>(connect('', {})) - -// upgrade -expectAssignable>(upgrade('')) -expectAssignable>(upgrade('', {})) diff --git a/test/types/balanced-pool.test-d.ts b/test/types/balanced-pool.test-d.ts index abbdf19ae60..fc6c0275a94 100644 --- a/test/types/balanced-pool.test-d.ts +++ b/test/types/balanced-pool.test-d.ts @@ -1,4 +1,3 @@ -import { Duplex, Readable, Writable } from 'stream' import { expectAssignable } from 'tsd' import { Dispatcher, BalancedPool, Client } from '../..' import { URL } from 'url' @@ -38,62 +37,6 @@ expectAssignable(new BalancedPool([new URL('http://localhost:4242' expectAssignable(data) })) - // stream - expectAssignable>(pool.stream({ origin: '', path: '', method: 'GET' }, data => { - expectAssignable(data) - return new Writable() - })) - expectAssignable>(pool.stream({ origin: new URL('http://localhost'), path: '', method: 'GET' }, data => { - expectAssignable(data) - return new Writable() - })) - expectAssignable(pool.stream( - { origin: '', path: '', method: 'GET' }, - data => { - expectAssignable(data) - return new Writable() - }, - (err, data) => { - expectAssignable(err) - expectAssignable(data) - } - )) - expectAssignable(pool.stream( - { origin: new URL('http://localhost'), path: '', method: 'GET' }, - data => { - expectAssignable(data) - return new Writable() - }, - (err, data) => { - expectAssignable(err) - expectAssignable(data) - } - )) - - // pipeline - expectAssignable(pool.pipeline({ origin: '', path: '', method: 'GET' }, data => { - expectAssignable(data) - return new Readable() - })) - expectAssignable(pool.pipeline({ origin: new URL('http://localhost'), path: '', method: 'GET' }, data => { - expectAssignable(data) - return new Readable() - })) - - // upgrade - expectAssignable>(pool.upgrade({ path: '' })) - expectAssignable(pool.upgrade({ path: '' }, (err, data) => { - expectAssignable(err) - expectAssignable(data) - })) - - // connect - expectAssignable>(pool.connect({ path: '' })) - expectAssignable(pool.connect({ path: '' }, (err, data) => { - expectAssignable(err) - expectAssignable(data) - })) - // dispatch expectAssignable(pool.dispatch({ origin: '', path: '', method: 'GET' }, {})) expectAssignable(pool.dispatch({ origin: new URL('http://localhost'), path: '', method: 'GET' }, {})) diff --git a/test/types/client.test-d.ts b/test/types/client.test-d.ts index 1c0f558a790..95f2b0813f5 100644 --- a/test/types/client.test-d.ts +++ b/test/types/client.test-d.ts @@ -1,4 +1,3 @@ -import { Duplex, Readable, Writable } from 'stream' import { expectAssignable } from 'tsd' import { Client, Dispatcher } from '../..' import { URL } from 'url' @@ -100,68 +99,6 @@ expectAssignable(new Client('', { expectAssignable(data) })) - // stream - expectAssignable>(client.stream({ origin: '', path: '', method: 'GET' }, data => { - expectAssignable(data) - return new Writable() - })) - expectAssignable>(client.stream({ origin: new URL('http://localhost'), path: '', method: 'GET' }, data => { - expectAssignable(data) - return new Writable() - })) - expectAssignable(client.stream( - { origin: '', path: '', method: 'GET' }, - data => { - expectAssignable(data) - return new Writable() - }, - (err, data) => { - expectAssignable(err) - expectAssignable(data) - } - )) - expectAssignable(client.stream( - { origin: new URL('http://localhost'), path: '', method: 'GET' }, - data => { - expectAssignable(data) - return new Writable() - }, - (err, data) => { - expectAssignable(err) - expectAssignable(data) - } - )) - - // pipeline - expectAssignable(client.pipeline({ origin: '', path: '', method: 'GET' }, data => { - expectAssignable(data) - return new Readable() - })) - expectAssignable(client.pipeline({ origin: new URL('http://localhost'), path: '', method: 'GET' }, data => { - expectAssignable(data) - return new Readable() - })) - - // upgrade - expectAssignable>(client.upgrade({ path: '' })) - expectAssignable>(client.upgrade({ path: '', headers: [] })) - expectAssignable>(client.upgrade({ path: '', headers: {} })) - expectAssignable>(client.upgrade({ path: '', headers: null })) - expectAssignable(client.upgrade({ path: '' }, (err, data) => { - expectAssignable(err) - expectAssignable(data) - })) - - // connect - expectAssignable>(client.connect({ path: '' })) - expectAssignable>(client.connect({ path: '', headers: [] })) - expectAssignable>(client.connect({ path: '', headers: {} })) - expectAssignable>(client.connect({ path: '', headers: null })) - expectAssignable(client.connect({ path: '' }, (err, data) => { - expectAssignable(err) - expectAssignable(data) - })) - // dispatch expectAssignable(client.dispatch({ origin: '', path: '', method: 'GET' }, {})) expectAssignable(client.dispatch({ origin: '', path: '', method: 'GET', headers: [] }, {})) diff --git a/test/types/dispatcher.test-d.ts b/test/types/dispatcher.test-d.ts index 58dda692fdc..0b0bfd936b5 100644 --- a/test/types/dispatcher.test-d.ts +++ b/test/types/dispatcher.test-d.ts @@ -1,5 +1,4 @@ import { IncomingHttpHeaders } from 'http' -import { Duplex, Readable, Writable } from 'stream' import { expectAssignable, expectType } from 'tsd' import { Dispatcher, Headers } from '../..' import { URL } from 'url' @@ -37,20 +36,6 @@ expectAssignable(new Dispatcher()) expectAssignable(dispatcher.dispatch({ origin: new URL('http://localhost'), path: '', method: 'GET' }, {})) expectAssignable(dispatcher.dispatch({ path: '', method: 'CUSTOM' }, {})) - // connect - expectAssignable>(dispatcher.connect({ origin: '', path: '', maxRedirections: 0 })) - expectAssignable>(dispatcher.connect({ origin: new URL('http://localhost'), path: '', maxRedirections: 0 })) - expectAssignable(dispatcher.connect({ origin: '', path: '' }, (err, data) => { - expectAssignable(err) - expectAssignable(data) - })) - expectAssignable(dispatcher.connect({ origin: new URL('http://localhost'), path: '' }, (err, data) => { - expectAssignable(err) - expectAssignable(data) - })) - expectAssignable>(dispatcher.connect({ origin: '', path: '', responseHeaders: 'raw' })) - expectAssignable>(dispatcher.connect({ origin: '', path: '', responseHeaders: null })) - // request expectAssignable>(dispatcher.request({ origin: '', path: '', method: 'GET', maxRedirections: 0 })) expectAssignable>(dispatcher.request({ origin: '', path: '', method: 'GET', maxRedirections: 0, query: {} })) @@ -69,94 +54,6 @@ expectAssignable(new Dispatcher()) expectAssignable>(dispatcher.request({ origin: '', path: '', method: 'GET', responseHeaders: null })) expectAssignable>>(dispatcher.request({ origin: '', path: '', method: 'GET', opaque: { example: '' } })) - // pipeline - expectAssignable(dispatcher.pipeline({ origin: '', path: '', method: 'GET', maxRedirections: 0 }, data => { - expectAssignable(data) - return new Readable() - })) - expectAssignable(dispatcher.pipeline({ origin: new URL('http://localhost'), path: '', method: 'GET' }, data => { - expectAssignable(data) - return new Readable() - })) - expectAssignable(dispatcher.pipeline({ origin: '', path: '', method: 'GET', responseHeaders: 'raw' }, data => { - expectAssignable(data) - return new Readable() - })) - expectAssignable(dispatcher.pipeline({ origin: '', path: '', method: 'GET', responseHeaders: null }, data => { - expectAssignable(data) - return new Readable() - })) - expectAssignable(dispatcher.pipeline({ origin: '', path: '', method: 'GET', opaque: { example: '' } }, data => { - expectAssignable>(data) - expectType<{ example: string }>(data.opaque) - return new Readable() - })) - - // stream - expectAssignable>(dispatcher.stream({ origin: '', path: '', method: 'GET', maxRedirections: 0 }, data => { - expectAssignable(data) - return new Writable() - })) - expectAssignable>(dispatcher.stream({ origin: new URL('http://localhost'), path: '', method: 'GET' }, data => { - expectAssignable(data) - return new Writable() - })) - expectAssignable>>(dispatcher.stream({ origin: '', path: '', method: 'GET', opaque: { example: '' } }, data => { - expectType<{ example: string }>(data.opaque) - return new Writable() - })) - expectAssignable(dispatcher.stream( - { origin: '', path: '', method: 'GET', reset: false }, - data => { - expectAssignable(data) - return new Writable() - }, - (err, data) => { - expectAssignable(err) - expectAssignable(data) - } - )) - expectAssignable(dispatcher.stream( - { origin: new URL('http://localhost'), path: '', method: 'GET' }, - data => { - expectAssignable(data) - return new Writable() - }, - (err, data) => { - expectAssignable(err) - expectAssignable(data) - } - )) - expectAssignable(dispatcher.stream( - { origin: new URL('http://localhost'), path: '', method: 'GET', opaque: { example: '' } }, - data => { - expectAssignable>(data) - return new Writable() - }, - (err, data) => { - expectAssignable(err) - expectAssignable>(data) - expectType<{ example: string }>(data.opaque) - } - )) - expectAssignable>(dispatcher.stream({ origin: '', path: '', method: 'GET', responseHeaders: 'raw' }, data => { - expectAssignable(data) - return new Writable() - })) - expectAssignable>(dispatcher.stream({ origin: '', path: '', method: 'GET', responseHeaders: null }, data => { - expectAssignable(data) - return new Writable() - })) - - // upgrade - expectAssignable>(dispatcher.upgrade({ path: '', maxRedirections: 0 })) - expectAssignable(dispatcher.upgrade({ path: '' }, (err, data) => { - expectAssignable(err) - expectAssignable(data) - })) - expectAssignable>(dispatcher.upgrade({ path: '', responseHeaders: 'raw' })) - expectAssignable>(dispatcher.upgrade({ path: '', responseHeaders: null })) - // close expectAssignable>(dispatcher.close()) expectAssignable(dispatcher.close(() => {})) diff --git a/test/types/env-http-proxy-agent.test-d.ts b/test/types/env-http-proxy-agent.test-d.ts index e6370e4418a..ae47c92fdbe 100644 --- a/test/types/env-http-proxy-agent.test-d.ts +++ b/test/types/env-http-proxy-agent.test-d.ts @@ -1,4 +1,3 @@ -import { Duplex, Readable, Writable } from 'stream' import { expectAssignable } from 'tsd' import { EnvHttpProxyAgent, setGlobalDispatcher, getGlobalDispatcher, Dispatcher } from '../..' @@ -12,7 +11,6 @@ expectAssignable(new EnvHttpProxyAgent({ httpProxy: 'http://l // request expectAssignable>(agent.request({ origin: '', path: '', method: 'GET' })) - expectAssignable>(agent.request({ origin: '', path: '', method: 'GET', onInfo: (info) => {} })) expectAssignable>(agent.request({ origin: new URL('http://localhost'), path: '', method: 'GET' })) expectAssignable(agent.request({ origin: '', path: '', method: 'GET' }, (err, data) => { expectAssignable(err) @@ -23,75 +21,6 @@ expectAssignable(new EnvHttpProxyAgent({ httpProxy: 'http://l expectAssignable(data) })) - // stream - expectAssignable>(agent.stream({ origin: '', path: '', method: 'GET' }, data => { - expectAssignable(data) - return new Writable() - })) - expectAssignable>(agent.stream({ origin: '', path: '', method: 'GET', onInfo: (info) => {} }, data => { - expectAssignable(data) - return new Writable() - })) - expectAssignable>(agent.stream({ origin: new URL('http://localhost'), path: '', method: 'GET' }, data => { - expectAssignable(data) - return new Writable() - })) - expectAssignable(agent.stream( - { origin: '', path: '', method: 'GET' }, - data => { - expectAssignable(data) - return new Writable() - }, - (err, data) => { - expectAssignable(err) - expectAssignable(data) - } - )) - expectAssignable(agent.stream( - { origin: new URL('http://localhost'), path: '', method: 'GET' }, - data => { - expectAssignable(data) - return new Writable() - }, - (err, data) => { - expectAssignable(err) - expectAssignable(data) - } - )) - - // pipeline - expectAssignable(agent.pipeline({ origin: '', path: '', method: 'GET' }, data => { - expectAssignable(data) - return new Readable() - })) - expectAssignable(agent.pipeline({ origin: '', path: '', method: 'GET', onInfo: (info) => {} }, data => { - expectAssignable(data) - return new Readable() - })) - expectAssignable(agent.pipeline({ origin: new URL('http://localhost'), path: '', method: 'GET' }, data => { - expectAssignable(data) - return new Readable() - })) - - // upgrade - expectAssignable>(agent.upgrade({ path: '' })) - expectAssignable(agent.upgrade({ path: '' }, (err, data) => { - expectAssignable(err) - expectAssignable(data) - })) - - // connect - expectAssignable>(agent.connect({ origin: '', path: '' })) - expectAssignable>(agent.connect({ origin: new URL('http://localhost'), path: '' })) - expectAssignable(agent.connect({ origin: '', path: '' }, (err, data) => { - expectAssignable(err) - expectAssignable(data) - })) - expectAssignable(agent.connect({ origin: new URL('http://localhost'), path: '' }, (err, data) => { - expectAssignable(err) - expectAssignable(data) - })) - // dispatch expectAssignable(agent.dispatch({ origin: '', path: '', method: 'GET' }, {})) expectAssignable(agent.dispatch({ origin: '', path: '', method: 'GET', maxRedirections: 1 }, {})) diff --git a/test/types/pool.test-d.ts b/test/types/pool.test-d.ts index c237468e836..745d464732d 100644 --- a/test/types/pool.test-d.ts +++ b/test/types/pool.test-d.ts @@ -1,4 +1,3 @@ -import { Duplex, Readable, Writable } from 'stream' import { expectAssignable, expectType } from 'tsd' import { Dispatcher, Pool, Client } from '../..' import { URL } from 'url' @@ -30,62 +29,6 @@ expectAssignable(new Pool('', { connections: 1 })) expectAssignable(data) })) - // stream - expectAssignable>(pool.stream({ origin: '', path: '', method: 'GET' }, data => { - expectAssignable(data) - return new Writable() - })) - expectAssignable>(pool.stream({ origin: new URL('http://localhost'), path: '', method: 'GET' }, data => { - expectAssignable(data) - return new Writable() - })) - expectAssignable(pool.stream( - { origin: '', path: '', method: 'GET' }, - data => { - expectAssignable(data) - return new Writable() - }, - (err, data) => { - expectAssignable(err) - expectAssignable(data) - } - )) - expectAssignable(pool.stream( - { origin: new URL('http://localhost'), path: '', method: 'GET' }, - data => { - expectAssignable(data) - return new Writable() - }, - (err, data) => { - expectAssignable(err) - expectAssignable(data) - } - )) - - // pipeline - expectAssignable(pool.pipeline({ origin: '', path: '', method: 'GET' }, data => { - expectAssignable(data) - return new Readable() - })) - expectAssignable(pool.pipeline({ origin: new URL('http://localhost'), path: '', method: 'GET' }, data => { - expectAssignable(data) - return new Readable() - })) - - // upgrade - expectAssignable>(pool.upgrade({ path: '' })) - expectAssignable(pool.upgrade({ path: '' }, (err, data) => { - expectAssignable(err) - expectAssignable(data) - })) - - // connect - expectAssignable>(pool.connect({ path: '' })) - expectAssignable(pool.connect({ path: '' }, (err, data) => { - expectAssignable(err) - expectAssignable(data) - })) - // dispatch expectAssignable(pool.dispatch({ origin: '', path: '', method: 'GET' }, {})) expectAssignable(pool.dispatch({ origin: new URL('http://localhost'), path: '', method: 'GET' }, {})) diff --git a/test/utils/esm-wrapper.mjs b/test/utils/esm-wrapper.mjs index 104190c1afe..8f43b207300 100644 --- a/test/utils/esm-wrapper.mjs +++ b/test/utils/esm-wrapper.mjs @@ -6,14 +6,10 @@ import { Agent, Client, errors, - pipeline, Pool, request, - connect, - upgrade, setGlobalDispatcher, - getGlobalDispatcher, - stream + getGlobalDispatcher } from '../../index.js' test('imported Client works with basic GET', async (t) => { @@ -91,15 +87,11 @@ test('imported errors work with request args validation promise', (t) => { }) test('named exports', (t) => { - t = tspl(t, { plan: 10 }) + t = tspl(t, { plan: 6 }) t.strictEqual(typeof Client, 'function') t.strictEqual(typeof Pool, 'function') t.strictEqual(typeof Agent, 'function') t.strictEqual(typeof request, 'function') - t.strictEqual(typeof stream, 'function') - t.strictEqual(typeof pipeline, 'function') - t.strictEqual(typeof connect, 'function') - t.strictEqual(typeof upgrade, 'function') t.strictEqual(typeof setGlobalDispatcher, 'function') t.strictEqual(typeof getGlobalDispatcher, 'function') }) diff --git a/types/api.d.ts b/types/api.d.ts index e58d08f61cc..c76f923944e 100644 --- a/types/api.d.ts +++ b/types/api.d.ts @@ -1,5 +1,4 @@ import { URL, UrlObject } from 'url' -import { Duplex } from 'stream' import Dispatcher from './dispatcher' /** Performs an HTTP request. */ @@ -8,36 +7,6 @@ declare function request ( options?: { dispatcher?: Dispatcher } & Omit, 'origin' | 'path' | 'method'> & Partial>, ): Promise> -/** A faster version of `request`. */ -declare function stream ( - url: string | URL | UrlObject, - options: { dispatcher?: Dispatcher } & Omit, 'origin' | 'path'>, - factory: Dispatcher.StreamFactory -): Promise> - -/** For easy use with `stream.pipeline`. */ -declare function pipeline ( - url: string | URL | UrlObject, - options: { dispatcher?: Dispatcher } & Omit, 'origin' | 'path'>, - handler: Dispatcher.PipelineHandler -): Duplex - -/** Starts two-way communications with the requested resource. */ -declare function connect ( - url: string | URL | UrlObject, - options?: { dispatcher?: Dispatcher } & Omit, 'origin' | 'path'> -): Promise> - -/** Upgrade to a different protocol. */ -declare function upgrade ( - url: string | URL | UrlObject, - options?: { dispatcher?: Dispatcher } & Omit -): Promise - export { - request, - stream, - pipeline, - connect, - upgrade + request } diff --git a/types/dispatcher.d.ts b/types/dispatcher.d.ts index 7a9810a2c47..10d721367fc 100644 --- a/types/dispatcher.d.ts +++ b/types/dispatcher.d.ts @@ -1,5 +1,5 @@ import { URL } from 'url' -import { Duplex, Readable, Writable } from 'stream' +import { Duplex, Readable } from 'stream' import { EventEmitter } from 'events' import { Blob } from 'buffer' import { IncomingHttpHeaders } from './header' @@ -16,23 +16,12 @@ export default Dispatcher declare class Dispatcher extends EventEmitter { /** Dispatches a request. This API is expected to evolve through semver-major versions and is less stable than the preceding higher level APIs. It is primarily intended for library developers who implement higher level APIs on top of this. */ dispatch (options: Dispatcher.DispatchOptions, handler: Dispatcher.DispatchHandlers): boolean - /** Starts two-way communications with the requested resource. */ - connect(options: Dispatcher.ConnectOptions): Promise> - connect(options: Dispatcher.ConnectOptions, callback: (err: Error | null, data: Dispatcher.ConnectData) => void): void /** Compose a chain of dispatchers */ compose (dispatchers: Dispatcher.DispatcherComposeInterceptor[]): Dispatcher.ComposedDispatcher compose (...dispatchers: Dispatcher.DispatcherComposeInterceptor[]): Dispatcher.ComposedDispatcher /** Performs an HTTP request. */ request(options: Dispatcher.RequestOptions): Promise> request(options: Dispatcher.RequestOptions, callback: (err: Error | null, data: Dispatcher.ResponseData) => void): void - /** For easy use with `stream.pipeline`. */ - pipeline(options: Dispatcher.PipelineOptions, handler: Dispatcher.PipelineHandler): Duplex - /** A faster version of `Dispatcher.request`. */ - stream(options: Dispatcher.RequestOptions, factory: Dispatcher.StreamFactory): Promise> - stream(options: Dispatcher.RequestOptions, factory: Dispatcher.StreamFactory, callback: (err: Error | null, data: Dispatcher.StreamData) => void): void - /** Upgrade to a different protocol. */ - upgrade (options: Dispatcher.UpgradeOptions): Promise - upgrade (options: Dispatcher.UpgradeOptions, callback: (err: Error | null, data: Dispatcher.UpgradeData) => void): void /** Closes the client and gracefully waits for enqueued requests to complete before invoking the callback (or returning a promise if no callback is provided). */ close (): Promise close (callback: () => void): void @@ -123,22 +112,6 @@ declare namespace Dispatcher { /** For H2, it appends the expect: 100-continue header, and halts the request body until a 100-continue is received from the remote server */ expectContinue?: boolean; } - export interface ConnectOptions { - origin: string | URL; - path: string; - /** Default: `null` */ - headers?: IncomingHttpHeaders | string[] | null; - /** Default: `null` */ - signal?: AbortSignal | EventEmitter | null; - /** This argument parameter is passed through to `ConnectData` */ - opaque?: TOpaque; - /** Default: 0 */ - maxRedirections?: number; - /** Default: false */ - redirectionLimitReached?: boolean; - /** Default: `null` */ - responseHeaders?: 'raw' | null; - } export interface RequestOptions extends DispatchOptions { /** Default: `null` */ opaque?: TOpaque; @@ -148,40 +121,9 @@ declare namespace Dispatcher { maxRedirections?: number; /** Default: false */ redirectionLimitReached?: boolean; - /** Default: `null` */ - onInfo?: (info: { statusCode: number, headers: Record }) => void; - /** Default: `null` */ - responseHeaders?: 'raw' | null; /** Default: `64 KiB` */ highWaterMark?: number; } - export interface PipelineOptions extends RequestOptions { - /** `true` if the `handler` will return an object stream. Default: `false` */ - objectMode?: boolean; - } - export interface UpgradeOptions { - path: string; - /** Default: `'GET'` */ - method?: string; - /** Default: `null` */ - headers?: IncomingHttpHeaders | string[] | null; - /** A string of comma separated protocols, in descending preference order. Default: `'Websocket'` */ - protocol?: string; - /** Default: `null` */ - signal?: AbortSignal | EventEmitter | null; - /** Default: 0 */ - maxRedirections?: number; - /** Default: false */ - redirectionLimitReached?: boolean; - /** Default: `null` */ - responseHeaders?: 'raw' | null; - } - export interface ConnectData { - statusCode: number; - headers: IncomingHttpHeaders; - socket: Duplex; - opaque: TOpaque; - } export interface ResponseData { statusCode: number; headers: IncomingHttpHeaders; @@ -190,29 +132,6 @@ declare namespace Dispatcher { opaque: TOpaque; context: object; } - export interface PipelineHandlerData { - statusCode: number; - headers: IncomingHttpHeaders; - opaque: TOpaque; - body: BodyReadable; - context: object; - } - export interface StreamData { - opaque: TOpaque; - trailers: Record; - } - export interface UpgradeData { - headers: IncomingHttpHeaders; - socket: Duplex; - opaque: TOpaque; - } - export interface StreamFactoryData { - statusCode: number; - headers: IncomingHttpHeaders; - opaque: TOpaque; - context: object; - } - export type StreamFactory = (data: StreamFactoryData) => Writable export interface DispatchHandlers { /** Invoked before request is dispatched on socket. May be invoked multiple times when a request is retried when the request at the head of the pipeline fails. */ onConnect?(abort: (err?: Error) => void): void; @@ -231,7 +150,6 @@ declare namespace Dispatcher { /** Invoked when a body chunk is sent to the server. May be invoked multiple times for chunked requests */ onBodySent?(chunkSize: number, totalBytesSent: number): void; } - export type PipelineHandler = (data: PipelineHandlerData) => Readable export type HttpMethod = Autocomplete<'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'CONNECT' | 'OPTIONS' | 'TRACE' | 'PATCH'> /** diff --git a/types/index.d.ts b/types/index.d.ts index 45276234925..b3670cbbcb3 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -17,7 +17,7 @@ import ProxyAgent from './proxy-agent' import EnvHttpProxyAgent from './env-http-proxy-agent' import RetryHandler from './retry-handler' import RetryAgent from './retry-agent' -import { request, pipeline, stream, connect, upgrade } from './api' +import { request } from './api' import interceptors from './interceptors' export * from './util' @@ -31,7 +31,7 @@ export * from './content-type' export * from './cache' export { Interceptable } from './mock-interceptor' -export { Dispatcher, BalancedPool, Pool, Client, buildConnector, errors, Agent, request, stream, pipeline, connect, upgrade, setGlobalDispatcher, getGlobalDispatcher, setGlobalOrigin, getGlobalOrigin, interceptors, MockClient, MockPool, MockAgent, mockErrors, ProxyAgent, EnvHttpProxyAgent, RedirectHandler, DecoratorHandler, RetryHandler, RetryAgent } +export { Dispatcher, BalancedPool, Pool, Client, buildConnector, errors, Agent, request, setGlobalDispatcher, getGlobalDispatcher, setGlobalOrigin, getGlobalOrigin, interceptors, MockClient, MockPool, MockAgent, mockErrors, ProxyAgent, EnvHttpProxyAgent, RedirectHandler, DecoratorHandler, RetryHandler, RetryAgent } export default Undici declare namespace Undici { @@ -49,10 +49,6 @@ declare namespace Undici { const setGlobalDispatcher: typeof import('./global-dispatcher').setGlobalDispatcher const getGlobalDispatcher: typeof import('./global-dispatcher').getGlobalDispatcher const request: typeof import('./api').request - const stream: typeof import('./api').stream - const pipeline: typeof import('./api').pipeline - const connect: typeof import('./api').connect - const upgrade: typeof import('./api').upgrade const MockClient: typeof import('./mock-client').default const MockPool: typeof import('./mock-pool').default const MockAgent: typeof import('./mock-agent').default