diff --git a/.changeset/hot-papayas-jam.md b/.changeset/hot-papayas-jam.md new file mode 100644 index 000000000000..c3c0c0951a74 --- /dev/null +++ b/.changeset/hot-papayas-jam.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: respect HTML attributes `enctype` and `formenctype` for forms with `use:enhance` diff --git a/.changeset/strange-planets-fly.md b/.changeset/strange-planets-fly.md new file mode 100644 index 000000000000..8ba121b0f210 --- /dev/null +++ b/.changeset/strange-planets-fly.md @@ -0,0 +1,5 @@ +--- +"@sveltejs/kit": patch +--- + +fix: set default `Content-Type` header to `application/x-www-form-urlencoded` for `POST` form submissions with `use:enhance` to align with native form behaviour diff --git a/packages/kit/src/runtime/app/forms.js b/packages/kit/src/runtime/app/forms.js index ce7a666fe57c..bdb4f0fa8321 100644 --- a/packages/kit/src/runtime/app/forms.js +++ b/packages/kit/src/runtime/app/forms.js @@ -124,9 +124,13 @@ export function enhance(form_element, submit = () => {}) { : clone(form_element).action ); + const enctype = event.submitter?.hasAttribute('formenctype') + ? /** @type {HTMLButtonElement | HTMLInputElement} */ (event.submitter).formEnctype + : clone(form_element).enctype; + const form_data = new FormData(form_element); - if (DEV && clone(form_element).enctype !== 'multipart/form-data') { + if (DEV && enctype !== 'multipart/form-data') { for (const value of form_data.values()) { if (value instanceof File) { throw new Error( @@ -161,14 +165,31 @@ export function enhance(form_element, submit = () => {}) { let result; try { + const headers = new Headers({ + accept: 'application/json', + 'x-sveltekit-action': 'true' + }); + + // do not explicitly set the `Content-Type` header when sending `FormData` + // or else it will interfere with the browser's header setting + // see https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest_API/Using_FormData_Objects#sect4 + if (enctype !== 'multipart/form-data') { + headers.set( + 'Content-Type', + /^(:?application\/x-www-form-urlencoded|text\/plain)$/.test(enctype) + ? enctype + : 'application/x-www-form-urlencoded' + ); + } + + // @ts-expect-error `URLSearchParams(form_data)` is kosher, but typescript doesn't know that + const body = enctype === 'multipart/form-data' ? form_data : new URLSearchParams(form_data); + const response = await fetch(action, { method: 'POST', - headers: { - accept: 'application/json', - 'x-sveltekit-action': 'true' - }, + headers, cache: 'no-store', - body: form_data, + body, signal: controller.signal }); diff --git a/packages/kit/test/apps/basics/src/routes/actions/enhance/+page.server.js b/packages/kit/test/apps/basics/src/routes/actions/enhance/+page.server.js index a88bd62cccb8..8e0607d73f29 100644 --- a/packages/kit/test/apps/basics/src/routes/actions/enhance/+page.server.js +++ b/packages/kit/test/apps/basics/src/routes/actions/enhance/+page.server.js @@ -52,6 +52,17 @@ export const actions = { path: '/actions/enhance' }); + return {}; + }, + send_file: async ({ request }) => { + const data = await request.formData(); + const file = data.get('file'); + + if (file instanceof File) { + return { + result: 'file name:' + file.name + }; + } return {}; } }; diff --git a/packages/kit/test/apps/basics/src/routes/actions/enhance/+page.svelte b/packages/kit/test/apps/basics/src/routes/actions/enhance/+page.svelte index 671b095ecacd..c53e10ce3e31 100644 --- a/packages/kit/test/apps/basics/src/routes/actions/enhance/+page.svelte +++ b/packages/kit/test/apps/basics/src/routes/actions/enhance/+page.svelte @@ -57,3 +57,9 @@ + +
+ + + +
diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js index 170c0eb16ae2..fde55a5c3eb0 100644 --- a/packages/kit/test/apps/basics/test/test.js +++ b/packages/kit/test/apps/basics/test/test.js @@ -1138,6 +1138,57 @@ test.describe('Actions', () => { ); }); + test('use:enhance button with formenctype', async ({ page }) => { + await page.goto('/actions/enhance'); + + expect(await page.textContent('pre.formdata1')).toBe(JSON.stringify(null)); + expect(await page.textContent('pre.formdata2')).toBe(JSON.stringify(null)); + + const fileInput = page.locator('input[type="file"].form-file-input'); + + await fileInput.setInputFiles({ + name: 'test-file.txt', + mimeType: 'text/plain', + buffer: Buffer.from('this is test') + }); + + await page.locator('button.form-file-submit').click(); + + await expect(page.locator('pre.formdata1')).toHaveText( + JSON.stringify({ result: 'file name:test-file.txt' }) + ); + await expect(page.locator('pre.formdata2')).toHaveText( + JSON.stringify({ result: 'file name:test-file.txt' }) + ); + }); + + test('use:enhance has `application/x-www-form-urlencoded` as default value for `ContentType` request header', async ({ + page, + javaScriptEnabled + }) => { + test.skip(!javaScriptEnabled, 'skip when JavaScript is disabled'); + + await page.goto('/actions/enhance'); + + expect(await page.textContent('pre.formdata1')).toBe(JSON.stringify(null)); + expect(await page.textContent('pre.formdata2')).toBe(JSON.stringify(null)); + + await page.locator('input[name="username"]').fill('foo'); + + const [request] = await Promise.all([ + page.waitForRequest('/actions/enhance?/login'), + page.locator('button.form1').click() + ]); + + const requestHeaders = await request.allHeaders(); + + expect(requestHeaders['content-type']).toBe('application/x-www-form-urlencoded'); + + await expect(page.locator('pre.formdata1')).toHaveText(JSON.stringify({ result: 'foo' })); + await expect(page.locator('pre.formdata2')).toHaveText(JSON.stringify({ result: 'foo' })); + await expect(page.locator('input[name="username"]')).toHaveValue(''); + }); + test('use:enhance does not clear form on second submit', async ({ page }) => { await page.goto('/actions/enhance');