From 723b96030f245837dc37dff2ed878bca274a5f6e Mon Sep 17 00:00:00 2001 From: Gyubong Lee Date: Thu, 12 May 2022 12:19:43 +0900 Subject: [PATCH] Restore logic and fix tests --- source/as-promise/index.ts | 27 ++++++++++++++++++++++++++- source/core/errors.ts | 12 ++++++++++++ source/core/index.ts | 3 ++- source/create.ts | 20 ++++++++++++++++++-- 4 files changed, 58 insertions(+), 4 deletions(-) diff --git a/source/as-promise/index.ts b/source/as-promise/index.ts index 95ce06dd2..de236e544 100644 --- a/source/as-promise/index.ts +++ b/source/as-promise/index.ts @@ -5,6 +5,7 @@ import { RequestError, HTTPError, RetryError, + AbortError, } from '../core/errors.js'; import Request from '../core/index.js'; import {parseBody, isResponseOk} from '../core/response.js'; @@ -22,13 +23,37 @@ const proxiedRequestEvents = [ 'downloadProgress', ]; -export default function asPromise(firstRequest?: Request): CancelableRequest { +// eslint-disable-next-line @typescript-eslint/naming-convention +const getDOMException = (errorMessage: string) => globalThis.DOMException === undefined + ? new AbortError(errorMessage) + : new DOMException(errorMessage); + +const getAbortedReason = (signal: AbortSignal) => { + // To do: Remove below any castings when '@types/node' targets 18 + const reason = (signal as any).reason === undefined + ? getDOMException('This operation was aborted.') + : (signal as any).reason; + + return reason instanceof Error ? reason : getDOMException(reason); +}; + +export default function asPromise(firstRequest?: Request, signal?: AbortSignal): CancelableRequest { let globalRequest: Request; let globalResponse: Response; let normalizedOptions: Options; const emitter = new EventEmitter(); const promise = new PCancelable((resolve, reject, onCancel) => { + if (signal) { + if (signal.aborted) { + reject(getAbortedReason(signal)); + } + + signal.addEventListener('abort', () => { + reject(getAbortedReason(signal)); + }); + } + onCancel(() => { globalRequest.destroy(); }); diff --git a/source/core/errors.ts b/source/core/errors.ts index fc48166f2..b7277d989 100644 --- a/source/core/errors.ts +++ b/source/core/errors.ts @@ -170,3 +170,15 @@ export class RetryError extends RequestError { this.code = 'ERR_RETRYING'; } } + +/** +An error to be thrown when the request is aborted by AbortController. +DOMException is thrown instead of this Error when DOMException is available. +*/ +export class AbortError extends Error { + constructor(message: string) { + super(); + this.name = 'AbortError'; + this.message = message; + } +} diff --git a/source/core/index.ts b/source/core/index.ts index 3e1a35114..972401958 100644 --- a/source/core/index.ts +++ b/source/core/index.ts @@ -31,6 +31,7 @@ import { TimeoutError, UploadError, CacheError, + AbortError, } from './errors.js'; import type {PlainResponse} from './response.js'; import type {PromiseCookieJar, NativeRequestOptions, RetryOptions} from './options.js'; @@ -236,7 +237,7 @@ export default class Request extends Duplex implements RequestEvents { } this.options.signal?.addEventListener('abort', () => { - this.destroy(new Error('This operation was aborted.')); + this.destroy(new AbortError('This operation was aborted.')); }); // Important! If you replace `body` in a handler with another stream, make sure it's readable first. diff --git a/source/create.ts b/source/create.ts index 376fda30f..f5386e00a 100644 --- a/source/create.ts +++ b/source/create.ts @@ -25,6 +25,20 @@ const delay = async (ms: number) => new Promise(resolve => { const isGotInstance = (value: Got | ExtendOptions): value is Got => is.function_(value); +const getSignal = (url: string | URL | OptionsInit | undefined, options?: OptionsInit): AbortSignal | undefined => { + let signal; + + if (typeof url === 'object' && 'signal' in (url as OptionsInit)) { + signal = (url as OptionsInit).signal; + } + + if (options?.signal) { + signal = options.signal; + } + + return signal; +}; + const aliases: readonly HTTPAlias[] = [ 'get', 'post', @@ -52,6 +66,8 @@ const create = (defaults: InstanceDefaults): Got => { const request = new Request(url, options, defaultOptions); let promise: CancelableRequest | undefined; + const signal = getSignal(url, options); + const lastHandler = (normalized: Options): GotReturn => { // Note: `options` is `undefined` when `new Options(...)` fails request.options = normalized; @@ -63,7 +79,7 @@ const create = (defaults: InstanceDefaults): Got => { } if (!promise) { - promise = asPromise(request); + promise = asPromise(request, signal); } return promise; @@ -77,7 +93,7 @@ const create = (defaults: InstanceDefaults): Got => { if (is.promise(result) && !request.options.isStream) { if (!promise) { - promise = asPromise(request); + promise = asPromise(request, signal); } if (result !== promise) {