diff --git a/packages/kit/src/runtime/server/fetch.js b/packages/kit/src/runtime/server/fetch.js index 913cedaf9748..7403407a9477 100644 --- a/packages/kit/src/runtime/server/fetch.js +++ b/packages/kit/src/runtime/server/fetch.js @@ -145,10 +145,7 @@ export function create_fetch({ event, options, manifest, state, get_cookie_heade ); } - const response = await respond(request, options, manifest, { - ...state, - depth: state.depth + 1 - }); + const response = await internal_fetch(request, options, manifest, state); const set_cookie = response.headers.get('set-cookie'); if (set_cookie) { @@ -195,3 +192,43 @@ function normalize_fetch_input(info, init, url) { return new Request(typeof info === 'string' ? new URL(info, url) : info, init); } + +/** + * @param {Request} request + * @param {import('types').SSROptions} options + * @param {import('@sveltejs/kit').SSRManifest} manifest + * @param {import('types').SSRState} state + * @returns {Promise} + */ +async function internal_fetch(request, options, manifest, state) { + if (request.signal) { + if (request.signal.aborted) { + throw new DOMException('The operation was aborted.', 'AbortError'); + } + + let remove_abort_listener = () => {}; + /** @type {Promise} */ + const abort_promise = new Promise((_, reject) => { + const on_abort = () => { + reject(new DOMException('The operation was aborted.', 'AbortError')); + }; + request.signal.addEventListener('abort', on_abort, { once: true }); + remove_abort_listener = () => request.signal.removeEventListener('abort', on_abort); + }); + + const result = await Promise.race([ + respond(request, options, manifest, { + ...state, + depth: state.depth + 1 + }), + abort_promise + ]); + remove_abort_listener(); + return result; + } else { + return await respond(request, options, manifest, { + ...state, + depth: state.depth + 1 + }); + } +} diff --git a/packages/kit/test/apps/basics/src/routes/load/fetch-abort-signal/+page.server.js b/packages/kit/test/apps/basics/src/routes/load/fetch-abort-signal/+page.server.js new file mode 100644 index 000000000000..6cf4493e9ad6 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/load/fetch-abort-signal/+page.server.js @@ -0,0 +1,31 @@ +export async function load({ fetch }) { + const aborted_controller = new AbortController(); + aborted_controller.abort(); + + let aborted_immediately = false; + try { + await fetch('/load/fetch-abort-signal/data', { signal: aborted_controller.signal }); + } catch (error) { + if (error.name === 'AbortError') { + aborted_immediately = true; + } + } + + let aborted_during_request = false; + try { + await fetch('/load/fetch-abort-signal/slow', { signal: AbortSignal.timeout(100) }); + } catch (error) { + if (error.name === 'AbortError') { + aborted_during_request = true; + } + } + + const successful_response = await fetch('/load/fetch-abort-signal/data'); + const successful_data = await successful_response.json(); + + return { + aborted_immediately, + aborted_during_request, + successful_data + }; +} diff --git a/packages/kit/test/apps/basics/src/routes/load/fetch-abort-signal/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/fetch-abort-signal/+page.svelte new file mode 100644 index 000000000000..1867db965604 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/load/fetch-abort-signal/+page.svelte @@ -0,0 +1,10 @@ + + +
+

AbortSignal Test Results

+

Aborted immediately: {data.aborted_immediately}

+

Aborted during request: {data.aborted_during_request}

+

Successful data: {JSON.stringify(data.successful_data)}

+
diff --git a/packages/kit/test/apps/basics/src/routes/load/fetch-abort-signal/data/+server.js b/packages/kit/test/apps/basics/src/routes/load/fetch-abort-signal/data/+server.js new file mode 100644 index 000000000000..0c9615bc374e --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/load/fetch-abort-signal/data/+server.js @@ -0,0 +1,5 @@ +import { json } from '@sveltejs/kit'; + +export async function GET() { + return json({ message: 'success', timestamp: Date.now() }); +} diff --git a/packages/kit/test/apps/basics/src/routes/load/fetch-abort-signal/slow/+server.js b/packages/kit/test/apps/basics/src/routes/load/fetch-abort-signal/slow/+server.js new file mode 100644 index 000000000000..633b106c0c0e --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/load/fetch-abort-signal/slow/+server.js @@ -0,0 +1,3 @@ +export function GET() { + return new Promise(() => {}); +} diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js index 5abb712141a1..7f3bcf1910a1 100644 --- a/packages/kit/test/apps/basics/test/test.js +++ b/packages/kit/test/apps/basics/test/test.js @@ -569,6 +569,14 @@ test.describe('Load', () => { expect(await page.textContent('h1')).toBe('404'); }); + + test('AbortSignal works with internal fetch optimization', async ({ page }) => { + await page.goto('/load/fetch-abort-signal'); + + expect(await page.textContent('.aborted-immediately')).toBe('Aborted immediately: true'); + expect(await page.textContent('.aborted-during-request')).toBe('Aborted during request: true'); + expect(await page.textContent('.successful-data')).toContain('"message":"success"'); + }); }); test.describe('Nested layouts', () => {