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