diff --git a/src/HttpClient.ts b/src/HttpClient.ts index 7a550a48..cfc9697d 100644 --- a/src/HttpClient.ts +++ b/src/HttpClient.ts @@ -406,6 +406,7 @@ export class HttpClient extends EventEmitter { bodyTimeout, opaque: internalOpaque, dispatcher: args.dispatcher ?? this.#dispatcher, + signal: args.signal, }; if (typeof args.highWaterMark === 'number') { requestOptions.highWaterMark = args.highWaterMark; diff --git a/src/Request.ts b/src/Request.ts index 4a786ba7..44f61e96 100644 --- a/src/Request.ts +++ b/src/Request.ts @@ -1,4 +1,5 @@ import type { Readable, Writable } from 'node:stream'; +import type { EventEmitter } from 'node:events'; import type { Dispatcher } from 'undici'; import type { IncomingHttpHeaders } from './IncomingHttpHeaders.js'; import type { HttpClientResponse } from './Response.js'; @@ -10,6 +11,8 @@ export type RequestURL = string | URL; export type FixJSONCtlCharsHandler = (data: string) => string; export type FixJSONCtlChars = boolean | FixJSONCtlCharsHandler; +type AbortSignal = unknown; + export type RequestOptions = { /** Request method, defaults to GET. Could be GET, POST, DELETE or PUT. Alias 'type'. */ method?: HttpMethod | Lowercase; @@ -148,6 +151,7 @@ export type RequestOptions = { reset?: boolean; /** Default: `64 KiB` */ highWaterMark?: number; + signal?: AbortSignal | EventEmitter; }; export type RequestMeta = { diff --git a/test/options.signal.test.ts b/test/options.signal.test.ts new file mode 100644 index 00000000..3f2d4b85 --- /dev/null +++ b/test/options.signal.test.ts @@ -0,0 +1,56 @@ +import { strict as assert } from 'node:assert'; +import { EventEmitter } from 'node:events'; +import { describe, it, beforeAll, afterAll } from 'vitest'; +import urllib from '../src'; +import { startServer } from './fixtures/server'; +import { sleep } from './utils'; + +describe('options.signal.test.ts', () => { + let close: any; + let _url: string; + beforeAll(async () => { + const { closeServer, url } = await startServer(); + close = closeServer; + _url = url; + }); + + afterAll(async () => { + await close(); + }); + + it.skipIf(typeof global.AbortController === 'undefined')('should throw error when AbortController abort', async () => { + await assert.rejects(async () => { + const abortController = new AbortController(); + const p = urllib.request(`${_url}?timeout=2000`, { + signal: abortController.signal, + }); + await sleep(100); + abortController.abort(); + await p; + }, (err: any) => { + // console.error(err); + assert.equal(err.name, 'AbortError'); + assert.equal(err.message, 'Request aborted'); + assert.equal(err.code, 'UND_ERR_ABORTED'); + return true; + }); + }); + + it('should throw error when EventEmitter emit abort event', async () => { + await assert.rejects(async () => { + const ee = new EventEmitter(); + const p = urllib.request(`${_url}?timeout=2000`, { + signal: ee, + }); + await sleep(100); + ee.emit('abort'); + await p; + }, (err: any) => { + // console.error(err); + assert.equal(err.name, 'AbortError'); + assert.equal(err.message, 'Request aborted'); + assert.equal(err.code, 'UND_ERR_ABORTED'); + return true; + }); + }); +});