iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
😢

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

Named-actions | Docs・Svelte

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.

Hooks | Docs・Svelte

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 handle function runs every time the SvelteKit server receives a request and determines the response. It receives an event object representing the request and a function called resolve, 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).

Hooks / handle | Docs・Svelte

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.

References

Discussion