From 8c2013da0aaa1c57011deb2a2fe77e77782abb0f Mon Sep 17 00:00:00 2001 From: Rishab49 <25582966+Rishab49@users.noreply.github.com> Date: Tue, 30 Dec 2025 23:37:52 +0530 Subject: [PATCH 1/7] fix: use validated args in batch resolver --- .../src/runtime/app/server/remote/query.js | 12 +++- .../src/runtime/app/server/remote/shared.js | 58 +++++++++++++++++++ 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/packages/kit/src/runtime/app/server/remote/query.js b/packages/kit/src/runtime/app/server/remote/query.js index d8bb50ba7392..e8527b2a0c0a 100644 --- a/packages/kit/src/runtime/app/server/remote/query.js +++ b/packages/kit/src/runtime/app/server/remote/query.js @@ -4,7 +4,13 @@ import { get_request_store } from '@sveltejs/kit/internal/server'; import { create_remote_key, stringify_remote_arg } from '../../../shared.js'; import { prerendering } from '__sveltekit/environment'; -import { create_validator, get_cache, get_response, run_remote_function } from './shared.js'; +import { + create_validator, + get_cache, + get_response, + run_remote_batch_function, + run_remote_function +} from './shared.js'; /** * Creates a remote query. When called from the browser, the function will be invoked on the server via a `fetch` call. @@ -190,7 +196,7 @@ function batch(validate_or_fn, maybe_fn) { batching = { args: [], resolvers: [] }; try { - const get_result = await run_remote_function( + const result = await run_remote_batch_function( event, state, false, @@ -201,7 +207,7 @@ function batch(validate_or_fn, maybe_fn) { for (let i = 0; i < batched.resolvers.length; i++) { try { - batched.resolvers[i].resolve(get_result(batched.args[i], i)); + batched.resolvers[i].resolve(result.resolver(result.validated_args[i], i)); } catch (error) { batched.resolvers[i].reject(error); } diff --git a/packages/kit/src/runtime/app/server/remote/shared.js b/packages/kit/src/runtime/app/server/remote/shared.js index 43698b76244d..cf6998e18d1b 100644 --- a/packages/kit/src/runtime/app/server/remote/shared.js +++ b/packages/kit/src/runtime/app/server/remote/shared.js @@ -146,6 +146,64 @@ export async function run_remote_function(event, state, allow_cookies, arg, vali return with_request_store(store, () => fn(validated)); } +/** + * Like `run_remote_function` but returns validated args along with resolver function. + * @template T + * @param {RequestEvent} event + * @param {RequestState} state + * @param {boolean} allow_cookies + * @param {any} arg + * @param {(arg: any) => any} validate + * @param {(arg?: any) => T} fn + */ +export async function run_remote_batch_function(event, state, allow_cookies, arg, validate, fn) { + /** @type {RequestStore} */ + const store = { + event: { + ...event, + setHeaders: () => { + throw new Error('setHeaders is not allowed in remote functions'); + }, + cookies: { + ...event.cookies, + set: (name, value, opts) => { + if (!allow_cookies) { + throw new Error('Cannot set cookies in `query` or `prerender` functions'); + } + + if (opts.path && !opts.path.startsWith('/')) { + throw new Error('Cookies set in remote functions must have an absolute path'); + } + + return event.cookies.set(name, value, opts); + }, + delete: (name, opts) => { + if (!allow_cookies) { + throw new Error('Cannot delete cookies in `query` or `prerender` functions'); + } + + if (opts.path && !opts.path.startsWith('/')) { + throw new Error('Cookies deleted in remote functions must have an absolute path'); + } + + return event.cookies.delete(name, opts); + } + } + }, + state: { + ...state, + is_in_remote_function: true + } + }; + + // In two parts, each with_event, so that runtimes without async local storage can still get the event at the start of the function + const validated = await with_request_store(store, () => validate(arg)); + return { + resolver: with_request_store(store, () => fn(validated)), + validated_args: validated + }; +} + /** * @param {RemoteInfo} info * @param {RequestState} state From 53335f6e9621ab31c1ad0db9ceec030b52b9f6fc Mon Sep 17 00:00:00 2001 From: Rishab49 <25582966+Rishab49@users.noreply.github.com> Date: Thu, 8 Jan 2026 18:52:16 +0530 Subject: [PATCH 2/7] fix: use validated args in batch resolver in both csr and ssr --- .changeset/rude-islands-flow.md | 5 ++++ .../src/runtime/app/server/remote/query.js | 3 +- packages/kit/src/runtime/server/remote.js | 4 +-- packages/kit/src/types/internal.d.ts | 4 ++- .../src/routes/remote/batch_csr/+page.js | 1 + .../src/routes/remote/batch_csr/+page.svelte | 11 +++++++ .../routes/remote/batch_csr/batch.remote.js | 6 ++++ .../src/routes/remote/batch_ssr/+page.js | 1 + .../src/routes/remote/batch_ssr/+page.svelte | 13 +++++++++ .../routes/remote/batch_ssr/batch.remote.js | 6 ++++ .../kit/test/apps/async/test/client.test.js | 29 +++++++++++++++++++ 11 files changed, 78 insertions(+), 5 deletions(-) create mode 100644 .changeset/rude-islands-flow.md create mode 100644 packages/kit/test/apps/async/src/routes/remote/batch_csr/+page.js create mode 100644 packages/kit/test/apps/async/src/routes/remote/batch_csr/+page.svelte create mode 100644 packages/kit/test/apps/async/src/routes/remote/batch_csr/batch.remote.js create mode 100644 packages/kit/test/apps/async/src/routes/remote/batch_ssr/+page.js create mode 100644 packages/kit/test/apps/async/src/routes/remote/batch_ssr/+page.svelte create mode 100644 packages/kit/test/apps/async/src/routes/remote/batch_ssr/batch.remote.js diff --git a/.changeset/rude-islands-flow.md b/.changeset/rude-islands-flow.md new file mode 100644 index 000000000000..8ba9a1d6730c --- /dev/null +++ b/.changeset/rude-islands-flow.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +fix: use validated args in batch resolver in both csr and ssr diff --git a/packages/kit/src/runtime/app/server/remote/query.js b/packages/kit/src/runtime/app/server/remote/query.js index e8527b2a0c0a..4efc259d96ca 100644 --- a/packages/kit/src/runtime/app/server/remote/query.js +++ b/packages/kit/src/runtime/app/server/remote/query.js @@ -156,8 +156,7 @@ function batch(validate_or_fn, maybe_fn) { name: '', run: (args) => { const { event, state } = get_request_store(); - - return run_remote_function( + return run_remote_batch_function( event, state, false, diff --git a/packages/kit/src/runtime/server/remote.js b/packages/kit/src/runtime/server/remote.js index d52b4be8d87b..3bf7eb9f7323 100644 --- a/packages/kit/src/runtime/server/remote.js +++ b/packages/kit/src/runtime/server/remote.js @@ -76,9 +76,9 @@ async function handle_remote_call_internal(event, state, options, manifest, id) const args = payloads.map((payload) => parse_remote_arg(payload, transport)); const get_result = await with_request_store({ event, state }, () => info.run(args)); const results = await Promise.all( - args.map(async (arg, i) => { + get_result.validated_args.map(async (arg, i) => { try { - return { type: 'result', data: get_result(arg, i) }; + return { type: 'result', data: get_result.resolver(arg, i) }; } catch (error) { return { type: 'error', diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index 6384201af551..3e3b79527a64 100644 --- a/packages/kit/src/types/internal.d.ts +++ b/packages/kit/src/types/internal.d.ts @@ -571,7 +571,9 @@ export type RemoteInfo = id: string; name: string; /** Direct access to the function without batching etc logic, for remote functions called from the client */ - run: (args: any[]) => Promise<(arg: any, idx: number) => any>; + run: ( + args: any[] + ) => Promise<{ resolver: (arg: any, idx: number) => any; validated_args: any[] }>; } | { type: 'form'; diff --git a/packages/kit/test/apps/async/src/routes/remote/batch_csr/+page.js b/packages/kit/test/apps/async/src/routes/remote/batch_csr/+page.js new file mode 100644 index 000000000000..a3d15781a772 --- /dev/null +++ b/packages/kit/test/apps/async/src/routes/remote/batch_csr/+page.js @@ -0,0 +1 @@ +export const ssr = false; diff --git a/packages/kit/test/apps/async/src/routes/remote/batch_csr/+page.svelte b/packages/kit/test/apps/async/src/routes/remote/batch_csr/+page.svelte new file mode 100644 index 000000000000..40a506854d81 --- /dev/null +++ b/packages/kit/test/apps/async/src/routes/remote/batch_csr/+page.svelte @@ -0,0 +1,11 @@ + + +
+ {#each ['2026-01-01', '2025-12-01', '2025-11-01'] as d} +
+ {await getData(d)} +
+ {/each} +
diff --git a/packages/kit/test/apps/async/src/routes/remote/batch_csr/batch.remote.js b/packages/kit/test/apps/async/src/routes/remote/batch_csr/batch.remote.js new file mode 100644 index 000000000000..96e5ea440530 --- /dev/null +++ b/packages/kit/test/apps/async/src/routes/remote/batch_csr/batch.remote.js @@ -0,0 +1,6 @@ +import { query } from '$app/server'; +import * as v from 'valibot'; + +export const getData = query.batch(v.pipe(v.string(), v.toDate()), (dates) => { + return (x) => typeof x; +}); diff --git a/packages/kit/test/apps/async/src/routes/remote/batch_ssr/+page.js b/packages/kit/test/apps/async/src/routes/remote/batch_ssr/+page.js new file mode 100644 index 000000000000..26bb5688768d --- /dev/null +++ b/packages/kit/test/apps/async/src/routes/remote/batch_ssr/+page.js @@ -0,0 +1 @@ +export const csr = false; diff --git a/packages/kit/test/apps/async/src/routes/remote/batch_ssr/+page.svelte b/packages/kit/test/apps/async/src/routes/remote/batch_ssr/+page.svelte new file mode 100644 index 000000000000..8daff4bff661 --- /dev/null +++ b/packages/kit/test/apps/async/src/routes/remote/batch_ssr/+page.svelte @@ -0,0 +1,13 @@ + + + +
+ {#each ['2026-01-01', '2025-12-01', '2025-11-01'] as d} +
+ {await getData(d)} +
+ {/each} +
+ diff --git a/packages/kit/test/apps/async/src/routes/remote/batch_ssr/batch.remote.js b/packages/kit/test/apps/async/src/routes/remote/batch_ssr/batch.remote.js new file mode 100644 index 000000000000..96e5ea440530 --- /dev/null +++ b/packages/kit/test/apps/async/src/routes/remote/batch_ssr/batch.remote.js @@ -0,0 +1,6 @@ +import { query } from '$app/server'; +import * as v from 'valibot'; + +export const getData = query.batch(v.pipe(v.string(), v.toDate()), (dates) => { + return (x) => typeof x; +}); diff --git a/packages/kit/test/apps/async/test/client.test.js b/packages/kit/test/apps/async/test/client.test.js index 8266b5ac8338..1246d9522f8b 100644 --- a/packages/kit/test/apps/async/test/client.test.js +++ b/packages/kit/test/apps/async/test/client.test.js @@ -301,6 +301,35 @@ test.describe('remote function mutations', () => { expect(request_count).toBe(1); // only the command request }); + test('query.batch ssr resolver function has first argument of type Output', async ({ page }) => { + await page.goto('/remote/batch_ssr', { + waitUntil: 'domcontentloaded' + }); + + const items = page.locator('#ssr_batch div'); + let count = await items.count(); + expect(count).toBe(3); + + let textContent = await items.allTextContents(); + + expect(textContent).toEqual(['object', 'object', 'object']); + }); + + test('query.batch csr resolver function has first argument of type Output', async ({ page }) => { + await page.goto('/remote/batch_csr', { + waitUntil: 'domcontentloaded' + }); + + await page.locator('body.started').waitFor({ state: 'visible' }); + const items = page.locator('#csr_batch div'); + let count = await items.count(); + expect(count).toBe(3); + + let textContent = await items.allTextContents(); + + expect(textContent).toEqual(['object', 'object', 'object']); + }); + // TODO ditto test('query works with transport', async ({ page }) => { await page.goto('/remote/transport'); From c830c38f3a903651f48455587e8c33449026bfaf Mon Sep 17 00:00:00 2001 From: Rishab49 <25582966+Rishab49@users.noreply.github.com> Date: Sat, 24 Jan 2026 10:52:27 +0530 Subject: [PATCH 3/7] fixing linting issues --- .../async/src/routes/remote/batch_csr/batch.remote.js | 2 +- .../async/src/routes/remote/batch_ssr/batch.remote.js | 2 +- packages/kit/test/apps/async/test/client.test.js | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/kit/test/apps/async/src/routes/remote/batch_csr/batch.remote.js b/packages/kit/test/apps/async/src/routes/remote/batch_csr/batch.remote.js index 96e5ea440530..a722fb9d01b8 100644 --- a/packages/kit/test/apps/async/src/routes/remote/batch_csr/batch.remote.js +++ b/packages/kit/test/apps/async/src/routes/remote/batch_csr/batch.remote.js @@ -1,6 +1,6 @@ import { query } from '$app/server'; import * as v from 'valibot'; -export const getData = query.batch(v.pipe(v.string(), v.toDate()), (dates) => { +export const getData = query.batch(v.pipe(v.string(), v.toDate()), () => { return (x) => typeof x; }); diff --git a/packages/kit/test/apps/async/src/routes/remote/batch_ssr/batch.remote.js b/packages/kit/test/apps/async/src/routes/remote/batch_ssr/batch.remote.js index 96e5ea440530..a722fb9d01b8 100644 --- a/packages/kit/test/apps/async/src/routes/remote/batch_ssr/batch.remote.js +++ b/packages/kit/test/apps/async/src/routes/remote/batch_ssr/batch.remote.js @@ -1,6 +1,6 @@ import { query } from '$app/server'; import * as v from 'valibot'; -export const getData = query.batch(v.pipe(v.string(), v.toDate()), (dates) => { +export const getData = query.batch(v.pipe(v.string(), v.toDate()), () => { return (x) => typeof x; }); diff --git a/packages/kit/test/apps/async/test/client.test.js b/packages/kit/test/apps/async/test/client.test.js index 1246d9522f8b..a0889034a9df 100644 --- a/packages/kit/test/apps/async/test/client.test.js +++ b/packages/kit/test/apps/async/test/client.test.js @@ -307,10 +307,10 @@ test.describe('remote function mutations', () => { }); const items = page.locator('#ssr_batch div'); - let count = await items.count(); + const count = await items.count(); expect(count).toBe(3); - let textContent = await items.allTextContents(); + const textContent = await items.allTextContents(); expect(textContent).toEqual(['object', 'object', 'object']); }); @@ -322,10 +322,10 @@ test.describe('remote function mutations', () => { await page.locator('body.started').waitFor({ state: 'visible' }); const items = page.locator('#csr_batch div'); - let count = await items.count(); + const count = await items.count(); expect(count).toBe(3); - let textContent = await items.allTextContents(); + const textContent = await items.allTextContents(); expect(textContent).toEqual(['object', 'object', 'object']); }); From a4673254d1ff86b1467e28e75405833cf583ca39 Mon Sep 17 00:00:00 2001 From: Elliott Johnson Date: Mon, 26 Jan 2026 18:03:32 -0700 Subject: [PATCH 4/7] chore: rework/tidy --- .../src/runtime/app/server/remote/query.js | 20 ++--- .../src/runtime/app/server/remote/shared.js | 80 ++++++------------- packages/kit/src/runtime/server/remote.js | 56 +++++++------ packages/kit/src/types/internal.d.ts | 4 +- 4 files changed, 67 insertions(+), 93 deletions(-) diff --git a/packages/kit/src/runtime/app/server/remote/query.js b/packages/kit/src/runtime/app/server/remote/query.js index 4efc259d96ca..25f36a99207f 100644 --- a/packages/kit/src/runtime/app/server/remote/query.js +++ b/packages/kit/src/runtime/app/server/remote/query.js @@ -178,7 +178,7 @@ function batch(validate_or_fn, maybe_fn) { ); } - const { event, state } = get_request_store(); + const { state } = get_request_store(); const get_remote_function_result = () => { // Collect all the calls to the same query in the same macrotask, @@ -195,20 +195,14 @@ function batch(validate_or_fn, maybe_fn) { batching = { args: [], resolvers: [] }; try { - const result = await run_remote_batch_function( - event, - state, - false, - batched.args, - (array) => Promise.all(array.map(validate)), - fn - ); + const results = await __.run(batched.args); for (let i = 0; i < batched.resolvers.length; i++) { - try { - batched.resolvers[i].resolve(result.resolver(result.validated_args[i], i)); - } catch (error) { - batched.resolvers[i].reject(error); + const result = results[i]; + if (result.status === 'fulfilled') { + batched.resolvers[i].resolve(result.value); + } else { + batched.resolvers[i].reject(result.reason); } } } catch (error) { diff --git a/packages/kit/src/runtime/app/server/remote/shared.js b/packages/kit/src/runtime/app/server/remote/shared.js index cf6998e18d1b..a68a641261e2 100644 --- a/packages/kit/src/runtime/app/server/remote/shared.js +++ b/packages/kit/src/runtime/app/server/remote/shared.js @@ -92,18 +92,13 @@ export function parse_remote_response(data, transport) { } /** - * Like `with_event` but removes things from `event` you cannot see/call in remote functions, such as `setHeaders`. - * @template T * @param {RequestEvent} event * @param {RequestState} state * @param {boolean} allow_cookies - * @param {any} arg - * @param {(arg: any) => any} validate - * @param {(arg?: any) => T} fn + * @returns {RequestStore} */ -export async function run_remote_function(event, state, allow_cookies, arg, validate, fn) { - /** @type {RequestStore} */ - const store = { +function sanitize_event_for_remote_function(event, state, allow_cookies) { + return { event: { ...event, setHeaders: () => { @@ -140,68 +135,45 @@ export async function run_remote_function(event, state, allow_cookies, arg, vali is_in_remote_function: true } }; +} +/** + * Like `with_event` but removes things from `event` you cannot see/call in remote functions, such as `setHeaders`. + * @template T + * @param {RequestEvent} event + * @param {RequestState} state + * @param {boolean} allow_cookies + * @param {any} arg + * @param {(arg: any) => any} validate + * @param {(arg?: any) => T} fn + */ +export async function run_remote_function(event, state, allow_cookies, arg, validate, fn) { + const store = sanitize_event_for_remote_function(event, state, allow_cookies); // In two parts, each with_event, so that runtimes without async local storage can still get the event at the start of the function const validated = await with_request_store(store, () => validate(arg)); return with_request_store(store, () => fn(validated)); } /** - * Like `run_remote_function` but returns validated args along with resolver function. + * Additionally-constrained version of `run_remote_function` that handles the array/validation dance of batching. + * Uses `Promise.allSettled` so that individual item errors can be captured and returned separately. * @template T * @param {RequestEvent} event * @param {RequestState} state * @param {boolean} allow_cookies * @param {any} arg - * @param {(arg: any) => any} validate - * @param {(arg?: any) => T} fn + * @param {(arg: any[]) => MaybePromise} validate + * @param {(arg?: any[]) => MaybePromise<(arg: any, idx: number) => T>} fn + * @returns {Promise[]>} */ export async function run_remote_batch_function(event, state, allow_cookies, arg, validate, fn) { - /** @type {RequestStore} */ - const store = { - event: { - ...event, - setHeaders: () => { - throw new Error('setHeaders is not allowed in remote functions'); - }, - cookies: { - ...event.cookies, - set: (name, value, opts) => { - if (!allow_cookies) { - throw new Error('Cannot set cookies in `query` or `prerender` functions'); - } - - if (opts.path && !opts.path.startsWith('/')) { - throw new Error('Cookies set in remote functions must have an absolute path'); - } - - return event.cookies.set(name, value, opts); - }, - delete: (name, opts) => { - if (!allow_cookies) { - throw new Error('Cannot delete cookies in `query` or `prerender` functions'); - } - - if (opts.path && !opts.path.startsWith('/')) { - throw new Error('Cookies deleted in remote functions must have an absolute path'); - } - - return event.cookies.delete(name, opts); - } - } - }, - state: { - ...state, - is_in_remote_function: true - } - }; - + const store = sanitize_event_for_remote_function(event, state, allow_cookies); // In two parts, each with_event, so that runtimes without async local storage can still get the event at the start of the function const validated = await with_request_store(store, () => validate(arg)); - return { - resolver: with_request_store(store, () => fn(validated)), - validated_args: validated - }; + const resolver = await with_request_store(store, () => fn(validated)); + return Promise.allSettled( + validated.map(async (value, index) => Promise.resolve(resolver(value, index))) + ); } /** diff --git a/packages/kit/src/runtime/server/remote.js b/packages/kit/src/runtime/server/remote.js index 3bf7eb9f7323..523077cd494b 100644 --- a/packages/kit/src/runtime/server/remote.js +++ b/packages/kit/src/runtime/server/remote.js @@ -1,4 +1,4 @@ -/** @import { ActionResult, RemoteForm, RequestEvent, SSRManifest } from '@sveltejs/kit' */ +/** @import { ActionResult, RemoteForm, RequestEvent, SSRManifest, Transport } from '@sveltejs/kit' */ /** @import { RemoteFunctionResponse, RemoteInfo, RequestState, SSROptions } from 'types' */ import { json, error } from '@sveltejs/kit'; @@ -74,28 +74,8 @@ async function handle_remote_call_internal(event, state, options, manifest, id) const { payloads } = await event.request.json(); const args = payloads.map((payload) => parse_remote_arg(payload, transport)); - const get_result = await with_request_store({ event, state }, () => info.run(args)); - const results = await Promise.all( - get_result.validated_args.map(async (arg, i) => { - try { - return { type: 'result', data: get_result.resolver(arg, i) }; - } catch (error) { - return { - type: 'error', - error: await handle_error_and_jsonify(event, state, options, error), - status: - error instanceof HttpError || error instanceof SvelteKitError ? error.status : 500 - }; - } - }) - ); - - return json( - /** @type {RemoteFunctionResponse} */ ({ - type: 'result', - result: stringify(results, transport) - }) - ); + const results = await info.run(args); + return batch_to_response(results, transport, event, state, options); } if (info.type === 'form') { @@ -340,3 +320,33 @@ export function get_remote_id(url) { export function get_remote_action(url) { return url.searchParams.get('/remote'); } + +/** + * @param {PromiseSettledResult[]} results + * @param {Transport} transport + * @param {RequestEvent} event + * @param {RequestState} state + * @param {SSROptions} options + * @returns {Promise} + */ +async function batch_to_response(results, transport, event, state, options) { + const data = await Promise.all( + results.map(async (result) => { + if (result.status === 'fulfilled') { + return { type: 'result', data: result.value }; + } else { + const err = result.reason; + return { + type: 'error', + error: await handle_error_and_jsonify(event, state, options, err), + status: err instanceof HttpError || err instanceof SvelteKitError ? err.status : 500 + }; + } + }) + ); + + return json({ + type: 'result', + result: stringify(data, transport) + }); +} diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index 3e3b79527a64..1d305b044c4d 100644 --- a/packages/kit/src/types/internal.d.ts +++ b/packages/kit/src/types/internal.d.ts @@ -571,9 +571,7 @@ export type RemoteInfo = id: string; name: string; /** Direct access to the function without batching etc logic, for remote functions called from the client */ - run: ( - args: any[] - ) => Promise<{ resolver: (arg: any, idx: number) => any; validated_args: any[] }>; + run: (args: any[]) => Promise>>; } | { type: 'form'; From 43c0f8bdf7517a126869c72ea19a008a618c31e8 Mon Sep 17 00:00:00 2001 From: Elliott Johnson Date: Tue, 27 Jan 2026 10:34:08 -0700 Subject: [PATCH 5/7] fix: error handling --- packages/kit/src/runtime/app/server/remote/shared.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/kit/src/runtime/app/server/remote/shared.js b/packages/kit/src/runtime/app/server/remote/shared.js index a68a641261e2..9f506183879f 100644 --- a/packages/kit/src/runtime/app/server/remote/shared.js +++ b/packages/kit/src/runtime/app/server/remote/shared.js @@ -156,7 +156,6 @@ export async function run_remote_function(event, state, allow_cookies, arg, vali /** * Additionally-constrained version of `run_remote_function` that handles the array/validation dance of batching. - * Uses `Promise.allSettled` so that individual item errors can be captured and returned separately. * @template T * @param {RequestEvent} event * @param {RequestState} state @@ -171,9 +170,13 @@ export async function run_remote_batch_function(event, state, allow_cookies, arg // In two parts, each with_event, so that runtimes without async local storage can still get the event at the start of the function const validated = await with_request_store(store, () => validate(arg)); const resolver = await with_request_store(store, () => fn(validated)); - return Promise.allSettled( - validated.map(async (value, index) => Promise.resolve(resolver(value, index))) - ); + return validated.map((value, index) => { + try { + return { status: 'fulfilled', value: resolver(value, index) }; + } catch (e) { + return { status: 'rejected', reason: e }; + } + }); } /** From 1177e0d67bf2e7fdda5186a888875adebc2bdc2b Mon Sep 17 00:00:00 2001 From: Elliott Johnson Date: Tue, 27 Jan 2026 10:35:34 -0700 Subject: [PATCH 6/7] changeset --- .changeset/rude-islands-flow.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/rude-islands-flow.md b/.changeset/rude-islands-flow.md index 8ba9a1d6730c..3e7813c69467 100644 --- a/.changeset/rude-islands-flow.md +++ b/.changeset/rude-islands-flow.md @@ -1,5 +1,5 @@ --- -'@sveltejs/kit': major +'@sveltejs/kit': patch --- fix: use validated args in batch resolver in both csr and ssr From 2b5a39896f949f01fe0092e07b836973e57d5b21 Mon Sep 17 00:00:00 2001 From: Elliott Johnson Date: Tue, 27 Jan 2026 11:05:32 -0700 Subject: [PATCH 7/7] overhaul tests --- .../remote/batch-validation/+page.svelte | 13 ++++++++ .../remote/batch-validation/batch.remote.js | 12 +++++++ .../src/routes/remote/batch_csr/+page.js | 1 - .../src/routes/remote/batch_csr/+page.svelte | 11 ------- .../routes/remote/batch_csr/batch.remote.js | 6 ---- .../src/routes/remote/batch_ssr/+page.js | 1 - .../src/routes/remote/batch_ssr/+page.svelte | 13 -------- .../routes/remote/batch_ssr/batch.remote.js | 6 ---- .../kit/test/apps/async/test/client.test.js | 31 +++---------------- 9 files changed, 30 insertions(+), 64 deletions(-) create mode 100644 packages/kit/test/apps/async/src/routes/remote/batch-validation/+page.svelte create mode 100644 packages/kit/test/apps/async/src/routes/remote/batch-validation/batch.remote.js delete mode 100644 packages/kit/test/apps/async/src/routes/remote/batch_csr/+page.js delete mode 100644 packages/kit/test/apps/async/src/routes/remote/batch_csr/+page.svelte delete mode 100644 packages/kit/test/apps/async/src/routes/remote/batch_csr/batch.remote.js delete mode 100644 packages/kit/test/apps/async/src/routes/remote/batch_ssr/+page.js delete mode 100644 packages/kit/test/apps/async/src/routes/remote/batch_ssr/+page.svelte delete mode 100644 packages/kit/test/apps/async/src/routes/remote/batch_ssr/batch.remote.js diff --git a/packages/kit/test/apps/async/src/routes/remote/batch-validation/+page.svelte b/packages/kit/test/apps/async/src/routes/remote/batch-validation/+page.svelte new file mode 100644 index 000000000000..710c4f806c6a --- /dev/null +++ b/packages/kit/test/apps/async/src/routes/remote/batch-validation/+page.svelte @@ -0,0 +1,13 @@ + + +
+ {#each words as word, i} + {await reverse(word)}{i === words.length - 1 ? '' : ' '} + {/each} +
+ diff --git a/packages/kit/test/apps/async/src/routes/remote/batch-validation/batch.remote.js b/packages/kit/test/apps/async/src/routes/remote/batch-validation/batch.remote.js new file mode 100644 index 000000000000..95ff1e550aa8 --- /dev/null +++ b/packages/kit/test/apps/async/src/routes/remote/batch-validation/batch.remote.js @@ -0,0 +1,12 @@ +import { query } from '$app/server'; +import * as v from 'valibot'; + +export const reverse = query.batch( + v.pipe( + v.string(), + v.transform((val) => val.split('').reverse().join('')) + ), + () => { + return (x) => x; + } +); diff --git a/packages/kit/test/apps/async/src/routes/remote/batch_csr/+page.js b/packages/kit/test/apps/async/src/routes/remote/batch_csr/+page.js deleted file mode 100644 index a3d15781a772..000000000000 --- a/packages/kit/test/apps/async/src/routes/remote/batch_csr/+page.js +++ /dev/null @@ -1 +0,0 @@ -export const ssr = false; diff --git a/packages/kit/test/apps/async/src/routes/remote/batch_csr/+page.svelte b/packages/kit/test/apps/async/src/routes/remote/batch_csr/+page.svelte deleted file mode 100644 index 40a506854d81..000000000000 --- a/packages/kit/test/apps/async/src/routes/remote/batch_csr/+page.svelte +++ /dev/null @@ -1,11 +0,0 @@ - - -
- {#each ['2026-01-01', '2025-12-01', '2025-11-01'] as d} -
- {await getData(d)} -
- {/each} -
diff --git a/packages/kit/test/apps/async/src/routes/remote/batch_csr/batch.remote.js b/packages/kit/test/apps/async/src/routes/remote/batch_csr/batch.remote.js deleted file mode 100644 index a722fb9d01b8..000000000000 --- a/packages/kit/test/apps/async/src/routes/remote/batch_csr/batch.remote.js +++ /dev/null @@ -1,6 +0,0 @@ -import { query } from '$app/server'; -import * as v from 'valibot'; - -export const getData = query.batch(v.pipe(v.string(), v.toDate()), () => { - return (x) => typeof x; -}); diff --git a/packages/kit/test/apps/async/src/routes/remote/batch_ssr/+page.js b/packages/kit/test/apps/async/src/routes/remote/batch_ssr/+page.js deleted file mode 100644 index 26bb5688768d..000000000000 --- a/packages/kit/test/apps/async/src/routes/remote/batch_ssr/+page.js +++ /dev/null @@ -1 +0,0 @@ -export const csr = false; diff --git a/packages/kit/test/apps/async/src/routes/remote/batch_ssr/+page.svelte b/packages/kit/test/apps/async/src/routes/remote/batch_ssr/+page.svelte deleted file mode 100644 index 8daff4bff661..000000000000 --- a/packages/kit/test/apps/async/src/routes/remote/batch_ssr/+page.svelte +++ /dev/null @@ -1,13 +0,0 @@ - - - -
- {#each ['2026-01-01', '2025-12-01', '2025-11-01'] as d} -
- {await getData(d)} -
- {/each} -
- diff --git a/packages/kit/test/apps/async/src/routes/remote/batch_ssr/batch.remote.js b/packages/kit/test/apps/async/src/routes/remote/batch_ssr/batch.remote.js deleted file mode 100644 index a722fb9d01b8..000000000000 --- a/packages/kit/test/apps/async/src/routes/remote/batch_ssr/batch.remote.js +++ /dev/null @@ -1,6 +0,0 @@ -import { query } from '$app/server'; -import * as v from 'valibot'; - -export const getData = query.batch(v.pipe(v.string(), v.toDate()), () => { - return (x) => typeof x; -}); diff --git a/packages/kit/test/apps/async/test/client.test.js b/packages/kit/test/apps/async/test/client.test.js index a0889034a9df..d6d4a4da9717 100644 --- a/packages/kit/test/apps/async/test/client.test.js +++ b/packages/kit/test/apps/async/test/client.test.js @@ -301,33 +301,12 @@ test.describe('remote function mutations', () => { expect(request_count).toBe(1); // only the command request }); - test('query.batch ssr resolver function has first argument of type Output', async ({ page }) => { - await page.goto('/remote/batch_ssr', { - waitUntil: 'domcontentloaded' - }); + test('query.batch resolver function always receives validated arguments', async ({ page }) => { + await page.goto('/remote/batch-validation'); - const items = page.locator('#ssr_batch div'); - const count = await items.count(); - expect(count).toBe(3); - - const textContent = await items.allTextContents(); - - expect(textContent).toEqual(['object', 'object', 'object']); - }); - - test('query.batch csr resolver function has first argument of type Output', async ({ page }) => { - await page.goto('/remote/batch_csr', { - waitUntil: 'domcontentloaded' - }); - - await page.locator('body.started').waitFor({ state: 'visible' }); - const items = page.locator('#csr_batch div'); - const count = await items.count(); - expect(count).toBe(3); - - const textContent = await items.allTextContents(); - - expect(textContent).toEqual(['object', 'object', 'object']); + await expect(page.locator('#phrase')).toHaveText('use the force'); + await page.locator('button').click(); + await expect(page.locator('#phrase')).toHaveText('i am your father'); }); // TODO ditto