Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/remote-form-diagnostics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

chore: standardize remote-function and shared form diagnostics
47 changes: 47 additions & 0 deletions packages/kit/messages/client-errors/remote.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
## remote_command_redirect

> Redirects are not allowed in commands. Return a result instead and use `goto` on the client

Remote commands are imperative mutations and cannot redirect. Return the destination or another result from the command, then call [`goto`](https://svelte.dev/docs/kit/$app-navigation#goto) in the browser after awaiting it. Remote forms support redirects when you need a progressively enhanced submission.

## remote_form_multiple_elements

> A form object can only be attached to a single `<form>` element

Each remote form instance tracks one element's input, validation issues and submission state. To render several forms from the same remote function, use `myForm.for(key)` with a different stable key for each element. An instance returned by `.for(key)` must also be attached to only one element; reusing its key does not create a second independent instance. This includes keys such as `0`, `false` and `''`.

## remote_form_mixed_inputs

> Cannot mix and match file and non-file inputs under the same name (`%name%`)

Inputs that share an array field must all contain the same kind of data. Use separate fields for file inputs and non-file inputs, and spread the corresponding `form.fields.field.as(...)` props onto each control.

## remote_form_multiple_files

> Can only use the `multiple` attribute when `name` includes a `[]` suffix — consider changing `%name%` to `%name%[]`

Use `form.fields.files.as('file multiple')` rather than adding `multiple` to the props for a single file input. The array suffix tells SvelteKit to collect all selected files instead of a single file.

## remote_form_not_attached

> Cannot call `submit()` before the form is attached

Spread the remote form object onto a `<form>` element before calling its `submit()` method. Call it from a browser event handler after the element has mounted, and do not submit an instance whose element has been removed.

## remote_form_reserved_field

> `$` is used to collect all FormData validation issues and cannot be used as the `name` of a form control

`form.fields.allIssues()` uses the reserved `$` path for the form's complete issue list. Choose a different field name, including for nested paths and arrays that start with `$`.

## remote_updates_duplicate_override

> Multiple overrides for the same query are not allowed in a single `updates()` invocation

Pass at most one `withOverride(...)` result for each query instance to `.updates(...)`. Combine changes to the same instance into a single override callback. Different arguments identify different query instances and may each have an override.

## remote_updates_invalid_argument

> `updates()` expects a query or live query function, query resource, or query override

Pass a query function to update all its active instances, a query resource to update one instance, or a `withOverride(...)` result to apply an optimistic update. A release callback from an integration is also accepted. Do not pass a command, form, prerender function or an arbitrary value.
15 changes: 15 additions & 0 deletions packages/kit/messages/client-warnings/remote.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
## remote_form_issues_ignored

> Form submission had invalid data, but the validation issues were ignored:
>
> %issues%
>
> Make sure you provide actionable feedback to users, using e.g. `myForm.fields.myField.issues()` or `myForm.fields.allIssues()`

A remote form submission or preflight validation failed, but your UI did not read all its issues. Display field-specific issues with `form.fields.field.issues()`, or display all issues with `form.fields.allIssues()`, so the user knows what to fix. The list contains the validator's own issue messages and paths; it does not change those issues. This warning is only emitted in development, after giving the UI time to read them.

## remote_updates_repeated

> Updates can only be sent once per %invocation%. Ignoring additional updates.

Call `.updates(...)` once on a command invocation or form submission, passing every query or override in that call. Subsequent calls are ignored and return the original promise; they do not send more updates. Calling `.updates()` without arguments is still a call and opts out of automatic invalidation.
113 changes: 113 additions & 0 deletions packages/kit/messages/server-errors/remote.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
## remote_invalid_validator

> Invalid validator passed to remote function. Expected `'unchecked'` or a Standard Schema (https://standardschema.dev)

Pass a [Standard Schema](https://standardschema.dev) validator as the first argument to a [remote function](https://svelte.dev/docs/kit/remote-functions), followed by its callback. Libraries such as Valibot, Zod and ArkType implement this interface. Use `'unchecked'` only when you intentionally handle validation yourself. A function without an input argument can be declared with just its callback.

## remote_headers_forbidden

> `setHeaders` is not allowed in remote functions

Remote functions can run inside another request or share a response with other queries, so they cannot set response headers. Set headers in a server `load` function, endpoint or `handle` hook instead.

## remote_cookie_forbidden

> Cannot %operation% cookies in `query` or `prerender` functions

Queries and prerender functions read data rather than mutate it. Set or delete cookies inside a remote `command` or `form` handler instead, or in an endpoint or form action. Reading cookies is allowed.

## remote_cookie_path_relative

> Cookies %operation% in remote functions must have an absolute path

Remote functions can be invoked from different pages, so a relative cookie path would depend on which page invoked them. Pass an absolute `path`, such as `'/'`, to `cookies.set` or `cookies.delete`. Use the same path when deleting a cookie as when setting it.

## remote_request_property

> Cannot access `event.%property%` in a query. Pass the value as an argument to the query instead

A query's cache key is determined by its argument, not by the page that calls it. Read `url`, `params` or `route` in the caller and pass the required value as an argument so that different values produce different cache entries. This also applies to queries nested inside live queries.

## remote_query_live_not_iterable

> `query.live` `%name%` must return an `Iterator`, `Iterable`, `AsyncIterator` or `AsyncIterable`

A [`query.live`](https://svelte.dev/docs/kit/remote-functions#query.live) callback must provide a sequence of values. Use an async generator (`async function*`) and `yield` values, or return an iterator or iterable. Returning a single object or a promise of a single value is not sufficient; use a regular `query` for that.

## remote_query_live_no_value

> `query.live` `%name%` did not yield a value

When a live query is awaited on the server, it must yield at least one value before completing. Check for branches that return without yielding. Yield an initial value (including `null` if appropriate) before waiting for updates.

## remote_query_prerender

> Cannot call `%type%` `%name%` while prerendering, as prerendered pages need static data. Use `prerender` from `$app/server` instead

Regular, batched and live queries fetch runtime data, while a prerendered page must be generated at build time. Use [`prerender`](https://svelte.dev/docs/kit/remote-functions#prerender) for static data, or disable prerendering for pages that need these queries.

## remote_command_readonly

> Cannot call a command (`%name%`) inside a query or prerender function

Commands mutate data, so they cannot run inside a query or prerender function. Invoke the command from a browser event handler, a remote form handler, a form action or an endpoint that handles a mutative HTTP method.

## remote_command_method

> Cannot call a command (`%name%`) from a `%method%` handler

Commands mutate data and cannot be invoked by read-only HTTP requests. Use a handler for `POST`, `PUT`, `PATCH` or `DELETE`, or invoke the command from a browser event handler. Do not change state in `load` functions.

## remote_command_render

> Cannot call a command (`%name%`) during server-side rendering

Rendering must not mutate application state. Call the command in response to a user interaction, such as a button click, or use a remote form for progressively enhanced submissions.

## remote_form_fail

> `fail(...)` is for form actions. A remote `form` handler should call `invalid(...)` instead. See https://svelte.dev/docs/kit/remote-functions#form-Programmatic-validation

[`fail`](https://svelte.dev/docs/kit/@sveltejs-kit#fail) returns an action failure for a page's form actions. Remote forms use [`invalid`](https://svelte.dev/docs/kit/@sveltejs-kit#invalid) to report validation issues. Call `invalid(issue.field('Message'))` with the issue builder supplied to the form callback, or pass issue strings for errors that apply to the whole form. Do not return or throw the result of `fail` from a remote form handler.

## remote_module_default_export

> Cannot export `default` from a remote module (`%file%`) — please use named exports instead

Each remote function needs a named export so SvelteKit can identify it in requests and generate its client implementation. Replace the default export with a named export created by `query`, `query.batch`, `query.live`, `command`, `form` or `prerender` from `$app/server`.

## remote_module_invalid_export

> `%name%` exported from `%file%` is invalid — all exports from this file must be remote functions

A `.remote.js` or `.remote.ts` module can only export remote functions created with `$app/server`. Keep helper values private, or move values that other modules need into a separate module. Type-only exports are allowed in TypeScript because they are erased at runtime.

## remote_requested_invalid_query

> `requested(...)` expects a query function created with `query(...)`, `query.batch(...)`, or `query.live(...)`

Pass the query function itself to [`requested`](https://svelte.dev/docs/kit/remote-functions#Single-flight-mutations-Client-requested-refreshes), not a query resource returned by calling it. Commands, forms and prerender functions are not queries.

## remote_requested_context

> `requested(...)` can only be called in the context of a command/form remote function

`requested` reads the queries that a client asked a mutation to refresh or reconnect. Use it inside a remote `command` or `form` callback, where that request information exists, rather than in a `load` function, endpoint or query.

## remote_requested_async_validator

> `requested(%name%, %limit%)` cannot be used with synchronous iteration because the query validator is async. Use `for await ... of` instead

Standard Schema validation can be asynchronous. Iterate the result of `requested` with `for await (const { arg, query } of requested(...))` so validation completes before you use each entry. `refreshAll`, `reconnectAll` and `ignoreAll` already use asynchronous iteration.

## remote_requested_wrong_method

> `%method%()` is invalid for %type% queries. Use `%replacement%()` instead.

Use `refreshAll()` for regular and batched queries, and `reconnectAll()` for live queries. A live query needs to restart its stream rather than perform a one-off fetch. Call the appropriate method on the result of `requested`.

## remote_requested_invalid_limit

> Limit must be a non-negative integer or `Infinity`

The second argument to `requested` limits how many client-requested query instances the mutation will handle. Pass a non-negative integer, or `Infinity` to handle all instances. Negative numbers, fractions and `NaN` are not valid limits.
29 changes: 29 additions & 0 deletions packages/kit/messages/shared-errors/forms.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
## form_field_unbound

> Form contained a field that wasn't created with `form.fields.as(...)`: `%name%`

Spread the props from `myForm.fields.field.as(...)` onto every named control in a remote form. These props encode the field's type and the form it belongs to. Don't replace the generated `name` with a handwritten name or reuse props from a different remote form.

## form_field_duplicate

> Form cannot contain duplicated keys — `%name%` has %count% values

A single-value field cannot receive several submitted values. Give independent controls different field names. For an array of choices or multiple files, use the corresponding array input props, such as `form.fields.choices.as('checkbox', 'option')` or `form.fields.files.as('file multiple')`, so the generated name has an array suffix.

## form_field_invalid_name

> Invalid field name `%name%`: field names are written in JS object notation, so keys that would need quoting are not supported. See https://svelte.dev/docs/kit/remote-functions#form-Fields

Remote form field paths use identifiers separated by dots and numeric array indexes in brackets, such as `user.name` or `items[0].title`. Use a schema whose field names can be expressed with this notation. Keys containing spaces, hyphens or other characters requiring quotes are not supported.

## form_field_forbidden_key

> Invalid key `%key%`: This key is not allowed to prevent prototype pollution.

Remote form field paths cannot contain `__proto__`, `constructor` or `prototype`. These names could modify an object's prototype rather than its data. Rename the field in both the form and its schema. Never use untrusted input to construct field paths without validating it.

## form_field_array_conflict

> Invalid array key `%key%`

The same form field path is being used as both an array and an object. Check the names of nested controls and the values passed to `form.fields.field.set(...)`. Numeric indexes must address arrays, while named properties must address objects. Don't mix paths such as `items[0]` and `items.title`.
17 changes: 17 additions & 0 deletions packages/kit/messages/shared-errors/remote.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
## remote_argument_unsupported

> %type% are not valid remote function arguments

Remote function arguments must be serializable. Pass a regular expression's source and flags separately rather than a `RegExp`, and await promises before passing their resolved values. For custom types, use a [transport hook](https://svelte.dev/docs/kit/hooks#Universal-hooks-transport) to encode and decode them. Commands support `File` arguments; the promises SvelteKit uses internally to read those files are allowed.

## form_input_missing_value

> %type% inputs must have a value

Pass a value as the second argument to `form.fields.field.as(...)` for hidden and submit inputs, radio buttons and checkbox arrays. Radio buttons and checkbox arrays need an option value to distinguish their choices. A single boolean checkbox instead takes its checked state. This check only runs in development.

## load_invalid_response

> a `load` function %location% returned %type%, but must return a plain object at the top level (i.e. `return {...}`)

A [`load`](https://svelte.dev/docs/kit/load) function provides the properties of the page's `data` object. Return a plain object such as `{ posts }`, rather than an array, a `Response`, a class instance or a primitive at the top level. `null` and `undefined` are allowed when there is no data to return. To return a `Response`, use an endpoint instead.
11 changes: 11 additions & 0 deletions packages/kit/messages/shared-warnings/remote.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
## form_fields_enumerated

> The properties of `form.fields` are virtual, so operators like `in` and `Object.keys` are meaningless. If you need the current value of a form field, use `form.fields.x.value()`

Remote form fields are proxies created as you access them, not enumerable properties. Read `form.fields.value()` to obtain the current form data, or `form.fields.field.value()` for a specific field. Don't spread `form.fields`, enumerate its keys or use `in` to test whether a field has a value. This development-only warning is emitted once per call site.

## depends_special_scheme

> `%route%`: Calling `depends('%dependency%')` will throw an error in Firefox because `%scheme%` is a special URI scheme

Firefox treats `moz-icon:`, `view-source:` and `jar:` as special URI schemes. For a custom dependency passed to [`depends`](https://svelte.dev/docs/kit/load#Rerunning-load-functions-Manual-invalidation), choose a different application-specific scheme, such as `app:posts`, and use the same identifier when invalidating it.
9 changes: 3 additions & 6 deletions packages/kit/src/exports/internal/server/remote-functions.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/** @import { RemoteInternals } from 'types' */
import * as e from '../../../messages/server-errors.js';

/** @type {RemoteInternals['type'][]} */
const types = ['command', 'form', 'prerender', 'query', 'query_batch', 'query_live'];
Expand All @@ -10,16 +11,12 @@ const types = ['command', 'form', 'prerender', 'query', 'query_batch', 'query_li
*/
export function init_remote_functions(module, file, hash) {
if (module.default) {
throw new Error(
`Cannot export \`default\` from a remote module (${file}) — please use named exports instead`
);
e.remote_module_default_export({ file });
}

for (const [name, fn] of Object.entries(module)) {
if (!types.includes(fn?.__?.type)) {
throw new Error(
`\`${name}\` exported from ${file} is invalid — all exports from this file must be remote functions`
);
e.remote_module_invalid_export({ name, file });
}

fn.__.id = `${hash}/${name}`;
Expand Down
25 changes: 25 additions & 0 deletions packages/kit/src/exports/internal/server/remote-functions.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { expect, test } from 'vitest';
import { init_remote_functions } from './remote-functions.js';

test('remote modules reject default exports', () => {
expect(() =>
init_remote_functions({ default: () => {} }, 'src/lib/a.remote.js', 'hash')
).toThrowKitError('remote_module_default_export', { contains: ['src/lib/a.remote.js'] });
});

test('remote modules reject exports that are not remote functions', () => {
expect(() =>
init_remote_functions({ helper: () => {} }, 'src/lib/a.remote.js', 'hash')
).toThrowKitError('remote_module_invalid_export', {
contains: ['helper', 'src/lib/a.remote.js']
});
});

test.each(['command', 'form', 'prerender', 'query', 'query_batch', 'query_live'])(
'remote modules retain %s metadata',
(type) => {
const fn = Object.assign(() => {}, { __: { type, id: '', name: '' } });
init_remote_functions({ fn }, 'src/lib/a.remote.js', 'hash');
expect(fn.__).toEqual({ type, id: 'hash/fn', name: 'fn' });
}
);
Loading
Loading