From f4251a533ac3c82cc0128db1fa7a3d9747a6959c Mon Sep 17 00:00:00 2001 From: "S. Elliott Johnson" Date: Tue, 10 Jun 2025 12:26:56 -0600 Subject: [PATCH 1/5] feat: Respect abort signals during serverside fetch optimization --- packages/kit/src/runtime/server/fetch.js | 41 +++++++++++++++++-- .../load/fetch-abort-signal/+page.server.js | 31 ++++++++++++++ .../load/fetch-abort-signal/+page.svelte | 10 +++++ .../load/fetch-abort-signal/data/+server.js | 5 +++ .../load/fetch-abort-signal/slow/+server.js | 3 ++ packages/kit/test/apps/basics/test/test.js | 8 ++++ 6 files changed, 94 insertions(+), 4 deletions(-) create mode 100644 packages/kit/test/apps/basics/src/routes/load/fetch-abort-signal/+page.server.js create mode 100644 packages/kit/test/apps/basics/src/routes/load/fetch-abort-signal/+page.svelte create mode 100644 packages/kit/test/apps/basics/src/routes/load/fetch-abort-signal/data/+server.js create mode 100644 packages/kit/test/apps/basics/src/routes/load/fetch-abort-signal/slow/+server.js diff --git a/packages/kit/src/runtime/server/fetch.js b/packages/kit/src/runtime/server/fetch.js index 913cedaf9748..99aa2a74cb8c 100644 --- a/packages/kit/src/runtime/server/fetch.js +++ b/packages/kit/src/runtime/server/fetch.js @@ -4,6 +4,42 @@ import * as paths from '__sveltekit/paths'; import { read_implementation } from '__sveltekit/server'; import { has_prerendered_path } from './utils.js'; +/** + * @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'); + } + + /** @type {Promise} */ + const abortPromise = new Promise((_, reject) => { + const onAbort = () => { + reject(new DOMException('The operation was aborted.', 'AbortError')); + }; + request.signal.addEventListener('abort', onAbort, { once: true }); + }); + + return await Promise.race([ + respond(request, options, manifest, { + ...state, + depth: state.depth + 1 + }), + abortPromise + ]); + } else { + return await respond(request, options, manifest, { + ...state, + depth: state.depth + 1 + }); + } +} + /** * @param {{ * event: import('@sveltejs/kit').RequestEvent; @@ -145,10 +181,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) { 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..448335f26eef --- /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 abortedController = new AbortController(); + abortedController.abort(); + + let abortedImmediately = false; + try { + await fetch('/load/fetch-abort-signal/data', { signal: abortedController.signal }); + } catch (error) { + if (error.name === 'AbortError') { + abortedImmediately = true; + } + } + + let abortedDuringRequest = false; + try { + await fetch('/load/fetch-abort-signal/slow', { signal: AbortSignal.timeout(100) }); + } catch (error) { + if (error.name === 'AbortError') { + abortedDuringRequest = true; + } + } + + const successfulResponse = await fetch('/load/fetch-abort-signal/data'); + const successfulData = await successfulResponse.json(); + + return { + abortedImmediately, + abortedDuringRequest, + successfulData + }; +} \ No newline at end of file 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..41d20e199602 --- /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.abortedImmediately}

+

Aborted during request: {data.abortedDuringRequest}

+

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

+
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..5eb037c64c43 --- /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() }); +} \ No newline at end of file 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..e67ca2baf9af --- /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(() => {}) +} \ No newline at end of file diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js index 5abb712141a1..caf81e0650c0 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.only('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', () => { From 4ca2e17535af45294127b4f744b598659a3186b4 Mon Sep 17 00:00:00 2001 From: "S. Elliott Johnson" Date: Tue, 10 Jun 2025 12:28:57 -0600 Subject: [PATCH 2/5] move --- packages/kit/src/runtime/server/fetch.js | 72 ++++++++++++------------ 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/packages/kit/src/runtime/server/fetch.js b/packages/kit/src/runtime/server/fetch.js index 99aa2a74cb8c..1749d3f40abf 100644 --- a/packages/kit/src/runtime/server/fetch.js +++ b/packages/kit/src/runtime/server/fetch.js @@ -4,42 +4,6 @@ import * as paths from '__sveltekit/paths'; import { read_implementation } from '__sveltekit/server'; import { has_prerendered_path } from './utils.js'; -/** - * @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'); - } - - /** @type {Promise} */ - const abortPromise = new Promise((_, reject) => { - const onAbort = () => { - reject(new DOMException('The operation was aborted.', 'AbortError')); - }; - request.signal.addEventListener('abort', onAbort, { once: true }); - }); - - return await Promise.race([ - respond(request, options, manifest, { - ...state, - depth: state.depth + 1 - }), - abortPromise - ]); - } else { - return await respond(request, options, manifest, { - ...state, - depth: state.depth + 1 - }); - } -} - /** * @param {{ * event: import('@sveltejs/kit').RequestEvent; @@ -228,3 +192,39 @@ 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'); + } + + /** @type {Promise} */ + const abortPromise = new Promise((_, reject) => { + const onAbort = () => { + reject(new DOMException('The operation was aborted.', 'AbortError')); + }; + request.signal.addEventListener('abort', onAbort, { once: true }); + }); + + return await Promise.race([ + respond(request, options, manifest, { + ...state, + depth: state.depth + 1 + }), + abortPromise + ]); + } else { + return await respond(request, options, manifest, { + ...state, + depth: state.depth + 1 + }); + } +} \ No newline at end of file From edd8be2def8dfd6d4d880a248e09182d344609f3 Mon Sep 17 00:00:00 2001 From: "S. Elliott Johnson" Date: Tue, 10 Jun 2025 15:28:35 -0600 Subject: [PATCH 3/5] lint --- packages/kit/src/runtime/server/fetch.js | 2 +- .../src/routes/load/fetch-abort-signal/+page.server.js | 10 +++++----- .../src/routes/load/fetch-abort-signal/data/+server.js | 2 +- .../src/routes/load/fetch-abort-signal/slow/+server.js | 4 ++-- packages/kit/test/apps/basics/test/test.js | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/kit/src/runtime/server/fetch.js b/packages/kit/src/runtime/server/fetch.js index 1749d3f40abf..ed68cc23d746 100644 --- a/packages/kit/src/runtime/server/fetch.js +++ b/packages/kit/src/runtime/server/fetch.js @@ -227,4 +227,4 @@ async function internal_fetch(request, options, manifest, state) { depth: state.depth + 1 }); } -} \ No newline at end of file +} 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 index 448335f26eef..a09839c974f2 100644 --- 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 @@ -1,7 +1,7 @@ export async function load({ fetch }) { const abortedController = new AbortController(); abortedController.abort(); - + let abortedImmediately = false; try { await fetch('/load/fetch-abort-signal/data', { signal: abortedController.signal }); @@ -10,7 +10,7 @@ export async function load({ fetch }) { abortedImmediately = true; } } - + let abortedDuringRequest = false; try { await fetch('/load/fetch-abort-signal/slow', { signal: AbortSignal.timeout(100) }); @@ -19,13 +19,13 @@ export async function load({ fetch }) { abortedDuringRequest = true; } } - + const successfulResponse = await fetch('/load/fetch-abort-signal/data'); const successfulData = await successfulResponse.json(); - + return { abortedImmediately, abortedDuringRequest, successfulData }; -} \ No newline at end of file +} 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 index 5eb037c64c43..0c9615bc374e 100644 --- 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 @@ -2,4 +2,4 @@ import { json } from '@sveltejs/kit'; export async function GET() { return json({ message: 'success', timestamp: Date.now() }); -} \ No newline at end of file +} 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 index e67ca2baf9af..633b106c0c0e 100644 --- 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 @@ -1,3 +1,3 @@ export function GET() { - return new Promise(() => {}) -} \ No newline at end of file + return new Promise(() => {}); +} diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js index caf81e0650c0..7f3bcf1910a1 100644 --- a/packages/kit/test/apps/basics/test/test.js +++ b/packages/kit/test/apps/basics/test/test.js @@ -570,7 +570,7 @@ test.describe('Load', () => { expect(await page.textContent('h1')).toBe('404'); }); - test.only('AbortSignal works with internal fetch optimization', async ({ page }) => { + 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'); From 53696bf833dbc9c1e33c8a80ffd03673e2751774 Mon Sep 17 00:00:00 2001 From: "S. Elliott Johnson" Date: Wed, 11 Jun 2025 13:06:27 -0600 Subject: [PATCH 4/5] snake_case --- packages/kit/src/runtime/server/fetch.js | 8 +++---- .../load/fetch-abort-signal/+page.server.js | 24 +++++++++---------- .../load/fetch-abort-signal/+page.svelte | 6 ++--- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/packages/kit/src/runtime/server/fetch.js b/packages/kit/src/runtime/server/fetch.js index ed68cc23d746..e58a84cbdf9d 100644 --- a/packages/kit/src/runtime/server/fetch.js +++ b/packages/kit/src/runtime/server/fetch.js @@ -207,11 +207,11 @@ async function internal_fetch(request, options, manifest, state) { } /** @type {Promise} */ - const abortPromise = new Promise((_, reject) => { - const onAbort = () => { + const abort_promise = new Promise((_, reject) => { + const on_abort = () => { reject(new DOMException('The operation was aborted.', 'AbortError')); }; - request.signal.addEventListener('abort', onAbort, { once: true }); + request.signal.addEventListener('abort', on_abort, { once: true }); }); return await Promise.race([ @@ -219,7 +219,7 @@ async function internal_fetch(request, options, manifest, state) { ...state, depth: state.depth + 1 }), - abortPromise + abort_promise ]); } else { return await respond(request, options, manifest, { 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 index a09839c974f2..6cf4493e9ad6 100644 --- 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 @@ -1,31 +1,31 @@ export async function load({ fetch }) { - const abortedController = new AbortController(); - abortedController.abort(); + const aborted_controller = new AbortController(); + aborted_controller.abort(); - let abortedImmediately = false; + let aborted_immediately = false; try { - await fetch('/load/fetch-abort-signal/data', { signal: abortedController.signal }); + await fetch('/load/fetch-abort-signal/data', { signal: aborted_controller.signal }); } catch (error) { if (error.name === 'AbortError') { - abortedImmediately = true; + aborted_immediately = true; } } - let abortedDuringRequest = false; + let aborted_during_request = false; try { await fetch('/load/fetch-abort-signal/slow', { signal: AbortSignal.timeout(100) }); } catch (error) { if (error.name === 'AbortError') { - abortedDuringRequest = true; + aborted_during_request = true; } } - const successfulResponse = await fetch('/load/fetch-abort-signal/data'); - const successfulData = await successfulResponse.json(); + const successful_response = await fetch('/load/fetch-abort-signal/data'); + const successful_data = await successful_response.json(); return { - abortedImmediately, - abortedDuringRequest, - successfulData + 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 index 41d20e199602..1867db965604 100644 --- 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 @@ -4,7 +4,7 @@

AbortSignal Test Results

-

Aborted immediately: {data.abortedImmediately}

-

Aborted during request: {data.abortedDuringRequest}

-

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

+

Aborted immediately: {data.aborted_immediately}

+

Aborted during request: {data.aborted_during_request}

+

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

From d1699971151666f8f8f65310a7b49af906951ba8 Mon Sep 17 00:00:00 2001 From: "S. Elliott Johnson" Date: Wed, 11 Jun 2025 15:17:39 -0600 Subject: [PATCH 5/5] chore: Remove abort listener --- packages/kit/src/runtime/server/fetch.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/kit/src/runtime/server/fetch.js b/packages/kit/src/runtime/server/fetch.js index e58a84cbdf9d..7403407a9477 100644 --- a/packages/kit/src/runtime/server/fetch.js +++ b/packages/kit/src/runtime/server/fetch.js @@ -206,21 +206,25 @@ async function internal_fetch(request, options, manifest, state) { 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); }); - return await Promise.race([ + 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,