From e347493211bb1724e018429150e11f116d41d270 Mon Sep 17 00:00:00 2001 From: Yakov Zhmurov Date: Fri, 25 Oct 2024 12:30:31 +0300 Subject: [PATCH] [fix] ApiContext - handle aborts during response read/parse phase (.json() and alike) --- uui-core/src/services/ApiContext.ts | 5 + .../services/__tests__/ApiContexts.test.ts | 91 ++++++++++++++++++- 2 files changed, 92 insertions(+), 4 deletions(-) diff --git a/uui-core/src/services/ApiContext.ts b/uui-core/src/services/ApiContext.ts index 0e406be27e..59713a243d 100644 --- a/uui-core/src/services/ApiContext.ts +++ b/uui-core/src/services/ApiContext.ts @@ -241,6 +241,11 @@ export class ApiContext extends BaseContext implements IApiContext { this.resolveCall(call, result); }) .catch((e) => { + if (e?.name === 'AbortError') { + this.removeFromQueue(call); + return; + } + /* Problem with response JSON parsing */ call.status = 'error'; this.setStatus('error'); diff --git a/uui-core/src/services/__tests__/ApiContexts.test.ts b/uui-core/src/services/__tests__/ApiContexts.test.ts index 6cd2d53a49..83f08878cc 100644 --- a/uui-core/src/services/__tests__/ApiContexts.test.ts +++ b/uui-core/src/services/__tests__/ApiContexts.test.ts @@ -91,7 +91,7 @@ describe('ApiContext', () => { 'POST', testData, ); - await delay(100); + await delay(); const call = context.getActiveCalls()[0]; expect(call.status).toEqual('scheduled'); @@ -116,7 +116,7 @@ describe('ApiContext', () => { errorHandling: 'manual', }, ); - await delay(100); + await delay(); const call = context.getActiveCalls()[0]; expect(call.status).toEqual('scheduled'); @@ -140,7 +140,7 @@ describe('ApiContext', () => { 'POST', testData, ); - await delay(100); + await delay(); const call = context.getActiveCalls()[0]; expect(call.status).toEqual('scheduled'); @@ -166,7 +166,7 @@ describe('ApiContext', () => { errorHandling: 'manual', }, ); - await delay(100); + await delay(); const call = context.getActiveCalls()[0]; expect(call.status).toEqual('scheduled'); @@ -427,4 +427,87 @@ describe('ApiContext', () => { }, ); }); + + it('should be able to cancel calls during fetch() stage', async () => { + const fetchMock = (url, rq: RequestInit) => { + return new Promise((resolve, reject) => { + rq.signal!.addEventListener('abort', () => { + reject(new DOMException('Aborted', 'AbortError')); + }); + }); + }; + + global.fetch = fetchMock as any; + + const abortController = new AbortController(); + + context.processRequest( + 'path', + 'POST', + testData, + { + fetchOptions: { + signal: abortController.signal, + }, + }, + ); + + await delay(1); + + abortController.abort(); + + await delay(1); + + const calls = context.getActiveCalls(); + expect(calls.length).toEqual(0); + + // If you get a promise, you expect it to be either resolved or rejected. + // Unfortunately, our API doesn't do either historically, as error is handled in ApiContext + // We need to rethink this somehow later. + // await expect(apiPromise).rejects.toHaveAttribute('name', 'AbortError'); + }); + + it('should be able to cancel calls during json() stage', async () => { + const fetchMock = (url, rq: RequestInit) => { + return Promise.resolve({ + ok: true, + json: () => { + return new Promise((resolve, reject) => { + rq.signal!.addEventListener('abort', () => { + reject(new DOMException('Aborted', 'AbortError')); + }); + }); + }, + }); + }; + + global.fetch = fetchMock as any; + + const abortController = new AbortController(); + + context.processRequest( + 'path', + 'POST', + testData, + { + fetchOptions: { + signal: abortController.signal, + }, + }, + ); + + await delay(1); + + abortController.abort(); + + await delay(1); + + const calls = context.getActiveCalls(); + expect(calls.length).toEqual(0); + + // If you get a promise, you expect it to be either resolved or rejected. + // Unfortunately, our API doesn't do either historically, as error is handled in ApiContext + // We need to rethink this somehow later. + // await expect(apiPromise).rejects.toHaveAttribute('name', 'AbortError'); + }); });