Skip to content

feat: middleware for several adapters #13477

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 49 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
8269fd8
provide emulate hook to intercept request early on
dummdidumm Feb 14, 2025
9d0beac
vercel WIP
dummdidumm Feb 15, 2025
fd8c296
fix
dummdidumm Feb 15, 2025
b5ea739
bundle ourselves instead
dummdidumm Feb 15, 2025
ac0ca4b
wrap rewrite and url automatically
dummdidumm Feb 15, 2025
9a38b48
devtimefix
dummdidumm Feb 15, 2025
2d406bd
add capability for adapters to provide additional entry points
dummdidumm Feb 18, 2025
253318a
vercel middleware
dummdidumm Feb 18, 2025
05e9b62
node
dummdidumm Feb 18, 2025
8b8c4ac
lint
dummdidumm Feb 19, 2025
344dd7b
expose new helpers from Kit
dummdidumm Feb 19, 2025
cb2d693
incorporate new helpers in Vercel adapter + fix a few bugs
dummdidumm Feb 19, 2025
db4ed04
do base path normalization within Vite middleware so adapters don't h…
dummdidumm Feb 19, 2025
b6e623f
tweak adapter API: switch allowed to disallowed, restrict imports to …
dummdidumm Feb 19, 2025
1c9b512
tests
dummdidumm Feb 19, 2025
fe1b82b
writing adapters docs
dummdidumm Feb 19, 2025
45b4706
fix
dummdidumm Feb 19, 2025
c20d423
fix lint / test
dummdidumm Feb 19, 2025
62289cb
vercel-middleware -> edge-middleware
dummdidumm Feb 19, 2025
9a3c6e2
netlify edge middleware
dummdidumm Feb 19, 2025
e994082
cloudflare adapter
dummdidumm Feb 20, 2025
05c941e
fix
dummdidumm Feb 20, 2025
1d9839a
fix
dummdidumm Feb 20, 2025
5d5eb26
fix, docs
dummdidumm Feb 20, 2025
70ad0f8
fix, docs tweaks
dummdidumm Feb 20, 2025
b159f21
fix
dummdidumm Feb 20, 2025
4335dc5
changesets
dummdidumm Feb 20, 2025
b5f68cb
drive-by notes fix
dummdidumm Feb 21, 2025
37bc902
reuse `supports` mechanism, simplify additionalEntryPoints to a recor…
dummdidumm Feb 21, 2025
5585df2
fix
dummdidumm Feb 21, 2025
0b5396e
do normalization inside adapters
dummdidumm Feb 22, 2025
76f5a9b
don't bundle sveltekit
dummdidumm Feb 22, 2025
679759f
put them into src so that they benefit from better intellisense
dummdidumm Feb 22, 2025
e39683e
fix docs
dummdidumm Feb 22, 2025
c1f7591
Merge branch 'main' into middleware-take-2
dummdidumm Feb 22, 2025
0876716
tweaks
dummdidumm Feb 24, 2025
8524d6f
netlify: make where it runs on configurable; node/preview: run it on …
dummdidumm Feb 24, 2025
6e90adc
beforeRequest -> interceptRequest
dummdidumm Feb 24, 2025
083b22d
fix test config
dummdidumm Feb 24, 2025
bec3bed
Merge branch 'main' into middleware-take-2
dummdidumm Feb 24, 2025
4e948fa
Merge remote-tracking branch 'origin/main' into middleware-take-2
dummdidumm Feb 24, 2025
b367434
Update tsconfig.json
dummdidumm Feb 24, 2025
1ee1d04
docs tweaks
dummdidumm Feb 28, 2025
a2c5222
fix folder path
dummdidumm Mar 3, 2025
30a3680
tweak
dummdidumm Mar 3, 2025
a2bb531
Merge branch 'main' into middleware-take-2
Rich-Harris Mar 3, 2025
cd116e9
docs feedback
dummdidumm Mar 3, 2025
663d59d
warn on stream read (don't error because you can't guard against it i…
dummdidumm Mar 3, 2025
f9efcbd
fix test
dummdidumm Mar 3, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/brave-trains-hang.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/adapter-vercel': minor
---

feat: support edge middleware
5 changes: 5 additions & 0 deletions .changeset/eleven-snakes-vanish.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/adapter-node': minor
---

feat: add support for polka/express middleware
5 changes: 5 additions & 0 deletions .changeset/famous-boats-enjoy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': minor
---

feat: allow adapters to influence compilation entry points and to intercept requests at dev/preview time
5 changes: 5 additions & 0 deletions .changeset/nice-tools-marry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/adapter-vercel': minor
---

feat: add support for edge middleware
5 changes: 5 additions & 0 deletions .changeset/tasty-shirts-shout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/adapter-cloudflare': minor
---

feat: add pages-like middleware
39 changes: 39 additions & 0 deletions documentation/docs/25-build-and-deploy/40-adapter-node.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,45 @@ The number of seconds to wait before forcefully closing any remaining connection

When using systemd socket activation, `IDLE_TIMEOUT` specifies the number of seconds after which the app is automatically put to sleep when receiving no requests. If not set, the app runs continuously. See [Socket activation](#Socket-activation) for more details.

## Middleware

You can integrate Express or Polka middleware into your SvelteKit application built with the Node adapter by placing a `node-middleware.js` file in your `src` folder. It must export a default function which receives the same arguments as [Express middleware](https://expressjs.com/en/guide/using-middleware.html) (if you don't use a custom server, then you may also make use of additional [Polka-specific API](https://github.com/lukeed/polka?tab=readme-ov-file#middleware), since that's what the Node adapter uses by default). Unlike the [handle](/docs/kit/hooks#Server-hooks-handle) hook, middleware runs on all requests, including for static assets and prerendered pages. If using [server-side route resolution](configuration#router) this means it runs prior to all navigations, no matter client- or server-side.

```js
/// file: node-middleware.js
// @filename: ambient.d.ts
declare module 'polka';

// @filename: index.js
// ---cut---
import { parse } from 'cookie';

/**
* @param {import('polka').Request} req
* @param {import('polka').Response} res
* @param {import('polka').NextHandler} next
*/
export default function middleware(req, res, next) {
if (req.url !== '/') return next();

// Retrieve feature flag from cookies
let flag = parse(req.headers.cookie ?? '').flag;

if (!flag) {
// Fall back to random value if this is a new visitor
flag = Math.random() > 0.5 ? 'a' : 'b';

// Set a cookie to remember the feature flags for this visitor
res.appendHeader('Set-Cookie', `flag=${flag}; Path=/`);
}

// Get destination URL based on the feature flag
req.url = flag === 'a' ? '/home-a' : '/home-b';

return next();
}
```

## Options

The adapter can be configured with various options:
Expand Down
47 changes: 47 additions & 0 deletions documentation/docs/25-build-and-deploy/60-adapter-cloudflare.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,53 @@ Cloudflare Workers specific values in the `platform` property are emulated durin

For testing the build, you should use [Wrangler](https://developers.cloudflare.com/workers/wrangler/) **version 3**. Once you have built your site, run `wrangler pages dev .svelte-kit/cloudflare`.

## Pages Middleware

You can deploy one middleware function that closely follows the [Pages Middleware API](https://developers.cloudflare.com/pages/functions/middleware/). Unlike the [handle](/docs/kit/hooks#Server-hooks-handle) hook, middleware runs on all requests, including for static assets and prerendered pages (depending on your configuration). If using [server-side route resolution](configuration#router) this means it runs prior to all navigations, no matter client- or server-side. This allows you to for example run A/B-tests on prerendered pages by rerouting a user to either variant A or B depending on a cookie.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

closely follows

In real Pages middleware you can export onRequestGet and onRequestPost and so on, and the exports can be arrays of middlewares. How committed are we to making this resemble the real thing?

I feel like we should either aim for full fidelity or avoid making it look like something it's not — I'm getting a real uncanny valley sensation to be honest


> [!NOTE] It isn't really Pages Middleware because the adapter compiles to a [single `_worker.js` file](https://developers.cloudflare.com/pages/platform/functions/#advanced-mode) (also see the [Notes](#Notes) section), which ignores middleware, but it closely mirrors its capabilities.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It isn't really Pages Middleware because the adapter compiles to a single _worker.js file

Pages Middleware and Functions are all compiled to a single _worker.js file too so I think we can omit this statement


To get started, place a `cloudflare-middleware.js` file in your `src` folder and export an `onRequest` function from it:

```js
/// file: src/cloudflare-middleware.js
import { normalizeUrl } from '@sveltejs/kit';
import { parse } from 'cookie';

/**
* @param {import('@sveltejs/adapter-cloudflare').EventContext)} context
*/
export function onRequest({ request, next }) {
const url = new URL(request.url);

if (url.pathname !== '/') return next();

// Retrieve cookies which contain the feature flags.
let flag = parse(request.headers.get('cookie') ?? '').flags;

// Fall back to random value if this is a new visitor
flag ||= Math.random() > 0.5 ? 'a' : 'b';

// Get destination URL based on the feature flag
request = new Request(new URL(flag === 'a' ? '/home-a' : '/home-b', url), request);

const response = await next(request);

// Set a cookie to remember the feature flags for this visitor
response.headers.set('Set-Cookie', `flags=${flag}; Path=/`);

return response;
}
```

The `context` parameter closely follows the [EventContext](https://developers.cloudflare.com/pages/functions/api-reference/#eventcontext) object but is missing some Pages-specific parameters such as `data`, `params` and `functionPath`.

> [!NOTE] If you want to run code prior to a request but neither have prerendered pages nor rerouting logic, then it makes more sense to use the [handle hook](hooks#Server-hooks-handle) instead.

The middleware runs on all requests that your worker is invoked for, which is dependent on the [`include/exclude` options](#Options-routes).

> [!NOTE] Locally during dev and preview this only approximates the capabilities of middleware. Notably, you cannot read the request or response body, and the `include/exclude` options are not honored.

## Notes

Functions contained in the [`/functions` directory](https://developers.cloudflare.com/pages/functions/routing/) at the project's root will _not_ be included in the deployment. Instead, functions should be implemented as [server endpoints](routing#server) in your SvelteKit app, which is compiled to a [single `_worker.js` file](https://developers.cloudflare.com/pages/functions/advanced-mode/).
Expand Down
44 changes: 44 additions & 0 deletions documentation/docs/25-build-and-deploy/80-adapter-netlify.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,50 @@ export default {
};
```

## Edge Middleware

You can deploy one Netlify Edge Function [as middleware](https://docs.netlify.com/edge-functions/api/#modify-a-response) by placing an `edge-middleware.js` file in your `src` folder. Unlike the [handle](/docs/kit/hooks#Server-hooks-handle) hook, middleware can run on all requests, including for static assets and prerendered pages. If using [server-side route resolution](configuration#router) this means it runs prior to all navigations, no matter client- or server-side. This allows you to for example run A/B-tests on prerendered pages by rerouting a user to either variant A or B depending on a cookie.

```js
/// file: edge-middleware.js
// @filename: ambient.d.ts
declare module '@netlify/edge-functions';

// @filename: index.js
// ---cut---
import { normalizeUrl } from '@sveltejs/kit';

/**
* @param {Request} request
* @param {import('@netlify/edge-functions').Context} context
*/
export default async function middleware(request, { next, cookies }) {
const url = new URL(request.url);

if (url.pathname !== '/') return next();

// Retrieve feature flag from cookies
let flag = cookies.get('flag');

if (!flag) {
// Fall back to random value if this is a new visitor
flag = Math.random() > 0.5 ? 'a' : 'b';

// Set a cookie to remember the feature flags for this visitor
cookies.set('flag', flag);
}

// Get destination URL based on the feature flag
return new URL(flag === 'a' ? '/home-a' : '/home-b', url);
}
```

[!NOTE] If you can do what you need to by using the [handle hook](hooks#Server-hooks-handle), do so. Avoid using edge middleware for requests that will end up hitting the SvelteKit server runtime (instead of e.g. static content) — it would be unnecessary (even if very small) overhead. Notable use cases include A/B testing using rerouting on prerendered pages, or adding headers to requests for static assets.

By default middleware runs on all requests except for SvelteKit-internal artifacts (such as the compiled JS files; normally within `_app/`). You can customize this by exporting a `export const config = { pattern: '<regex string>' }` object from the file similar to [how you can do it for native edge functions](https://docs.netlify.com/edge-functions/declarations/#declare-edge-functions-inline). Due to the aforementioned performance impact, you should configure this to only run on requests that actually need edge middleware.

> [!NOTE] Locally during dev and preview this only approximates the capabilities of edge functions. Notably, you cannot read the request or response body, and many properties on the context object are `null`ed.

## Netlify alternatives to SvelteKit functionality

You may build your app using functionality provided directly by SvelteKit without relying on any Netlify functionality. Using the SvelteKit versions of these features will allow them to be used in dev mode, tested with integration tests, and to work with other adapters should you ever decide to switch away from Netlify. However, in some scenarios you may find it beneficial to use the Netlify versions of these features. One example would be if you're migrating an app that's already hosted on Netlify to SvelteKit.
Expand Down
51 changes: 48 additions & 3 deletions documentation/docs/25-build-and-deploy/90-adapter-vercel.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export default {

Vercel supports [Incremental Static Regeneration](https://vercel.com/docs/incremental-static-regeneration) (ISR), which provides the performance and cost advantages of prerendered content with the flexibility of dynamically rendered content.

> Use ISR only on routes where every visitor should see the same content (much like when you prerender). If there's anything user-specific happening (like session cookies), they should happen on the client via JavaScript only to not leak sensitive information across visits
> [!NOTE] Use ISR only on routes where every visitor should see the same content (much like when you prerender). If there's anything user-specific happening (like session cookies), they should happen on the client via JavaScript only to not leak sensitive information across visits

To add ISR to a route, include the `isr` property in your `config` object:

Expand All @@ -107,7 +107,7 @@ export const config = {
};
```

> Using ISR on a route with `export const prerender = true` will have no effect, since the route is prerendered at build time
> [!NOTE] Using ISR on a route with `export const prerender = true` will have no effect, since the route is prerendered at build time

The `expiration` property is required; all others are optional. The properties are discussed in more detail below.

Expand Down Expand Up @@ -139,7 +139,52 @@ vercel env pull .env.development.local

A list of valid query parameters that contribute to the cache key. Other parameters (such as utm tracking codes) will be ignored, ensuring that they do not result in content being re-generated unnecessarily. By default, query parameters are ignored.

> Pages that are [prerendered](page-options#prerender) will ignore ISR configuration.
> [!NOTE] Pages that are [prerendered](page-options#prerender) will ignore ISR configuration.

## Edge Middleware

You can make use of [Vercel Edge Middleware](https://vercel.com/docs/functions/edge-middleware) by placing an `edge-middleware.js` file in your `src` folder. Unlike the [handle](/docs/kit/hooks#Server-hooks-handle) hook, middleware can run on all requests, including for static assets and prerendered or ISRed pages. If using [server-side route resolution](configuration#router) this means it runs prior to all navigations, no matter client- or server-side. This allows you to, for example, run A/B tests on prerendered or ISRed pages by rerouting a user to either variant A or B depending on a cookie.

```js
/// file: edge-middleware.js
// @filename: ambient.d.ts
declare module '@vercel/edge';

// @filename: index.js
// ---cut---
import { rewrite, next } from '@vercel/edge';
import { parse } from 'cookie';

/**
* @param {Request} request
*/
export default async function middleware(request) {
const url = new URL(request.url);

if (url.pathname !== '/') return next();

// Retrieve feature flag from cookies
let flag = parse(request.headers.get('cookie') ?? '').flag;

// Fall back to random value if this is a new visitor
flag ||= Math.random() > 0.5 ? 'a' : 'b';

return rewrite(
// Get destination URL based on the feature flag
flag === 'a' ? '/home-a' : '/home-b',
{
headers: {
// Set a cookie to remember the feature flags for this visitor
'Set-Cookie': `flag=${flag}; Path=/`
}
}
);
}
```

> [!NOTE] If you can do what you need to by using the [handle hook](hooks#Server-hooks-handle), do so. Avoid using edge middleware for requests that will end up hitting the SvelteKit server runtime (instead of e.g. an ISRed page or static content) — it would be unnecessary (even if very small) overhead. Notable use cases for edge middleware include A/B testing using rewrites on prerendered pages, or running lightweight logic (such as adding headers) while serving ISRed content.

By default, middleware runs on all requests except for SvelteKit-internal artifacts (such as the compiled JS files; normally within `_app/`). You can customize this by exporting a `config` object with a `matcher` property as described in Vercel's [API documentation](https://vercel.com/docs/functions/edge-middleware/middleware-api#match-paths-based-on-custom-matcher-config). Due to the aforementioned performance impact, you should configure this to only run on requests that actually need edge middleware.

## Environment variables

Expand Down
15 changes: 14 additions & 1 deletion documentation/docs/25-build-and-deploy/99-writing-adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,18 @@ export default function (options) {
async adapt(builder) {
// adapter implementation
},
async emulate() {
async emulate({ importEntryPoint }) {
return {
async platform({ config, prerender }) {
// the returned object becomes `event.platform` during dev, build and
// preview. Its shape is that of `App.Platform`
},
async interceptRequest(req, res, next) {
// Allows you to run code before a request to a prerendered page, a static asset,
// or a regular request to the SvelteKit runtime, both in dev and preview mode.
// Allows you to for example replicate middleware during dev and preview.
const module = await importEntryPoint('additional-entry-point');
module.default(req, res, next);
}
}
},
Expand All @@ -35,6 +42,12 @@ export default function (options) {
// from `$app/server` in production, return `false` if it can't.
// Or throw a descriptive error describing how to configure the deployment
}
},
// Allows you to configure additional entry points for compilation.
// You can use these via `importEntryPoint` within `emulate` and reference them
// from `${builder.getServerDirectory()}/adapter/<name>.js` for further compilation/bundling.
additionalEntryPoints: {
'additional-entry-point': 'my-project-root-relative-file.js'
}
};

Expand Down
13 changes: 13 additions & 0 deletions packages/adapter-cloudflare/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,16 @@ export interface RoutesJSONSpec {
include: string[];
exclude: string[];
}

/**
* The type of the parameter that is passed to the middleware.
* Closely modelled after Cloudflare's EventContext type.
*/
export interface EventContext<Bindings = Record<string, any>> {
request: Request;
bindings?: Bindings;
waitUntil(f: any): void;
passThroughOnException(): void;
env: Record<string, string> & { ASSETS: { fetch: typeof fetch } };
next: (input?: Request | string, init?: RequestInit) => Promise<Response>;
}
Loading
Loading