Skip to content

Commit

Permalink
[fix] parsing content-type header for actions (#7195)
Browse files Browse the repository at this point in the history
Fixes #7187
  • Loading branch information
repsac-by authored Oct 10, 2022
1 parent 65b3004 commit 3bd5245
Show file tree
Hide file tree
Showing 6 changed files with 51 additions and 12 deletions.
5 changes: 5 additions & 0 deletions .changeset/hot-shoes-bow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

Fix parsing content-type header for actions
5 changes: 2 additions & 3 deletions packages/kit/src/runtime/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { render_page } from './page/index.js';
import { render_response } from './page/render.js';
import { respond_with_error } from './page/respond_with_error.js';
import { coalesce_to_error } from '../../utils/error.js';
import { is_form_content_type } from '../../utils/http.js';
import { GENERIC_ERROR, handle_fatal_error } from './utils.js';
import { decode_params, disable_search, normalize_path } from '../../utils/url.js';
import { exec } from '../../utils/routing.js';
Expand All @@ -24,12 +25,10 @@ export async function respond(request, options, state) {
let url = new URL(request.url);

if (options.csrf.check_origin) {
const type = request.headers.get('content-type')?.split(';')[0];

const forbidden =
request.method === 'POST' &&
request.headers.get('origin') !== url.origin &&
(type === 'application/x-www-form-urlencoded' || type === 'multipart/form-data');
is_form_content_type(request);

if (forbidden) {
return new Response(`Cross-site ${request.method} form submissions are forbidden`, {
Expand Down
9 changes: 5 additions & 4 deletions packages/kit/src/runtime/server/page/actions.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { error, json } from '../../../exports/index.js';
import { normalize_error } from '../../../utils/error.js';
import { negotiate } from '../../../utils/http.js';
import { is_form_content_type, negotiate } from '../../../utils/http.js';
import { HttpError, Redirect, ValidationError } from '../../control.js';
import { handle_error_and_jsonify } from '../utils.js';

Expand Down Expand Up @@ -180,9 +180,10 @@ export async function call_action(event, actions) {
throw new Error(`No action with name '${name}' found`);
}

const type = event.request.headers.get('content-type')?.split('; ')[0];
if (type !== 'application/x-www-form-urlencoded' && type !== 'multipart/form-data') {
throw new Error(`Actions expect form-encoded data (received ${type})`);
if (!is_form_content_type(event.request)) {
throw new Error(
`Actions expect form-encoded data (received ${event.request.headers.get('content-type')}`
);
}

return action(event);
Expand Down
17 changes: 17 additions & 0 deletions packages/kit/src/utils/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,20 @@ export function negotiate(accept, types) {

return accepted;
}

/**
* Returns `true` if the request contains a `content-type` header with the given type
* @param {Request} request
* @param {...string} types
*/
export function is_content_type(request, ...types) {
const type = request.headers.get('content-type')?.split(';', 1)[0].trim() ?? '';
return types.includes(type);
}

/**
* @param {Request} request
*/
export function is_form_content_type(request) {
return is_content_type(request, 'application/x-www-form-urlencoded', 'multipart/form-data');
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
/** @type {import('./$types').ActionData} */
export let form;
async function submit() {
async function submit({ submitter }) {
const res = await fetch(this.action, {
method: 'POST',
body: new FormData(this),
body: submitter.getAttribute('formenctype') === 'multipart/form-data'
? new FormData(this)
: new URLSearchParams({ username: this['username'].value }),
headers: {
accept: 'application/json'
}
Expand All @@ -19,5 +21,6 @@

<form method="post" on:submit|preventDefault={submit}>
<input name="username" type="text" />
<button>Submit</button>
<button formenctype="multipart/form-data">Submit</button>
<button formenctype="application/x-www-form-urlencoded">Submit</button>
</form>
18 changes: 16 additions & 2 deletions packages/kit/test/apps/basics/test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1773,20 +1773,34 @@ test.describe('Actions', () => {
}
});

test('Success data is returned', async ({ page }) => {
test('Success data as form-data is returned', async ({ page }) => {
await page.goto('/actions/success-data');

expect(await page.textContent('pre')).toBe(JSON.stringify(null));

await page.type('input[name="username"]', 'foo');
await Promise.all([
page.waitForRequest((request) => request.url().includes('/actions/success-data')),
page.click('button')
page.click('button[formenctype="multipart/form-data"]')
]);

await expect(page.locator('pre')).toHaveText(JSON.stringify({ result: 'foo' }));
});

test('Success data as form-urlencoded is returned', async ({ page }) => {
await page.goto('/actions/success-data');

expect(await page.textContent('pre')).toBe(JSON.stringify(null));

await page.type('input[name="username"]', 'bar');
await Promise.all([
page.waitForRequest((request) => request.url().includes('/actions/success-data')),
page.click('button[formenctype="application/x-www-form-urlencoded"]')
]);

await expect(page.locator('pre')).toHaveText(JSON.stringify({ result: 'bar' }));
});

test('applyAction updates form prop', async ({ page, javaScriptEnabled }) => {
await page.goto('/actions/update-form');
expect(await page.textContent('pre')).toBe(JSON.stringify(null));
Expand Down

0 comments on commit 3bd5245

Please sign in to comment.