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/tall-boxes-develop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

Use custom x-sveltekit-action header to route enhanced form submissions to +page.server.js over +server.js
12 changes: 12 additions & 0 deletions documentation/docs/06-form-actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,18 @@ We can also implement progressive enhancement ourselves, without `use:enhance`,
</form>
```

If you have a `+server.js` alongside your `+page.server.js`, `fetch` requests will be routed there by default. To `POST` to an action in `+page.server.js` instead, use the custom `x-sveltekit-action` header:

```diff
const response = await fetch(this.action, {
method: 'POST',
body: data,
+ headers: {
+ 'x-sveltekit-action': 'true'
+ }
});
```

### Alternatives

Form actions are the preferred way to send data to the server, since they can be progressively enhanced, but you can also use [`+server.js`](/docs/routing#server) files to expose (for example) a JSON API.
3 changes: 2 additions & 1 deletion packages/kit/src/runtime/app/forms.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ export function enhance(form, submit = () => {}) {
const response = await fetch(action, {
method: 'POST',
headers: {
accept: 'application/json'
accept: 'application/json',
'x-sveltekit-action': 'true'
},
body: data,
signal: controller.signal
Expand Down
5 changes: 4 additions & 1 deletion packages/kit/src/runtime/server/endpoint.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,16 @@ export async function render_endpoint(event, mod, state) {
* @param {import('types').RequestEvent} event
*/
export function is_endpoint_request(event) {
const { method } = event.request;
const { method, headers } = event.request;

if (method === 'PUT' || method === 'PATCH' || method === 'DELETE') {
// These methods exist exclusively for endpoints
return true;
}

// use:enhance uses a custom header to disambiguate
if (method === 'POST' && headers.get('x-sveltekit-action') === 'true') return false;

// GET/POST requests may be for endpoints or pages. We prefer endpoints if this isn't a text/html request
const accept = event.request.headers.get('accept') ?? '*/*';
return negotiate(accept, ['*', 'text/html']) !== 'text/html';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const actions = {
default: () => {
return {
submitted: true
};
}
};
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
<script>
import { enhance } from '$app/forms';

/** @type {import('./$types').ActionData} */
export let form;

let result;

/** @param {string} method */
async function request(method) {
result = 'loading';
const response = await fetch('/routing/endpoint-next-to-page', {method});
const response = await fetch('/routing/content-negotiation', { method });
result = await response.text();
}
</script>
Expand All @@ -16,3 +21,9 @@
<button on:click={() => request('POST')}>POST</button>
<button on:click={() => request('DELETE')}>DELETE</button>
<pre>{result}</pre>

<form method="POST" use:enhance>
<button>Submit</button>
</form>

<p data-testid="form-result">form.submitted: {form?.submitted}</p>
32 changes: 21 additions & 11 deletions packages/kit/test/apps/basics/test/client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -900,15 +900,25 @@ test.describe('data-sveltekit attributes', () => {
});
});

test('+server.js next to +page.svelte works', async ({ page }) => {
await page.goto('/routing/endpoint-next-to-page');
expect(await page.textContent('p')).toBe('Hi');

for (const method of ['GET', 'PUT', 'PATCH', 'POST', 'DELETE']) {
await page.click(`button:has-text("${method}")`);
await page.waitForFunction(
(method) => document.querySelector('pre').textContent === method,
method
);
}
test.describe('Content negotiation', () => {
test('+server.js next to +page.svelte works', async ({ page }) => {
await page.goto('/routing/content-negotiation');
expect(await page.textContent('p')).toBe('Hi');

for (const method of ['GET', 'PUT', 'PATCH', 'POST', 'DELETE']) {
await page.click(`button:has-text("${method}")`);
await page.waitForFunction(
(method) => document.querySelector('pre')?.textContent === method,
method
);
}
});

test('use:enhance uses action, not POST handler', async ({ page }) => {
await page.goto('/routing/content-negotiation');

page.click('button:has-text("Submit")');
await page.waitForResponse('/routing/content-negotiation');
await expect(page.locator('[data-testid="form-result"]')).toHaveText('form.submitted: true');
});
});