diff --git a/benchmark/index.ts b/benchmark/index.ts index 9f8a530da..ccf65e4a6 100644 --- a/benchmark/index.ts +++ b/benchmark/index.ts @@ -1,4 +1,3 @@ -import {URL} from 'node:url'; import https from 'node:https'; /// import axios from 'axios'; import Benchmark from 'benchmark'; diff --git a/documentation/3-streams.md b/documentation/3-streams.md index 5fe237cb9..8b1accede 100644 --- a/documentation/3-streams.md +++ b/documentation/3-streams.md @@ -27,29 +27,27 @@ This constructor takes the same arguments as the Got promise. > - If there's no body on purpose, remember to `stream.end()` or set the body option to an empty string. ```js -import {promisify} from 'node:util'; import stream from 'node:stream'; +import {pipeline as streamPipeline} from 'node:stream/promises'; import fs from 'node:fs'; import got from 'got'; -const pipeline = promisify(stream.pipeline); - // This example streams the GET response of a URL to a file. -await pipeline( +await streamPipeline( got.stream('https://sindresorhus.com'), fs.createWriteStream('index.html') ); // For POST, PUT, PATCH, and DELETE methods, `got.stream` returns a `stream.Writable`. // This example POSTs the contents of a file to a URL. -await pipeline( +await streamPipeline( fs.createReadStream('index.html'), got.stream.post('https://sindresorhus.com'), new stream.PassThrough() ); // In order to POST, PUT, PATCH, or DELETE without a request body, explicitly specify an empty body: -await pipeline( +await streamPipeline( got.stream.post('https://sindresorhus.com', { body: '' }), new stream.PassThrough() ) @@ -181,7 +179,7 @@ Whether the socket was used for other previous requests. This is emitted when a HTTP response is received. ```js -import {pipeline} from 'node:stream/promises'; +import {pipeline as streamPipeline} from 'node:stream/promises'; import {createWriteStream} from 'node:fs'; import got from 'got'; @@ -202,7 +200,7 @@ readStream.on('response', async response => { readStream.off('error', onError); try { - await pipeline( + await streamPipeline( readStream, createWriteStream('image.png') ); diff --git a/documentation/migration-guides/nodejs.md b/documentation/migration-guides/nodejs.md index 15c8028dd..6e88600ae 100644 --- a/documentation/migration-guides/nodejs.md +++ b/documentation/migration-guides/nodejs.md @@ -78,10 +78,10 @@ Well, it's easy as that: ```js import got from 'got'; -import stream from 'node:stream'; +import {pipeline as streamPipeline} from 'node:stream/promises'; import fs from 'node:fs'; -await stream.promises.pipeline( +await streamPipeline( fs.createReadStream('article.txt'), got.stream.post('https://httpbin.org/anything'), fs.createWriteStream('httpbin.txt') diff --git a/documentation/migration-guides/request.md b/documentation/migration-guides/request.md index b6e80bea6..e1c23982a 100644 --- a/documentation/migration-guides/request.md +++ b/documentation/migration-guides/request.md @@ -113,15 +113,12 @@ http.createServer((serverRequest, serverResponse) => { The cool feature here is that Request can proxy headers with the stream, but Got can do that too! ```js -import {promisify} from 'node:util'; -import stream from 'node:stream'; +import {pipeline as streamPipeline} from 'node:stream/promises'; import got from 'got'; -const pipeline = promisify(stream.pipeline); - const server = http.createServer(async (serverRequest, serverResponse) => { if (serverRequest.url === '/doodle.png') { - await pipeline( + await streamPipeline( got.stream('https://example.com/doodle.png'), serverResponse ); diff --git a/documentation/quick-start.md b/documentation/quick-start.md index 4785a9b57..7e473ce9b 100644 --- a/documentation/quick-start.md +++ b/documentation/quick-start.md @@ -65,7 +65,7 @@ The [Stream API](3-streams.md) allows to leverage [Node.js Streams](https://node ```js import fs from 'node:fs'; -import {pipeline} from 'node:stream/promises'; +import {pipeline as streamPipeline} from 'node:stream/promises'; import got from 'got'; const url = 'https://httpbin.org/anything'; @@ -81,7 +81,7 @@ const gotStream = got.stream.post(url, options); const outStream = fs.createWriteStream('anything.json'); try { - await pipeline(gotStream, outStream); + await streamPipeline(gotStream, outStream); } catch (error) { console.error(error); } diff --git a/package.json b/package.json index 9754d4635..b8e662931 100644 --- a/package.json +++ b/package.json @@ -6,10 +6,12 @@ "repository": "sindresorhus/got", "funding": "https://github.com/sindresorhus/got?sponsor=1", "type": "module", - "exports": "./dist/source/index.js", - "types": "./dist/source/index.d.ts", + "exports": { + "types": "./dist/source/index.d.ts", + "default": "./dist/source/index.js" + }, "engines": { - "node": ">=14.16" + "node": ">=16" }, "scripts": { "test": "xo && tsc --noEmit && ava", @@ -99,8 +101,8 @@ "tough-cookie": "4.1.2", "ts-node": "^10.8.2", "type-fest": "^3.6.1", - "typescript": "~4.9.5", - "xo": "^0.53.1" + "typescript": "^5.0.4", + "xo": "^0.54.2" }, "sideEffects": false, "ava": { @@ -135,8 +137,6 @@ "rules": { "@typescript-eslint/no-empty-function": "off", "n/no-deprecated-api": "off", - "n/prefer-global/url": "off", - "n/prefer-global/url-search-params": "off", "@typescript-eslint/no-implicit-any-catch": "off", "unicorn/prefer-node-protocol": "off", "ava/assertion-arguments": "off", @@ -146,6 +146,8 @@ "@typescript-eslint/no-unsafe-call": "off", "@typescript-eslint/await-thenable": "off", "@typescript-eslint/no-redundant-type-constituents": "off", + "@typescript-eslint/no-unsafe-argument": "off", + "@typescript-eslint/promise-function-async": "off", "no-lone-blocks": "off", "unicorn/no-await-expression-member": "off" } diff --git a/source/core/index.ts b/source/core/index.ts index 6f79377a5..dca359e29 100644 --- a/source/core/index.ts +++ b/source/core/index.ts @@ -1,7 +1,6 @@ import process from 'node:process'; import {Buffer} from 'node:buffer'; import {Duplex, type Readable} from 'node:stream'; -import {URL, URLSearchParams} from 'node:url'; import http, {ServerResponse} from 'node:http'; import type {ClientRequest, RequestOptions} from 'node:http'; import type {Socket} from 'node:net'; @@ -46,6 +45,8 @@ import { AbortError, } from './errors.js'; +const {buffer: getStreamAsBuffer} = getStream; + type Error = NodeJS.ErrnoException; export type Progress = { @@ -54,8 +55,6 @@ export type Progress = { total?: number; }; -const {buffer: getBuffer} = getStream; - const supportsBrotli = is.string(process.versions.brotli); const methodsWithoutBody: ReadonlySet = new Set(['GET', 'HEAD']); @@ -176,7 +175,7 @@ export default class Request extends Duplex implements RequestEvents { private _isFromCache?: boolean; private _cannotHaveBody: boolean; private _triggerRead: boolean; - declare private _jobs: Array<() => void>; + declare private readonly _jobs: Array<() => void>; private _cancelTimeouts: () => void; private readonly _removeListeners: () => void; private _nativeResponse?: IncomingMessageWithTimings; @@ -877,7 +876,11 @@ export default class Request extends Duplex implements RequestEvents { try { // Errors are emitted via the `error` event - const rawBody = await getBuffer(from); + const rawBody = await getStreamAsBuffer(from); + + // TODO: Switch to this: + // let rawBody = await from.toArray(); + // rawBody = Buffer.concat(rawBody); // On retry Request is destroyed with no error, therefore the above will successfully resolve. // So in order to check if this was really successfull, we need to check if it has been properly ended. @@ -992,7 +995,6 @@ export default class Request extends Duplex implements RequestEvents { // We only need to implement the error handler in order to support HTTP2 caching. // The result will be a promise anyway. // @ts-expect-error ignore - // eslint-disable-next-line @typescript-eslint/promise-function-async result.once = (event: string, handler: (reason: unknown) => void) => { if (event === 'error') { (async () => { diff --git a/source/core/options.ts b/source/core/options.ts index fa058a738..d2d932648 100644 --- a/source/core/options.ts +++ b/source/core/options.ts @@ -1,7 +1,6 @@ import process from 'node:process'; import type {Buffer} from 'node:buffer'; import {promisify, inspect} from 'node:util'; -import {URL, URLSearchParams} from 'node:url'; import {checkServerIdentity} from 'node:tls'; // DO NOT use destructuring for `https.request` and `http.request` as it's not compatible with `nock`. import http from 'node:http'; @@ -976,7 +975,7 @@ const init = (options: OptionsInit, withOptions: OptionsInit, self: Options): vo export default class Options { private _unixOptions?: NativeRequestOptions; - private _internals: InternalsType; + private readonly _internals: InternalsType; private _merging: boolean; private readonly _init: OptionsInit[]; diff --git a/source/core/response.ts b/source/core/response.ts index 256789aee..1ec3e4845 100644 --- a/source/core/response.ts +++ b/source/core/response.ts @@ -1,5 +1,4 @@ import type {Buffer} from 'node:buffer'; -import type {URL} from 'node:url'; import type {IncomingMessageWithTimings, Timings} from '@szmarczak/http-timer'; import {RequestError} from './errors.js'; import type {ParseJsonFunction, ResponseType} from './options.js'; diff --git a/source/core/timed-out.ts b/source/core/timed-out.ts index 079219358..9cdd79c04 100644 --- a/source/core/timed-out.ts +++ b/source/core/timed-out.ts @@ -90,7 +90,7 @@ export default function timedOut(request: ClientRequest, delays: Delays, options } }); - if (typeof delays.request !== 'undefined') { + if (delays.request !== undefined) { const cancelTimeout = addTimeout(delays.request, timeoutHandler, 'request'); once(request, 'response', (response: IncomingMessage): void => { @@ -98,7 +98,7 @@ export default function timedOut(request: ClientRequest, delays: Delays, options }); } - if (typeof delays.socket !== 'undefined') { + if (delays.socket !== undefined) { const {socket} = delays; const socketTimeoutHandler = (): void => { @@ -115,10 +115,10 @@ export default function timedOut(request: ClientRequest, delays: Delays, options }); } - const hasLookup = typeof delays.lookup !== 'undefined'; - const hasConnect = typeof delays.connect !== 'undefined'; - const hasSecureConnect = typeof delays.secureConnect !== 'undefined'; - const hasSend = typeof delays.send !== 'undefined'; + const hasLookup = delays.lookup !== undefined; + const hasConnect = delays.connect !== undefined; + const hasSecureConnect = delays.secureConnect !== undefined; + const hasSend = delays.send !== undefined; if (hasLookup || hasConnect || hasSecureConnect || hasSend) { once(request, 'socket', (socket: net.Socket): void => { const {socketPath} = request as ClientRequest & {socketPath?: string}; @@ -127,7 +127,7 @@ export default function timedOut(request: ClientRequest, delays: Delays, options if (socket.connecting) { const hasPath = Boolean(socketPath ?? net.isIP(hostname ?? host ?? '') !== 0); - if (hasLookup && !hasPath && typeof (socket.address() as net.AddressInfo).address === 'undefined') { + if (hasLookup && !hasPath && (socket.address() as net.AddressInfo).address === undefined) { const cancelTimeout = addTimeout(delays.lookup!, timeoutHandler, 'lookup'); once(socket, 'lookup', cancelTimeout); } @@ -168,14 +168,14 @@ export default function timedOut(request: ClientRequest, delays: Delays, options }); } - if (typeof delays.response !== 'undefined') { + if (delays.response !== undefined) { once(request, 'upload-complete', (): void => { const cancelTimeout = addTimeout(delays.response!, timeoutHandler, 'response'); once(request, 'response', cancelTimeout); }); } - if (typeof delays.read !== 'undefined') { + if (delays.read !== undefined) { once(request, 'response', (response: IncomingMessage): void => { const cancelTimeout = addTimeout(delays.read!, timeoutHandler, 'read'); once(response, 'end', cancelTimeout); diff --git a/source/core/utils/is-unix-socket-url.ts b/source/core/utils/is-unix-socket-url.ts index e96188bcd..dcebe64bb 100644 --- a/source/core/utils/is-unix-socket-url.ts +++ b/source/core/utils/is-unix-socket-url.ts @@ -1,5 +1,3 @@ -import type {URL} from 'url'; - // eslint-disable-next-line @typescript-eslint/naming-convention export default function isUnixSocketURL(url: URL) { return url.protocol === 'unix:' || url.hostname === 'unix'; diff --git a/source/core/utils/options-to-url.ts b/source/core/utils/options-to-url.ts index b5edd0421..44e66dd6c 100644 --- a/source/core/utils/options-to-url.ts +++ b/source/core/utils/options-to-url.ts @@ -1,6 +1,3 @@ -/* istanbul ignore file: deprecated */ -import {URL} from 'node:url'; - // eslint-disable-next-line @typescript-eslint/naming-convention export type URLOptions = { href?: string; diff --git a/source/core/utils/url-to-options.ts b/source/core/utils/url-to-options.ts index 13d2168e2..7a3baef61 100644 --- a/source/core/utils/url-to-options.ts +++ b/source/core/utils/url-to-options.ts @@ -1,4 +1,4 @@ -import type {URL, UrlWithStringQuery} from 'node:url'; +import type {UrlWithStringQuery} from 'node:url'; import is from '@sindresorhus/is'; // TODO: Deprecate legacy URL at some point diff --git a/source/create.ts b/source/create.ts index c40337bab..5a706973e 100644 --- a/source/create.ts +++ b/source/create.ts @@ -1,4 +1,3 @@ -import type {URL} from 'node:url'; import is, {assert} from '@sindresorhus/is'; import asPromise from './as-promise/index.js'; import type { diff --git a/source/types.ts b/source/types.ts index 9c34a4335..0a47e69d2 100644 --- a/source/types.ts +++ b/source/types.ts @@ -1,5 +1,4 @@ import type {Buffer} from 'node:buffer'; -import type {URL} from 'node:url'; import type {CancelableRequest} from './as-promise/types.js'; import type {Response} from './core/response.js'; import type Options from './core/options.js'; diff --git a/test/abort.ts b/test/abort.ts index 525e4e80c..890f87899 100644 --- a/test/abort.ts +++ b/test/abort.ts @@ -1,6 +1,7 @@ import process from 'process'; import {EventEmitter} from 'events'; -import stream, {Readable as ReadableStream} from 'stream'; +import {Readable as ReadableStream} from 'stream'; +import {pipeline as streamPipeline} from 'stream/promises'; import test from 'ava'; import delay from 'delay'; import {pEvent} from 'p-event'; @@ -12,318 +13,314 @@ import type {GlobalClock} from './helpers/types.js'; import type {ExtendedHttpTestServer} from './helpers/create-http-test-server.js'; import withServer, {withServerAndFakeTimers} from './helpers/with-server.js'; -// eslint-disable-next-line no-negated-condition -if (globalThis.AbortController !== undefined) { - const prepareServer = (server: ExtendedHttpTestServer, clock: GlobalClock): {emitter: EventEmitter; promise: Promise} => { - const emitter = new EventEmitter(); +const prepareServer = (server: ExtendedHttpTestServer, clock: GlobalClock): {emitter: EventEmitter; promise: Promise} => { + const emitter = new EventEmitter(); - const promise = new Promise((resolve, reject) => { - server.all('/abort', async (request, response) => { - emitter.emit('connection'); + const promise = new Promise((resolve, reject) => { + server.all('/abort', async (request, response) => { + emitter.emit('connection'); - request.once('aborted', resolve); - response.once('finish', reject.bind(null, new Error('Request finished instead of aborting.'))); + request.once('aborted', resolve); + response.once('finish', reject.bind(null, new Error('Request finished instead of aborting.'))); - try { - await pEvent(request, 'end'); - } catch { - // Node.js 15.0.0 throws AND emits `aborted` - } - - response.end(); - }); - - server.get('/redirect', (_request, response) => { - response.writeHead(302, { - location: `${server.url}/abort`, - }); - response.end(); + try { + await pEvent(request, 'end'); + } catch { + // Node.js 15.0.0 throws AND emits `aborted` + } - emitter.emit('sentRedirect'); + response.end(); + }); - clock.tick(3000); - resolve(); + server.get('/redirect', (_request, response) => { + response.writeHead(302, { + location: `${server.url}/abort`, }); - }); + response.end(); - return {emitter, promise}; - }; + emitter.emit('sentRedirect'); - const downloadHandler = (clock?: GlobalClock): Handler => (_request, response) => { - response.writeHead(200, { - 'transfer-encoding': 'chunked', + clock.tick(3000); + resolve(); }); + }); - response.flushHeaders(); - - stream.pipeline( - slowDataStream(clock), - response, - () => { - response.end(); - }, - ); - }; + return {emitter, promise}; +}; - const sandbox = createSandbox(); +const downloadHandler = (clock?: GlobalClock): Handler => (_request, response) => { + response.writeHead(200, { + 'transfer-encoding': 'chunked', + }); - const createAbortController = (): {controller: AbortController; signalHandlersRemoved: () => boolean} => { - const controller = new AbortController(); - sandbox.spy(controller.signal); - // @ts-expect-error AbortSignal type definition issue: https://github.com/DefinitelyTyped/DefinitelyTyped/discussions/57805 - const signalHandlersRemoved = () => controller.signal.addEventListener.callCount === controller.signal.removeEventListener.callCount; - return { - controller, signalHandlersRemoved, - }; + response.flushHeaders(); + + (async () => { + try { + await streamPipeline( + slowDataStream(clock), + response, + ); + } catch {} + + response.end(); + })(); +}; + +const sandbox = createSandbox(); + +const createAbortController = (): {controller: AbortController; signalHandlersRemoved: () => boolean} => { + const controller = new AbortController(); + sandbox.spy(controller.signal); + // @ts-expect-error AbortSignal type definition issue: https://github.com/DefinitelyTyped/DefinitelyTyped/discussions/57805 + const signalHandlersRemoved = () => controller.signal.addEventListener.callCount === controller.signal.removeEventListener.callCount; + return { + controller, signalHandlersRemoved, }; +}; + +test.afterEach(() => { + sandbox.restore(); +}); + +test.serial('does not retry after abort', withServerAndFakeTimers, async (t, server, got, clock) => { + const {emitter, promise} = prepareServer(server, clock); + const {controller, signalHandlersRemoved} = createAbortController(); + + const gotPromise = got('redirect', { + signal: controller.signal, + retry: { + calculateDelay() { + t.fail('Makes a new try after abort'); + return 0; + }, + }, + }); - test.afterEach(() => { - sandbox.restore(); + emitter.once('sentRedirect', () => { + controller.abort(); }); - test.serial('does not retry after abort', withServerAndFakeTimers, async (t, server, got, clock) => { - const {emitter, promise} = prepareServer(server, clock); - const {controller, signalHandlersRemoved} = createAbortController(); - - const gotPromise = got('redirect', { - signal: controller.signal, - retry: { - calculateDelay() { - t.fail('Makes a new try after abort'); - return 0; - }, - }, - }); + await t.throwsAsync(gotPromise, { + code: 'ERR_ABORTED', + message: 'This operation was aborted.', + }); - emitter.once('sentRedirect', () => { - controller.abort(); - }); + await t.notThrowsAsync(promise, 'Request finished instead of aborting.'); - await t.throwsAsync(gotPromise, { - code: 'ERR_ABORTED', - message: 'This operation was aborted.', - }); + t.true(signalHandlersRemoved(), 'Abort signal event handlers not removed'); +}); - await t.notThrowsAsync(promise, 'Request finished instead of aborting.'); +test.serial('abort request timeouts', withServer, async (t, server, got) => { + server.get('/', () => {}); - t.true(signalHandlersRemoved(), 'Abort signal event handlers not removed'); - }); + const {controller, signalHandlersRemoved} = createAbortController(); - test.serial('abort request timeouts', withServer, async (t, server, got) => { - server.get('/', () => {}); + const gotPromise = got({ + signal: controller.signal, + timeout: { + request: 10, + }, + retry: { + calculateDelay({computedValue}) { + process.nextTick(() => { + controller.abort(); + }); - const {controller, signalHandlersRemoved} = createAbortController(); + if (computedValue) { + return 20; + } - const gotPromise = got({ - signal: controller.signal, - timeout: { - request: 10, + return 0; }, - retry: { - calculateDelay({computedValue}) { - process.nextTick(() => { - controller.abort(); - }); - - if (computedValue) { - return 20; - } - - return 0; - }, - limit: 1, - }, - }); - - await t.throwsAsync(gotPromise, { - code: 'ERR_ABORTED', - message: 'This operation was aborted.', - }); - - t.true(signalHandlersRemoved(), 'Abort signal event handlers not removed'); + limit: 1, + }, + }); - // Wait for unhandled errors - await delay(40); + await t.throwsAsync(gotPromise, { + code: 'ERR_ABORTED', + message: 'This operation was aborted.', }); - test.serial('aborts in-progress request', withServerAndFakeTimers, async (t, server, got, clock) => { - const {emitter, promise} = prepareServer(server, clock); + t.true(signalHandlersRemoved(), 'Abort signal event handlers not removed'); - const {controller, signalHandlersRemoved} = createAbortController(); + // Wait for unhandled errors + await delay(40); +}); - const body = new ReadableStream({ - read() {}, - }); - body.push('1'); +test.serial('aborts in-progress request', withServerAndFakeTimers, async (t, server, got, clock) => { + const {emitter, promise} = prepareServer(server, clock); - const gotPromise = got.post('abort', {body, signal: controller.signal}); + const {controller, signalHandlersRemoved} = createAbortController(); - // Wait for the connection to be established before canceling - emitter.once('connection', () => { - controller.abort(); - body.push(null); - }); + const body = new ReadableStream({ + read() {}, + }); + body.push('1'); - await t.throwsAsync(gotPromise, { - code: 'ERR_ABORTED', - message: 'This operation was aborted.', - }); - await t.notThrowsAsync(promise, 'Request finished instead of aborting.'); + const gotPromise = got.post('abort', {body, signal: controller.signal}); - t.true(signalHandlersRemoved(), 'Abort signal event handlers not removed'); + // Wait for the connection to be established before canceling + emitter.once('connection', () => { + controller.abort(); + body.push(null); }); - test.serial('aborts in-progress request with timeout', withServerAndFakeTimers, async (t, server, got, clock) => { - const {emitter, promise} = prepareServer(server, clock); + await t.throwsAsync(gotPromise, { + code: 'ERR_ABORTED', + message: 'This operation was aborted.', + }); + await t.notThrowsAsync(promise, 'Request finished instead of aborting.'); - const {controller, signalHandlersRemoved} = createAbortController(); + t.true(signalHandlersRemoved(), 'Abort signal event handlers not removed'); +}); - const body = new ReadableStream({ - read() {}, - }); - body.push('1'); +test.serial('aborts in-progress request with timeout', withServerAndFakeTimers, async (t, server, got, clock) => { + const {emitter, promise} = prepareServer(server, clock); - const gotPromise = got.post('abort', {body, timeout: {request: 10_000}, signal: controller.signal}); + const {controller, signalHandlersRemoved} = createAbortController(); - // Wait for the connection to be established before canceling - emitter.once('connection', () => { - controller.abort(); - body.push(null); - }); + const body = new ReadableStream({ + read() {}, + }); + body.push('1'); - await t.throwsAsync(gotPromise, { - code: 'ERR_ABORTED', - message: 'This operation was aborted.', - }); - await t.notThrowsAsync(promise, 'Request finished instead of aborting.'); + const gotPromise = got.post('abort', {body, timeout: {request: 10_000}, signal: controller.signal}); - t.true(signalHandlersRemoved(), 'Abort signal event handlers not removed'); + // Wait for the connection to be established before canceling + emitter.once('connection', () => { + controller.abort(); + body.push(null); }); - test.serial('abort immediately', withServerAndFakeTimers, async (t, server, got, clock) => { - const {controller, signalHandlersRemoved} = createAbortController(); - - const promise = new Promise((resolve, reject) => { - // We won't get an abort or even a connection - // We assume no request within 1000ms equals a (client side) aborted request - server.get('/abort', (_request, response) => { - response.once('finish', reject.bind(global, new Error('Request finished instead of aborting.'))); - response.end(); - }); + await t.throwsAsync(gotPromise, { + code: 'ERR_ABORTED', + message: 'This operation was aborted.', + }); + await t.notThrowsAsync(promise, 'Request finished instead of aborting.'); - clock.tick(1000); - resolve(); - }); + t.true(signalHandlersRemoved(), 'Abort signal event handlers not removed'); +}); - const gotPromise = got('abort', {signal: controller.signal}); - controller.abort(); +test.serial('abort immediately', withServerAndFakeTimers, async (t, server, got, clock) => { + const {controller, signalHandlersRemoved} = createAbortController(); - await t.throwsAsync(gotPromise, { - code: 'ERR_ABORTED', - message: 'This operation was aborted.', + const promise = new Promise((resolve, reject) => { + // We won't get an abort or even a connection + // We assume no request within 1000ms equals a (client side) aborted request + server.get('/abort', (_request, response) => { + response.once('finish', reject.bind(global, new Error('Request finished instead of aborting.'))); + response.end(); }); - await t.notThrowsAsync(promise, 'Request finished instead of aborting.'); - t.true(signalHandlersRemoved(), 'Abort signal event handlers not removed'); + clock.tick(1000); + resolve(); }); - test('recover from abort using abortable promise attribute', async t => { - // Abort before connection started - const {controller, signalHandlersRemoved} = createAbortController(); + const gotPromise = got('abort', {signal: controller.signal}); + controller.abort(); - const p = got('http://example.com', {signal: controller.signal}); - const recover = p.catch((error: Error) => { - if (controller.signal.aborted) { - return; - } + await t.throwsAsync(gotPromise, { + code: 'ERR_ABORTED', + message: 'This operation was aborted.', + }); + await t.notThrowsAsync(promise, 'Request finished instead of aborting.'); - throw error; - }); + t.true(signalHandlersRemoved(), 'Abort signal event handlers not removed'); +}); - controller.abort(); +test('recover from abort using abortable promise attribute', async t => { + // Abort before connection started + const {controller, signalHandlersRemoved} = createAbortController(); - await t.notThrowsAsync(recover); + const p = got('http://example.com', {signal: controller.signal}); + const recover = p.catch((error: Error) => { + if (controller.signal.aborted) { + return; + } - t.true(signalHandlersRemoved(), 'Abort signal event handlers not removed'); + throw error; }); - test('recover from abort using error instance', async t => { - const {controller, signalHandlersRemoved} = createAbortController(); + controller.abort(); - const p = got('http://example.com', {signal: controller.signal}); - const recover = p.catch((error: Error) => { - if (error.message === 'This operation was aborted.') { - return; - } + await t.notThrowsAsync(recover); - throw error; - }); + t.true(signalHandlersRemoved(), 'Abort signal event handlers not removed'); +}); - controller.abort(); +test('recover from abort using error instance', async t => { + const {controller, signalHandlersRemoved} = createAbortController(); - await t.notThrowsAsync(recover); + const p = got('http://example.com', {signal: controller.signal}); + const recover = p.catch((error: Error) => { + if (error.message === 'This operation was aborted.') { + return; + } - t.true(signalHandlersRemoved(), 'Abort signal event handlers not removed'); + throw error; }); - // TODO: Use `fakeTimers` here - test.serial('throws on incomplete (aborted) response', withServer, async (t, server, got) => { - server.get('/', downloadHandler()); + controller.abort(); - const {controller, signalHandlersRemoved} = createAbortController(); + await t.notThrowsAsync(recover); - const promise = got('', {signal: controller.signal}); + t.true(signalHandlersRemoved(), 'Abort signal event handlers not removed'); +}); - setTimeout(() => { - controller.abort(); - }, 400); +// TODO: Use `fakeTimers` here +test.serial('throws on incomplete (aborted) response', withServer, async (t, server, got) => { + server.get('/', downloadHandler()); - await t.throwsAsync(promise, { - code: 'ERR_ABORTED', - message: 'This operation was aborted.', - }); + const {controller, signalHandlersRemoved} = createAbortController(); + + const promise = got('', {signal: controller.signal}); + + setTimeout(() => { + controller.abort(); + }, 400); - t.true(signalHandlersRemoved(), 'Abort signal event handlers not removed'); + await t.throwsAsync(promise, { + code: 'ERR_ABORTED', + message: 'This operation was aborted.', }); - test('throws when aborting cached request', withServer, async (t, server, got) => { - server.get('/', (_request, response) => { - response.setHeader('Cache-Control', 'public, max-age=60'); - response.end(Date.now().toString()); - }); + t.true(signalHandlersRemoved(), 'Abort signal event handlers not removed'); +}); - const cache = new Map(); +test('throws when aborting cached request', withServer, async (t, server, got) => { + server.get('/', (_request, response) => { + response.setHeader('Cache-Control', 'public, max-age=60'); + response.end(Date.now().toString()); + }); - await got({cache}); + const cache = new Map(); - const {controller, signalHandlersRemoved} = createAbortController(); - const promise = got({cache, signal: controller.signal}); - controller.abort(); + await got({cache}); - await t.throwsAsync(promise, { - code: 'ERR_ABORTED', - message: 'This operation was aborted.', - }); + const {controller, signalHandlersRemoved} = createAbortController(); + const promise = got({cache, signal: controller.signal}); + controller.abort(); - t.true(signalHandlersRemoved(), 'Abort signal event handlers not removed'); + await t.throwsAsync(promise, { + code: 'ERR_ABORTED', + message: 'This operation was aborted.', }); - test('support setting the signal as a default option', async t => { - const {controller, signalHandlersRemoved} = createAbortController(); + t.true(signalHandlersRemoved(), 'Abort signal event handlers not removed'); +}); - const got2 = got.extend({signal: controller.signal}); - const p = got2('http://example.com', {signal: controller.signal}); - controller.abort(); +test('support setting the signal as a default option', async t => { + const {controller, signalHandlersRemoved} = createAbortController(); - await t.throwsAsync(p, { - code: 'ERR_ABORTED', - message: 'This operation was aborted.', - }); + const got2 = got.extend({signal: controller.signal}); + const p = got2('http://example.com', {signal: controller.signal}); + controller.abort(); - t.true(signalHandlersRemoved(), 'Abort signal event handlers not removed'); + await t.throwsAsync(p, { + code: 'ERR_ABORTED', + message: 'This operation was aborted.', }); -} else { - test('x', t => { - t.pass(); - }); -} + + t.true(signalHandlersRemoved(), 'Abort signal event handlers not removed'); +}); diff --git a/test/arguments.ts b/test/arguments.ts index 4528c2da4..6088caad1 100644 --- a/test/arguments.ts +++ b/test/arguments.ts @@ -1,4 +1,4 @@ -import {parse, URL, URLSearchParams} from 'url'; +import {parse} from 'url'; import test from 'ava'; import type {Handler} from 'express'; import {pEvent} from 'p-event'; diff --git a/test/cancel.ts b/test/cancel.ts index deeb514d8..dcaf95b0a 100644 --- a/test/cancel.ts +++ b/test/cancel.ts @@ -1,6 +1,7 @@ import process from 'process'; import {EventEmitter} from 'events'; -import stream, {Readable as ReadableStream} from 'stream'; +import {Readable as ReadableStream} from 'stream'; +import {pipeline as streamPipeline} from 'stream/promises'; import test from 'ava'; import delay from 'delay'; import {pEvent} from 'p-event'; @@ -54,13 +55,16 @@ const downloadHandler = (clock?: GlobalClock): Handler => (_request, response) = response.flushHeaders(); - stream.pipeline( - slowDataStream(clock), - response, - () => { - response.end(); - }, - ); + (async () => { + try { + await streamPipeline( + slowDataStream(clock), + response, + ); + } catch {} + + response.end(); + })(); }; test.serial('does not retry after cancelation', withServerAndFakeTimers, async (t, server, got, clock) => { diff --git a/test/create.ts b/test/create.ts index 1b1df29a0..934321266 100644 --- a/test/create.ts +++ b/test/create.ts @@ -4,7 +4,6 @@ import { type IncomingMessage, type RequestOptions, } from 'http'; -import {URL} from 'url'; import test from 'ava'; import is from '@sindresorhus/is'; import type {Handler} from 'express'; diff --git a/test/error.ts b/test/error.ts index dcdf5dae4..28db17fb6 100644 --- a/test/error.ts +++ b/test/error.ts @@ -1,9 +1,9 @@ import {Buffer} from 'buffer'; -import {URL} from 'url'; import {promisify} from 'util'; import net from 'net'; import http from 'http'; import stream from 'stream'; +import {pipeline as streamPipeline} from 'stream/promises'; import test from 'ava'; import getStream from 'get-stream'; import is from '@sindresorhus/is'; @@ -12,8 +12,6 @@ import type Request from '../source/core/index.js'; import withServer from './helpers/with-server.js'; import invalidUrl from './helpers/invalid-url.js'; -const pStreamPipeline = promisify(stream.pipeline); - test('properties', withServer, async (t, server, got) => { server.get('/', (_request, response) => { response.statusCode = 404; @@ -57,7 +55,7 @@ test('`options.body` form error message', async t => { test('no plain object restriction on json body', withServer, async (t, server, got) => { server.post('/body', async (request, response) => { - await pStreamPipeline(request, response); + await streamPipeline(request, response); }); class CustomObject { diff --git a/test/hooks.ts b/test/hooks.ts index c550b218d..c649bd8d0 100644 --- a/test/hooks.ts +++ b/test/hooks.ts @@ -1,5 +1,4 @@ import {Buffer} from 'buffer'; -import {URL} from 'url'; import {Agent as HttpAgent} from 'http'; import test from 'ava'; import nock from 'nock'; diff --git a/test/normalize-arguments.ts b/test/normalize-arguments.ts index ca25c67e4..a5177ecbd 100644 --- a/test/normalize-arguments.ts +++ b/test/normalize-arguments.ts @@ -1,4 +1,3 @@ -import {URL, URLSearchParams} from 'url'; import test from 'ava'; import got, {Options} from '../source/index.js'; diff --git a/test/pagination.ts b/test/pagination.ts index ec511f922..0b9849b82 100644 --- a/test/pagination.ts +++ b/test/pagination.ts @@ -1,5 +1,4 @@ import {Buffer} from 'buffer'; -import {URL} from 'url'; import test from 'ava'; import delay from 'delay'; import getStream from 'get-stream'; diff --git a/test/post.ts b/test/post.ts index 2c325381e..53d757438 100644 --- a/test/post.ts +++ b/test/post.ts @@ -1,7 +1,7 @@ import process from 'process'; import {Buffer} from 'buffer'; -import {promisify} from 'util'; import stream from 'stream'; +import {pipeline as streamPipeline} from 'stream/promises'; import fs from 'fs'; import fsPromises from 'fs/promises'; import path from 'path'; @@ -17,11 +17,9 @@ import FormData from 'form-data'; import got, {UploadError} from '../source/index.js'; import withServer from './helpers/with-server.js'; -const pStreamPipeline = promisify(stream.pipeline); - const defaultEndpoint: Handler = async (request, response) => { response.setHeader('method', request.method); - await pStreamPipeline(request, response); + await streamPipeline(request, response); }; const echoHeaders: Handler = (request, response) => { diff --git a/test/progress.ts b/test/progress.ts index 37e4f2594..c71068e58 100644 --- a/test/progress.ts +++ b/test/progress.ts @@ -2,6 +2,7 @@ import process from 'process'; import {Buffer} from 'buffer'; import {promisify} from 'util'; import stream from 'stream'; +import {pipeline as streamPipeline} from 'node:stream/promises'; import fs from 'fs'; // @ts-expect-error Fails to find slow-stream/index.d.ts import SlowStream from 'slow-stream'; @@ -49,14 +50,17 @@ const file = Buffer.alloc(1024 * 1024 * 2); const downloadEndpoint: Handler = (_request, response) => { response.setHeader('content-length', file.length); - stream.pipeline( - stream.Readable.from(file), - new SlowStream({maxWriteInterval: 50}), - response, - () => { - response.end(); - }, - ); + (async () => { + try { + await streamPipeline( + stream.Readable.from(file), + new SlowStream({maxWriteInterval: 50}), + response, + ); + } catch {} + + response.end(); + })(); }; const noTotalEndpoint: Handler = (_request, response) => { @@ -65,13 +69,16 @@ const noTotalEndpoint: Handler = (_request, response) => { }; const uploadEndpoint: Handler = (request, response) => { - stream.pipeline( - request, - new SlowStream({maxWriteInterval: 100}), - () => { - response.end(); - }, - ); + (async () => { + try { + await streamPipeline( + request, + new SlowStream({maxWriteInterval: 100}), + ); + } catch {} + + response.end(); + })(); }; test('download progress', withServer, async (t, server, got) => { @@ -177,11 +184,12 @@ test('upload progress - stream with known body size', withServer, async (t, serv }; const request = got.stream.post(options) - .on('uploadProgress', event => events.push(event)); + .on('uploadProgress', event => { + events.push(event); + }); - await getStream( - stream.pipeline(stream.Readable.from(file), request, () => {}), - ); + await streamPipeline(stream.Readable.from(file), request); + await getStream(request); checkEvents(t, events, file.length); }); @@ -192,11 +200,12 @@ test('upload progress - stream with unknown body size', withServer, async (t, se const events: Progress[] = []; const request = got.stream.post('') - .on('uploadProgress', event => events.push(event)); + .on('uploadProgress', event => { + events.push(event); + }); - await getStream( - stream.pipeline(stream.Readable.from(file), request, () => {}), - ); + await streamPipeline(stream.Readable.from(file), request); + await getStream(request); t.is(events[0]?.total, undefined); checkEvents(t, events); diff --git a/test/stream.ts b/test/stream.ts index 2e1540d71..a7e704506 100644 --- a/test/stream.ts +++ b/test/stream.ts @@ -1,9 +1,9 @@ import process from 'process'; import {Buffer} from 'buffer'; -import {promisify} from 'util'; import fs from 'fs'; import {Agent as HttpAgent} from 'http'; import stream, {Readable as ReadableStream, Writable} from 'stream'; +import {pipeline as streamPipeline} from 'stream/promises'; import {Readable as Readable2} from 'readable-stream'; import test from 'ava'; import type {Handler} from 'express'; @@ -15,8 +15,6 @@ import delay from 'delay'; import got, {HTTPError, RequestError} from '../source/index.js'; import withServer from './helpers/with-server.js'; -const pStreamPipeline = promisify(stream.pipeline); - const defaultHandler: Handler = (_request, response) => { response.writeHead(200, { unicorn: 'rainbow', @@ -33,7 +31,7 @@ const redirectHandler: Handler = (_request, response) => { }; const postHandler: Handler = async (request, response) => { - await pStreamPipeline(request, response); + await streamPipeline(request, response); }; const errorHandler: Handler = (_request, response) => { @@ -196,7 +194,7 @@ test('piping works', withServer, async (t, server, got) => { test('proxying headers works', withServer, async (t, server, got) => { server.get('/', defaultHandler); server.get('/proxy', async (_request, response) => { - await pStreamPipeline( + await streamPipeline( got.stream(''), response, ); @@ -211,7 +209,7 @@ test('proxying headers works', withServer, async (t, server, got) => { test('piping server request to Got proxies also headers', withServer, async (t, server, got) => { server.get('/', headersHandler); server.get('/proxy', async (request, response) => { - await pStreamPipeline( + await streamPipeline( request, got.stream(''), response, @@ -231,7 +229,7 @@ test('skips proxying headers after server has sent them already', withServer, as server.get('/proxy', async (_request, response) => { response.writeHead(200); - await pStreamPipeline( + await streamPipeline( got.stream(''), response, ); @@ -244,7 +242,7 @@ test('skips proxying headers after server has sent them already', withServer, as test('proxies `content-encoding` header when `options.decompress` is false', withServer, async (t, server, got) => { server.get('/', defaultHandler); server.get('/proxy', async (_request, response) => { - await pStreamPipeline( + await streamPipeline( got.stream({decompress: false}), response, ); @@ -286,13 +284,14 @@ test('piping to got.stream.put()', withServer, async (t, server, got) => { server.put('/post', postHandler); await t.notThrowsAsync(async () => { - await getStream( - stream.pipeline( - got.stream(''), - got.stream.put('post'), - () => {}, - ), + const stream = got.stream.put('post'); + + await streamPipeline( + got.stream(''), + stream, ); + + await getStream(stream); }); }); @@ -310,7 +309,7 @@ test.skip('no unhandled body stream errors', async t => { }); test('works with pipeline', async t => { - await t.throwsAsync(pStreamPipeline( + await t.throwsAsync(streamPipeline( new ReadableStream({ read() { this.push(null); @@ -369,7 +368,7 @@ test('the socket is alive on a successful pipeline', withServer, async (t, serve t.is(gotStream.socket, undefined); const receiver = new stream.PassThrough(); - await promisify(stream.pipeline)(gotStream, receiver); + await streamPipeline(gotStream, receiver); t.is(await getStream(receiver), payload); t.truthy(gotStream.socket); diff --git a/test/timeout.ts b/test/timeout.ts index af56914b6..0b3b390a0 100644 --- a/test/timeout.ts +++ b/test/timeout.ts @@ -1,7 +1,7 @@ import process from 'process'; -import {promisify} from 'util'; import {EventEmitter} from 'events'; import stream, {PassThrough as PassThroughStream} from 'stream'; +import {pipeline as streamPipeline} from 'stream/promises'; import http from 'http'; import net from 'net'; import getStream from 'get-stream'; @@ -16,8 +16,6 @@ import slowDataStream from './helpers/slow-data-stream.js'; import type {GlobalClock} from './helpers/types.js'; import withServer, {withServerAndFakeTimers, withHttpsServer} from './helpers/with-server.js'; -const pStreamPipeline = promisify(stream.pipeline); - const requestDelay = 800; const errorMatcher = { @@ -44,7 +42,7 @@ const downloadHandler = (clock?: GlobalClock): Handler => (_request, response) = response.flushHeaders(); setImmediate(async () => { - await pStreamPipeline(slowDataStream(clock), response); + await streamPipeline(slowDataStream(clock), response); }); }; diff --git a/test/url-to-options.ts b/test/url-to-options.ts index cffe9623c..8337e3930 100644 --- a/test/url-to-options.ts +++ b/test/url-to-options.ts @@ -1,4 +1,4 @@ -import {parse as urlParse, URL} from 'url'; +import {parse as urlParse} from 'url'; import test from 'ava'; import urlToOptions from '../source/core/utils/url-to-options.js';