diff --git a/example/__tests__/nitrofetch.harness.ts b/example/__tests__/nitrofetch.harness.ts index d5d18cf..ef1b800 100644 --- a/example/__tests__/nitrofetch.harness.ts +++ b/example/__tests__/nitrofetch.harness.ts @@ -531,6 +531,30 @@ describe('NitroFetch - AbortController', () => { expect(elapsed).toBeLessThan(5000); }); + it('abort after the response is buffered still rejects', async () => { + const controller = new AbortController(); + const pending = nitroFetch(`${BASE}/get`, { signal: controller.signal }); + + // Block the JS thread so native finishes while the continuation cannot run, + // then abort inside that window — cancel() is already a no-op by then. + const until = Date.now() + 1500; + let spins = 0; + while (Date.now() < until) { + spins += 1; + } + expect(spins).toBeGreaterThan(0); + controller.abort(); + + let threw = false; + try { + await pending; + } catch (e: any) { + threw = true; + expect(e.name).toBe('AbortError'); + } + expect(threw).toBe(true); + }); + it('normal fetch with signal (not aborted) succeeds', async () => { const controller = new AbortController(); const res = await nitroFetch(`${BASE}/get`, { diff --git a/packages/react-native-nitro-fetch/src/fetch.ts b/packages/react-native-nitro-fetch/src/fetch.ts index 89d9304..802cffb 100644 --- a/packages/react-native-nitro-fetch/src/fetch.ts +++ b/packages/react-native-nitro-fetch/src/fetch.ts @@ -734,6 +734,11 @@ async function nitroFetchRaw( try { const res: NitroResponseNative = await client.request(req); + // Native cancellation is best-effort — cancel() is a no-op once the + // response is buffered, so an aborted request can still resolve here. + if (signal?.aborted) { + throw createAbortError(); + } if (inspectorId) { NetworkInspector._recordEnd( inspectorId,