Skip to content

Commit 8429847

Browse files
fix: add Content-Type header for POST requests made by enhance (#12198)
* test: Add test cases for `enctype` / `formenctype` attributes for `/actions/enhance/` test page * fix: respect `enctype`/`formenctype` HTML form attributes from form + set `application/x-www-form-urlencoded` as a default `Content-Type` for `POST` requests made by `enhance` * chore: Add a new changelog entry * fix: Improve RegEx Co-authored-by: Tee Ming <[email protected]> * refactor: Use `Headers` class instead of an regular object * Update packages/kit/src/runtime/app/forms.js * fix: Remove unneeded console log from test code Co-authored-by: Tee Ming <[email protected]> * fix: Apply suggestions from code review for test files Co-authored-by: Tee Ming <[email protected]> * chore: Linting * test: Check default `ContentType` value for request made by `use:enhance` * test: Remove unneeded test * refactor: Better logic for handling manual `Content-Type` set by `enctype` / `formenctype` * chore: Formatting * chore: Update changeset * Delete .changeset/moody-apricots-kiss.md * separate changesets * cleanup * move closer to other button tests --------- Co-authored-by: Aleksander Grygier <[email protected]> Co-authored-by: Tee Ming <[email protected]>
1 parent 338ee63 commit 8429847

File tree

6 files changed

+105
-6
lines changed

6 files changed

+105
-6
lines changed

.changeset/hot-papayas-jam.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': patch
3+
---
4+
5+
fix: respect HTML attributes `enctype` and `formenctype` for forms with `use:enhance`

.changeset/strange-planets-fly.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@sveltejs/kit": patch
3+
---
4+
5+
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

packages/kit/src/runtime/app/forms.js

+27-6
Original file line numberDiff line numberDiff line change
@@ -124,9 +124,13 @@ export function enhance(form_element, submit = () => {}) {
124124
: clone(form_element).action
125125
);
126126

127+
const enctype = event.submitter?.hasAttribute('formenctype')
128+
? /** @type {HTMLButtonElement | HTMLInputElement} */ (event.submitter).formEnctype
129+
: clone(form_element).enctype;
130+
127131
const form_data = new FormData(form_element);
128132

129-
if (DEV && clone(form_element).enctype !== 'multipart/form-data') {
133+
if (DEV && enctype !== 'multipart/form-data') {
130134
for (const value of form_data.values()) {
131135
if (value instanceof File) {
132136
throw new Error(
@@ -161,14 +165,31 @@ export function enhance(form_element, submit = () => {}) {
161165
let result;
162166

163167
try {
168+
const headers = new Headers({
169+
accept: 'application/json',
170+
'x-sveltekit-action': 'true'
171+
});
172+
173+
// do not explicitly set the `Content-Type` header when sending `FormData`
174+
// or else it will interfere with the browser's header setting
175+
// see https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest_API/Using_FormData_Objects#sect4
176+
if (enctype !== 'multipart/form-data') {
177+
headers.set(
178+
'Content-Type',
179+
/^(:?application\/x-www-form-urlencoded|text\/plain)$/.test(enctype)
180+
? enctype
181+
: 'application/x-www-form-urlencoded'
182+
);
183+
}
184+
185+
// @ts-expect-error `URLSearchParams(form_data)` is kosher, but typescript doesn't know that
186+
const body = enctype === 'multipart/form-data' ? form_data : new URLSearchParams(form_data);
187+
164188
const response = await fetch(action, {
165189
method: 'POST',
166-
headers: {
167-
accept: 'application/json',
168-
'x-sveltekit-action': 'true'
169-
},
190+
headers,
170191
cache: 'no-store',
171-
body: form_data,
192+
body,
172193
signal: controller.signal
173194
});
174195

packages/kit/test/apps/basics/src/routes/actions/enhance/+page.server.js

+11
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,17 @@ export const actions = {
5252
path: '/actions/enhance'
5353
});
5454

55+
return {};
56+
},
57+
send_file: async ({ request }) => {
58+
const data = await request.formData();
59+
const file = data.get('file');
60+
61+
if (file instanceof File) {
62+
return {
63+
result: 'file name:' + file.name
64+
};
65+
}
5566
return {};
5667
}
5768
};

packages/kit/test/apps/basics/src/routes/actions/enhance/+page.svelte

+6
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,9 @@
5757
<button formmethod="dialog">Cancel</button>
5858
</form>
5959
</dialog>
60+
61+
<form action="?/send_file" method="post" use:enhance>
62+
<input type="file" name="file" class="form-file-input" />
63+
64+
<button class="form-file-submit" formenctype="multipart/form-data" type="submit">Submit</button>
65+
</form>

packages/kit/test/apps/basics/test/test.js

+51
Original file line numberDiff line numberDiff line change
@@ -1138,6 +1138,57 @@ test.describe('Actions', () => {
11381138
);
11391139
});
11401140

1141+
test('use:enhance button with formenctype', async ({ page }) => {
1142+
await page.goto('/actions/enhance');
1143+
1144+
expect(await page.textContent('pre.formdata1')).toBe(JSON.stringify(null));
1145+
expect(await page.textContent('pre.formdata2')).toBe(JSON.stringify(null));
1146+
1147+
const fileInput = page.locator('input[type="file"].form-file-input');
1148+
1149+
await fileInput.setInputFiles({
1150+
name: 'test-file.txt',
1151+
mimeType: 'text/plain',
1152+
buffer: Buffer.from('this is test')
1153+
});
1154+
1155+
await page.locator('button.form-file-submit').click();
1156+
1157+
await expect(page.locator('pre.formdata1')).toHaveText(
1158+
JSON.stringify({ result: 'file name:test-file.txt' })
1159+
);
1160+
await expect(page.locator('pre.formdata2')).toHaveText(
1161+
JSON.stringify({ result: 'file name:test-file.txt' })
1162+
);
1163+
});
1164+
1165+
test('use:enhance has `application/x-www-form-urlencoded` as default value for `ContentType` request header', async ({
1166+
page,
1167+
javaScriptEnabled
1168+
}) => {
1169+
test.skip(!javaScriptEnabled, 'skip when JavaScript is disabled');
1170+
1171+
await page.goto('/actions/enhance');
1172+
1173+
expect(await page.textContent('pre.formdata1')).toBe(JSON.stringify(null));
1174+
expect(await page.textContent('pre.formdata2')).toBe(JSON.stringify(null));
1175+
1176+
await page.locator('input[name="username"]').fill('foo');
1177+
1178+
const [request] = await Promise.all([
1179+
page.waitForRequest('/actions/enhance?/login'),
1180+
page.locator('button.form1').click()
1181+
]);
1182+
1183+
const requestHeaders = await request.allHeaders();
1184+
1185+
expect(requestHeaders['content-type']).toBe('application/x-www-form-urlencoded');
1186+
1187+
await expect(page.locator('pre.formdata1')).toHaveText(JSON.stringify({ result: 'foo' }));
1188+
await expect(page.locator('pre.formdata2')).toHaveText(JSON.stringify({ result: 'foo' }));
1189+
await expect(page.locator('input[name="username"]')).toHaveValue('');
1190+
});
1191+
11411192
test('use:enhance does not clear form on second submit', async ({ page }) => {
11421193
await page.goto('/actions/enhance');
11431194

0 commit comments

Comments
 (0)