diff --git a/docs/canceling-requests.md b/docs/canceling-requests.md index e7cf76658..fa8422620 100644 --- a/docs/canceling-requests.md +++ b/docs/canceling-requests.md @@ -2,7 +2,9 @@ The generated clients support canceling of requests, this works by canceling the promise that is returned from the request. Each method inside a service (operation) returns a `CancelablePromise` -object. This promise can be canceled by calling the `cancel()` method. +object. This promise can be canceled by calling the `cancel()` method. This method takes an optional `reason` parameter +which can help e.g. differentiate timeouts from repeated request aborts. The promise will then be rejected with this +reason. Below is an example of canceling the request after a certain timeout: @@ -17,6 +19,9 @@ const getAllUsers = async () => { if (!request.isResolved() && !request.isRejected()) { console.warn('Canceling request due to timeout'); request.cancel(); + + // Or providing your custom error: + // request.cancel(new MyTimeoutError()); } }, 1000); @@ -32,11 +37,12 @@ interface CancelablePromise extends Promise { readonly isResolved: boolean; readonly isRejected: boolean; readonly isCancelled: boolean; - cancel: () => void; + cancel: (reason?: any) => void; } ``` - `isResolved`: Indicates if the promise was resolved. - `isRejected`: Indicates if the promise was rejected. - `isCancelled`: Indicates if the promise was canceled. -- `cancel()`: Cancels the promise (and request) and throws a rejection error: `Request aborted`. +- `cancel()`: Cancels the promise (and request) and throws either the specified reason or a general error: + `Request aborted`. diff --git a/src/templates/core/CancelablePromise.hbs b/src/templates/core/CancelablePromise.hbs index b7db949b8..def745d8d 100644 --- a/src/templates/core/CancelablePromise.hbs +++ b/src/templates/core/CancelablePromise.hbs @@ -17,14 +17,14 @@ export interface OnCancel { readonly isRejected: boolean; readonly isCancelled: boolean; - (cancelHandler: () => void): void; + (cancelHandler: (reason?: any) => void): void; } export class CancelablePromise implements Promise { #isResolved: boolean; #isRejected: boolean; #isCancelled: boolean; - readonly #cancelHandlers: (() => void)[]; + readonly #cancelHandlers: ((reason?: any) => void)[]; readonly #promise: Promise; #resolve?: (value: T | PromiseLike) => void; #reject?: (reason?: any) => void; @@ -60,7 +60,7 @@ export class CancelablePromise implements Promise { if (this.#reject) this.#reject(reason); }; - const onCancel = (cancelHandler: () => void): void => { + const onCancel = (cancelHandler: (reason?: any) => void): void => { if (this.#isResolved || this.#isRejected || this.#isCancelled) { return; } @@ -104,7 +104,7 @@ export class CancelablePromise implements Promise { return this.#promise.finally(onFinally); } - public cancel(): void { + public cancel(reason?: any): void { if (this.#isResolved || this.#isRejected || this.#isCancelled) { return; } @@ -112,7 +112,7 @@ export class CancelablePromise implements Promise { if (this.#cancelHandlers.length) { try { for (const cancelHandler of this.#cancelHandlers) { - cancelHandler(); + cancelHandler(reason); } } catch (error) { console.warn('Cancellation threw an error', error); @@ -120,7 +120,7 @@ export class CancelablePromise implements Promise { } } this.#cancelHandlers.length = 0; - if (this.#reject) this.#reject(new CancelError('Request aborted')); + if (this.#reject) this.#reject(reason || new CancelError('Request aborted')); } public get isCancelled(): boolean { diff --git a/src/templates/core/axios/sendRequest.hbs b/src/templates/core/axios/sendRequest.hbs index 51492bf3f..b2fc665f0 100644 --- a/src/templates/core/axios/sendRequest.hbs +++ b/src/templates/core/axios/sendRequest.hbs @@ -19,7 +19,7 @@ export const sendRequest = async ( cancelToken: source.token, }; - onCancel(() => source.cancel('The user aborted a request.')); + onCancel((reason) => source.cancel(reason?.toString?.() ?? 'The user aborted a request.')); try { return await axiosClient.request(requestConfig); diff --git a/src/templates/core/fetch/sendRequest.hbs b/src/templates/core/fetch/sendRequest.hbs index 73f71f428..3a20eac6b 100644 --- a/src/templates/core/fetch/sendRequest.hbs +++ b/src/templates/core/fetch/sendRequest.hbs @@ -20,7 +20,7 @@ export const sendRequest = async ( request.credentials = config.CREDENTIALS; } - onCancel(() => controller.abort()); + onCancel((reason) => controller.abort(reason)); return await fetch(url, request); }; diff --git a/src/templates/core/node/sendRequest.hbs b/src/templates/core/node/sendRequest.hbs index a2ebf86d4..1acf7bf8f 100644 --- a/src/templates/core/node/sendRequest.hbs +++ b/src/templates/core/node/sendRequest.hbs @@ -15,7 +15,7 @@ export const sendRequest = async ( signal: controller.signal as AbortSignal, }; - onCancel(() => controller.abort()); + onCancel((reason) => controller.abort(reason)); return await fetch(url, request); }; diff --git a/test/__snapshots__/index.spec.ts.snap b/test/__snapshots__/index.spec.ts.snap index ef6b18554..b94df0da8 100644 --- a/test/__snapshots__/index.spec.ts.snap +++ b/test/__snapshots__/index.spec.ts.snap @@ -87,14 +87,14 @@ export interface OnCancel { readonly isRejected: boolean; readonly isCancelled: boolean; - (cancelHandler: () => void): void; + (cancelHandler: (reason?: any) => void): void; } export class CancelablePromise implements Promise { #isResolved: boolean; #isRejected: boolean; #isCancelled: boolean; - readonly #cancelHandlers: (() => void)[]; + readonly #cancelHandlers: ((reason?: any) => void)[]; readonly #promise: Promise; #resolve?: (value: T | PromiseLike) => void; #reject?: (reason?: any) => void; @@ -130,7 +130,7 @@ export class CancelablePromise implements Promise { if (this.#reject) this.#reject(reason); }; - const onCancel = (cancelHandler: () => void): void => { + const onCancel = (cancelHandler: (reason?: any) => void): void => { if (this.#isResolved || this.#isRejected || this.#isCancelled) { return; } @@ -174,7 +174,7 @@ export class CancelablePromise implements Promise { return this.#promise.finally(onFinally); } - public cancel(): void { + public cancel(reason?: any): void { if (this.#isResolved || this.#isRejected || this.#isCancelled) { return; } @@ -182,7 +182,7 @@ export class CancelablePromise implements Promise { if (this.#cancelHandlers.length) { try { for (const cancelHandler of this.#cancelHandlers) { - cancelHandler(); + cancelHandler(reason); } } catch (error) { console.warn('Cancellation threw an error', error); @@ -190,7 +190,7 @@ export class CancelablePromise implements Promise { } } this.#cancelHandlers.length = 0; - if (this.#reject) this.#reject(new CancelError('Request aborted')); + if (this.#reject) this.#reject(reason ?? new CancelError('Request aborted')); } public get isCancelled(): boolean { @@ -451,7 +451,7 @@ export const sendRequest = async ( request.credentials = config.CREDENTIALS; } - onCancel(() => controller.abort()); + onCancel((reason) => controller.abort(reason)); return await fetch(url, request); }; @@ -3180,14 +3180,14 @@ export interface OnCancel { readonly isRejected: boolean; readonly isCancelled: boolean; - (cancelHandler: () => void): void; + (cancelHandler: (reason?: any) => void): void; } export class CancelablePromise implements Promise { #isResolved: boolean; #isRejected: boolean; #isCancelled: boolean; - readonly #cancelHandlers: (() => void)[]; + readonly #cancelHandlers: ((reason?: any) => void)[]; readonly #promise: Promise; #resolve?: (value: T | PromiseLike) => void; #reject?: (reason?: any) => void; @@ -3223,7 +3223,7 @@ export class CancelablePromise implements Promise { if (this.#reject) this.#reject(reason); }; - const onCancel = (cancelHandler: () => void): void => { + const onCancel = (cancelHandler: (reason?: any) => void): void => { if (this.#isResolved || this.#isRejected || this.#isCancelled) { return; } @@ -3267,7 +3267,7 @@ export class CancelablePromise implements Promise { return this.#promise.finally(onFinally); } - public cancel(): void { + public cancel(reason?: any): void { if (this.#isResolved || this.#isRejected || this.#isCancelled) { return; } @@ -3275,7 +3275,7 @@ export class CancelablePromise implements Promise { if (this.#cancelHandlers.length) { try { for (const cancelHandler of this.#cancelHandlers) { - cancelHandler(); + cancelHandler(reason); } } catch (error) { console.warn('Cancellation threw an error', error); @@ -3283,7 +3283,7 @@ export class CancelablePromise implements Promise { } } this.#cancelHandlers.length = 0; - if (this.#reject) this.#reject(new CancelError('Request aborted')); + if (this.#reject) this.#reject(reason ?? new CancelError('Request aborted')); } public get isCancelled(): boolean { @@ -3544,7 +3544,7 @@ export const sendRequest = async ( request.credentials = config.CREDENTIALS; } - onCancel(() => controller.abort()); + onCancel((reason) => controller.abort(reason)); return await fetch(url, request); }; diff --git a/test/e2e/client.axios.spec.ts b/test/e2e/client.axios.spec.ts index a0ba54750..7ac3055cb 100644 --- a/test/e2e/client.axios.spec.ts +++ b/test/e2e/client.axios.spec.ts @@ -84,6 +84,23 @@ describe('client.axios', () => { expect(error).toContain('Request aborted'); }); + it('can abort the request with custom reason', async () => { + const reason = 'Timed out!'; + let error; + try { + const { ApiClient } = require('./generated/client/axios/index.js'); + const client = new ApiClient(); + const promise = client.simple.getCallWithoutParametersAndResponse(); + setTimeout(() => { + promise.cancel(new Error(reason)); + }, 10); + await promise; + } catch (e) { + error = (e as Error).message; + } + expect(error).toContain(reason); + }); + it('should throw known error (500)', async () => { let error; try { diff --git a/test/e2e/client.babel.spec.ts b/test/e2e/client.babel.spec.ts index 82320d1f9..949f305b4 100644 --- a/test/e2e/client.babel.spec.ts +++ b/test/e2e/client.babel.spec.ts @@ -101,6 +101,25 @@ describe('client.babel', () => { expect(error).toContain('Request aborted'); }); + it('can abort the request with custom error', async () => { + const reason = 'Timed out!'; + let error; + try { + await browser.evaluate(async errMsg => { + const { ApiClient } = (window as any).api; + const client = new ApiClient(); + const promise = client.simple.getCallWithoutParametersAndResponse(); + setTimeout(() => { + promise.cancel(new Error(errMsg)); + }, 10); + await promise; + }, reason); + } catch (e) { + error = (e as Error).message; + } + expect(error).toContain(reason); + }); + it('should throw known error (500)', async () => { const error = await browser.evaluate(async () => { try { diff --git a/test/e2e/client.fetch.spec.ts b/test/e2e/client.fetch.spec.ts index 9a64284bd..2f83d1e86 100644 --- a/test/e2e/client.fetch.spec.ts +++ b/test/e2e/client.fetch.spec.ts @@ -99,6 +99,25 @@ describe('client.fetch', () => { expect(error).toContain('Request aborted'); }); + it('can abort the request with custom reason', async () => { + const reason = 'Timeout!'; + let error; + try { + await browser.evaluate(async errMsg => { + const { ApiClient } = (window as any).api; + const client = new ApiClient(); + const promise = client.simple.getCallWithoutParametersAndResponse(); + setTimeout(() => { + promise.cancel(new Error(errMsg)); + }, 10); + await promise; + }, reason); + } catch (e) { + error = (e as Error).message; + } + expect(error).toContain(reason); + }); + it('should throw known error (500)', async () => { const error = await browser.evaluate(async () => { try { diff --git a/test/e2e/client.node.spec.ts b/test/e2e/client.node.spec.ts index 0d915919b..f789da864 100644 --- a/test/e2e/client.node.spec.ts +++ b/test/e2e/client.node.spec.ts @@ -84,6 +84,23 @@ describe('client.node', () => { expect(error).toContain('Request aborted'); }); + it('can abort the request with custom reason', async () => { + const reason = 'Timed out!'; + let error; + try { + const { ApiClient } = require('./generated/client/node/index.js'); + const client = new ApiClient(); + const promise = client.simple.getCallWithoutParametersAndResponse(); + setTimeout(() => { + promise.cancel(new Error(reason)); + }, 10); + await promise; + } catch (e) { + error = (e as Error).message; + } + expect(error).toContain(reason); + }); + it('should throw known error (500)', async () => { let error; try { diff --git a/test/e2e/client.xhr.spec.ts b/test/e2e/client.xhr.spec.ts index e0b516f39..e69e52a78 100644 --- a/test/e2e/client.xhr.spec.ts +++ b/test/e2e/client.xhr.spec.ts @@ -99,6 +99,25 @@ describe('client.xhr', () => { expect(error).toContain('Request aborted'); }); + it('can abort the request with custom reason', async () => { + const reason = 'Timed out!'; + let error; + try { + await browser.evaluate(async errMsg => { + const { ApiClient } = (window as any).api; + const client = new ApiClient(); + const promise = client.simple.getCallWithoutParametersAndResponse(); + setTimeout(() => { + promise.cancel(new Error(errMsg)); + }, 10); + await promise; + }, reason); + } catch (e) { + error = (e as Error).message; + } + expect(error).toContain(reason); + }); + it('should throw known error (500)', async () => { const error = await browser.evaluate(async () => { try { diff --git a/test/e2e/scripts/browser.ts b/test/e2e/scripts/browser.ts index ab0b96bc5..5417d0933 100644 --- a/test/e2e/scripts/browser.ts +++ b/test/e2e/scripts/browser.ts @@ -23,8 +23,8 @@ const stop = async () => { await _browser.close(); }; -const evaluate = async (fn: EvaluateFn) => { - return await _page.evaluate(fn); +const evaluate = async (fn: EvaluateFn, ...args: any[]) => { + return await _page.evaluate(fn, ...args); }; // eslint-disable-next-line @typescript-eslint/ban-types