Skip to content

Commit 211e78c

Browse files
feat: Respect abort signals during serverside fetch optimization (#13877)
* feat: Respect abort signals during serverside fetch optimization * move * lint * snake_case * chore: Remove abort listener
1 parent 5594597 commit 211e78c

File tree

6 files changed

+98
-4
lines changed

6 files changed

+98
-4
lines changed

packages/kit/src/runtime/server/fetch.js

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -145,10 +145,7 @@ export function create_fetch({ event, options, manifest, state, get_cookie_heade
145145
);
146146
}
147147

148-
const response = await respond(request, options, manifest, {
149-
...state,
150-
depth: state.depth + 1
151-
});
148+
const response = await internal_fetch(request, options, manifest, state);
152149

153150
const set_cookie = response.headers.get('set-cookie');
154151
if (set_cookie) {
@@ -195,3 +192,43 @@ function normalize_fetch_input(info, init, url) {
195192

196193
return new Request(typeof info === 'string' ? new URL(info, url) : info, init);
197194
}
195+
196+
/**
197+
* @param {Request} request
198+
* @param {import('types').SSROptions} options
199+
* @param {import('@sveltejs/kit').SSRManifest} manifest
200+
* @param {import('types').SSRState} state
201+
* @returns {Promise<Response>}
202+
*/
203+
async function internal_fetch(request, options, manifest, state) {
204+
if (request.signal) {
205+
if (request.signal.aborted) {
206+
throw new DOMException('The operation was aborted.', 'AbortError');
207+
}
208+
209+
let remove_abort_listener = () => {};
210+
/** @type {Promise<never>} */
211+
const abort_promise = new Promise((_, reject) => {
212+
const on_abort = () => {
213+
reject(new DOMException('The operation was aborted.', 'AbortError'));
214+
};
215+
request.signal.addEventListener('abort', on_abort, { once: true });
216+
remove_abort_listener = () => request.signal.removeEventListener('abort', on_abort);
217+
});
218+
219+
const result = await Promise.race([
220+
respond(request, options, manifest, {
221+
...state,
222+
depth: state.depth + 1
223+
}),
224+
abort_promise
225+
]);
226+
remove_abort_listener();
227+
return result;
228+
} else {
229+
return await respond(request, options, manifest, {
230+
...state,
231+
depth: state.depth + 1
232+
});
233+
}
234+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
export async function load({ fetch }) {
2+
const aborted_controller = new AbortController();
3+
aborted_controller.abort();
4+
5+
let aborted_immediately = false;
6+
try {
7+
await fetch('/load/fetch-abort-signal/data', { signal: aborted_controller.signal });
8+
} catch (error) {
9+
if (error.name === 'AbortError') {
10+
aborted_immediately = true;
11+
}
12+
}
13+
14+
let aborted_during_request = false;
15+
try {
16+
await fetch('/load/fetch-abort-signal/slow', { signal: AbortSignal.timeout(100) });
17+
} catch (error) {
18+
if (error.name === 'AbortError') {
19+
aborted_during_request = true;
20+
}
21+
}
22+
23+
const successful_response = await fetch('/load/fetch-abort-signal/data');
24+
const successful_data = await successful_response.json();
25+
26+
return {
27+
aborted_immediately,
28+
aborted_during_request,
29+
successful_data
30+
};
31+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<script>
2+
export let data;
3+
</script>
4+
5+
<div>
6+
<h1>AbortSignal Test Results</h1>
7+
<p class="aborted-immediately">Aborted immediately: {data.aborted_immediately}</p>
8+
<p class="aborted-during-request">Aborted during request: {data.aborted_during_request}</p>
9+
<p class="successful-data">Successful data: {JSON.stringify(data.successful_data)}</p>
10+
</div>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { json } from '@sveltejs/kit';
2+
3+
export async function GET() {
4+
return json({ message: 'success', timestamp: Date.now() });
5+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function GET() {
2+
return new Promise(() => {});
3+
}

packages/kit/test/apps/basics/test/test.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,14 @@ test.describe('Load', () => {
569569

570570
expect(await page.textContent('h1')).toBe('404');
571571
});
572+
573+
test('AbortSignal works with internal fetch optimization', async ({ page }) => {
574+
await page.goto('/load/fetch-abort-signal');
575+
576+
expect(await page.textContent('.aborted-immediately')).toBe('Aborted immediately: true');
577+
expect(await page.textContent('.aborted-during-request')).toBe('Aborted during request: true');
578+
expect(await page.textContent('.successful-data')).toContain('"message":"success"');
579+
});
572580
});
573581

574582
test.describe('Nested layouts', () => {

0 commit comments

Comments
 (0)