iTranslated by AI
Issue with SvelteKit Named Actions when using Lambda Function URLs
Background
This is an issue that occurred in a configuration where SvelteKit is hosted on a containerized Lambda and delivered via Lambda Function URL. When using a SvelteKit named action and sending a POST request, the named action itself is not recognized and returns a 400 error.
{"message":null}
SvelteKit's named action
In SvelteKit, you can POST data to the server by exporting actions from +page.server.ts.
import type { Actions } from './$types';
export const actions: Actions = {
default: async (event) => {
// TODO log the user in
}
};
Furthermore, in SvelteKit:
You can have as many named actions as you want instead of a single default action
import type { Actions } from './$types';
export const actions = {
- default: async (event) => {
+ login: async (event) => {
// TODO log the user in
},
+ register: async (event) => {
+ // TODO register the user
+ }
} satisfies Actions;
This is a useful feature when you want to place multiple actions on the same page (path). To call a named action, you add the name of that action prefixed with / to the query parameters.
In the example above, when logging in, the process written in login is called by POSTing to ?/login.
Making named actions work with Lambda Function URL
As mentioned at the beginning, when using a Lambda Function URL, the named action itself is not recognized and returns a 400 error.
{"message":null}
It is easier to understand when compared with the usage of general query parameters; it seems that the fact that the key contains special characters and the value is empty is causing the problem.
| Key | Value | |
|---|---|---|
| General query parameter | page |
1 |
| Query parameter used by a named action | /login |
(empty) |
Through trial and error, I found that if the query parameter key contains /, the Lambda Function URL rejects it with a 400. To make it work correctly, you must URL-encode the / in the query parameter key as %2F when making the request. The fact that the value is empty did not seem to be an issue.
- <form method="POST" action="?/login">
+ <form method="POST" action="?%2Flogin">
If you are already making heavy use of named actions, fixing them all is a lot of work, and the risk of implementation oversights will always be present. While there is an adapter that includes an implementation to replace with %2F, it hasn't been updated since 2024 (as of January 1, 2026), so caution is needed when using it.
Current solution
Avoiding replacement logic in leaf-level code is preferable because it can become a risk for bugs. Therefore, we will use Svelte's Hooks.
'Hooks' are app-wide functions you declare that SvelteKit will call in response to specific events, giving you fine-grained control over the framework's behaviour.
Since we are adding server-side processing this time, we create src/hooks.server.ts and add logic to the handle function.
export const handle: Handle = async ({ event, resolve }) => {
// Some processing
return resolve(event);
};
The
handlefunction runs every time the SvelteKit server receives a request and determines the response. It receives aneventobject representing the request and a function calledresolve, which renders the route and generates a response. This allows you to modify response headers or body, or bypass SvelteKit entirely (for example, to implement routes programmatically).
resolve supports an optional second argument, and by using transformPageChunk, you can create a response by adding arbitrary transformation processing to the HTML. Here, we replace / with %2F.
export const handle: Handle = async ({ event, resolve }) => {
return resolve(event, {
transformPageChunk: ({ html }) => html.replaceAll('action="?/', 'action="?%2F'),
});
};
This allows you to encode the URLs without having to modify each page file that uses named actions.
However, please note that this is not a complete solution because the transformation will not occur if you call the named action from a client-side fetch.
// Since it's not replaced, it results in a 400 error
const response = await fetch("?/login", {
method: "POST",
body,
});
When using Lambda Function URL, if you use forms, you can perform the replacement in Hooks; alternatively, using default actions instead of named actions might allow for safer operation.
Discussion