Skip to content
Open
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/wild-vans-bake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

fix: allow commands in more places
11 changes: 3 additions & 8 deletions packages/kit/src/runtime/app/server/remote/command.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,10 @@ export function command(validate_or_fn, maybe_fn) {
const wrapper = (arg) => {
const { event, state } = get_request_store();

if (state.is_endpoint_request) {
if (!['POST', 'PUT', 'PATCH', 'DELETE'].includes(event.request.method)) {
throw new Error(
`Cannot call a command (\`${__.name}(${maybe_fn ? '...' : ''})\`) from a ${event.request.method} handler`
);
}
} else if (!event.isRemoteRequest) {
if (!state.allows_commands) {
const disallowed_method = !['POST', 'PUT', 'PATCH', 'DELETE'].includes(event.request.method);
throw new Error(
`Cannot call a command (\`${__.name}(${maybe_fn ? '...' : ''})\`) during server-side rendering`
`Cannot call a command (\`${__.name}(${maybe_fn ? '...' : ''})\`) ${disallowed_method ? `from a ${event.request.method} handler or ` : ''}during server-side rendering`
);
}

Expand Down
3 changes: 1 addition & 2 deletions packages/kit/src/runtime/server/endpoint.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,8 @@ export async function render_endpoint(event, event_state, mod, state) {
}
}

event_state.is_endpoint_request = true;

try {
event_state.allows_commands = true;
const response = await with_request_store({ event, state: event_state }, () =>
handler(/** @type {import('@sveltejs/kit').RequestEvent<Record<string, any>>} */ (event))
);
Expand Down
1 change: 1 addition & 0 deletions packages/kit/src/runtime/server/page/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ async function call_action(event, event_state, actions) {
},
fn: async (current) => {
const traced_event = merge_tracing(event, current);
event_state.allows_commands = true;
const result = await with_request_store({ event: traced_event, state: event_state }, () =>
action(traced_event)
);
Expand Down
1 change: 1 addition & 0 deletions packages/kit/src/runtime/server/page/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ export async function render_response({
};
}

event_state.allows_commands = false;
rendered = await with_request_store({ event, state: event_state }, async () => {
// use relative paths during rendering, so that the resulting HTML is as
// portable as possible, but reset afterwards
Expand Down
1 change: 1 addition & 0 deletions packages/kit/src/runtime/server/respond.js
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,7 @@ export async function internal_respond(request, options, manifest, state) {
current: root_span
}
};
event_state.allows_commands = ['POST', 'PUT', 'PATCH', 'DELETE'].includes(request.method);
return await with_request_store({ event: traced_event, state: event_state }, () =>
options.hooks.handle({
event: traced_event,
Expand Down
2 changes: 1 addition & 1 deletion packages/kit/src/types/internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -625,7 +625,7 @@ export interface RequestState {
form_instances?: Map<any, any>;
remote_data?: Map<RemoteInfo, Record<string, MaybePromise<any>>>;
refreshes?: Record<string, Promise<any>>;
is_endpoint_request?: boolean;
allows_commands?: boolean;
}

export interface RequestStore {
Expand Down
20 changes: 20 additions & 0 deletions packages/kit/test/apps/async/src/hooks.server.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,23 @@
import { do_something } from './routes/remote/server-action/action.remote';

/** @type {import('@sveltejs/kit').Handle} */
export async function handle({ event, resolve }) {
if (event.url.pathname === '/remote/hook-command') {
try {
const result = await do_something('from-hook');
return new Response(JSON.stringify({ result }), {
headers: { 'content-type': 'application/json' }
});
} catch (e) {
return new Response(JSON.stringify({ error: /** @type {Error} */ (e).message }), {
status: 500,
headers: { 'content-type': 'application/json' }
});
}
}
return resolve(event);
}

/** @type {import('@sveltejs/kit').HandleValidationError} */
export const handleValidationError = ({ issues }) => {
return { message: issues[0].message };
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { do_something } from './action.remote';

export const actions = {
default: async ({ request }) => {
const fields = await request.formData();
const result = await do_something(fields.get('input') as string);
return { result };
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<script>
import { enhance } from '$app/forms';
let { form } = $props();
</script>

<p id="result">{form?.result ?? ''}</p>

<form method="POST" use:enhance>
<input type="hidden" name="input" value="hello" />
<button>submit</button>
</form>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import * as v from 'valibot';
import { command } from '$app/server';

export const do_something = command(v.string(), (input) => {
return `action: ${input}`;
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { do_something } from '../server-action/action.remote';

export async function load() {
const result = await do_something('test');
return { result };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script>
let { data } = $props();
</script>

<p>{data.result}</p>
27 changes: 27 additions & 0 deletions packages/kit/test/apps/async/test/client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,33 @@ test.describe('remote function mutations', () => {
await expect(page.locator('p')).toHaveText('post');
});

test('command inside form action works', async ({ page }) => {
await page.goto('/remote/server-action');

await page.getByRole('button', { name: 'submit' }).click();
await expect(page.locator('#result')).toHaveText('action: hello');
});

test('command inside handle hook works with POST', async ({ request }) => {
const response = await request.post('/remote/hook-command');
expect(response.status()).toBe(200);
const data = await response.json();
expect(data.result).toBe('action: from-hook');
});

test('command is blocked inside load functions', async ({ page }) => {
const response = await page.goto('/remote/server-load-command');
expect(response?.status()).toBe(500);
await expect(page.locator('#message')).toContainText('Cannot call a command');
});

test('command is blocked inside handle hook with GET', async ({ request }) => {
const response = await request.get('/remote/hook-command');
expect(response.status()).toBe(500);
const data = await response.json();
expect(data.error).toContain('Cannot call a command');
});

test('prerendered entries not called in prod', async ({ page }) => {
let request_count = 0;
page.on('request', (r) => (request_count += r.url().includes('/_app/remote') ? 1 : 0));
Expand Down
Loading