Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
129 commits
Select commit Hold shift + click to select a range
277389a
get it working
teemingc Jul 21, 2026
9d8040f
fix remote import
teemingc Jul 21, 2026
e97c6a1
replace ssrLoadModule and serialise assets path correctly
teemingc Jul 21, 2026
0c78c0e
ensure it works with vitest
teemingc Jul 21, 2026
0c19777
doc fix
teemingc Jul 21, 2026
ceaebc8
Merge branch 'version-3' into feat-server-entrypoint
teemingc Jul 21, 2026
3ff979b
import only after server has started
teemingc Jul 22, 2026
f8245f3
Update index.js
teemingc Jul 22, 2026
5e0e3f0
Merge branch 'version-3' into feat-server-entrypoint
teemingc Jul 22, 2026
0ba765a
prettier
teemingc Jul 22, 2026
9e0a2a9
fix
teemingc Jul 22, 2026
8fc3473
format
teemingc Jul 22, 2026
fd47988
standardise handler
teemingc Jul 22, 2026
4a3bae7
Merge branch 'version-3' into feat-server-entrypoint
teemingc Jul 22, 2026
930dfd5
resolve to ssr entry correctly
teemingc Jul 22, 2026
68e7364
format
teemingc Jul 22, 2026
a4c64f9
doc fix
teemingc Jul 22, 2026
bcc1de7
Merge branch 'version-3' into feat-server-entrypoint
teemingc Jul 23, 2026
88fb3fb
fix trace facade not exporting fetch handler
teemingc Jul 23, 2026
dd97b18
remove Vite fs prefix just before reading
teemingc Jul 23, 2026
35ac374
avoid collision
teemingc Jul 23, 2026
7391962
Update 99-writing-adapters.md
teemingc Jul 23, 2026
0bd9813
invalidate dev manifest data
teemingc Jul 23, 2026
a2a5446
Merge branch 'version-3' into feat-server-entrypoint
teemingc Jul 23, 2026
42eac09
format
teemingc Jul 23, 2026
9b751f6
windows
teemingc Jul 23, 2026
b403f30
revert
teemingc Jul 23, 2026
f2f90c0
format
teemingc Jul 23, 2026
14ee428
this doesn't belong here
teemingc Jul 23, 2026
a4f48bc
format
teemingc Jul 23, 2026
483fff6
base path in manifest is not needed yet
teemingc Jul 23, 2026
90ecc99
replace ssrLoadModule
teemingc Jul 23, 2026
a5beefd
remove comment
teemingc Jul 23, 2026
eb70e15
Merge branch 'runnable-dev-environment' into feat-server-entrypoint
teemingc Jul 23, 2026
f203fc6
Fix: The `change` file watcher dereferences `manifest_data.nodes` bef…
vercel[bot] Jul 23, 2026
b8bd2bd
regenerate types
teemingc Jul 23, 2026
8da087a
Merge branch 'runnable-dev-environment' into feat-server-entrypoint
teemingc Jul 23, 2026
b29f339
missed this
teemingc Jul 23, 2026
79b8d5f
Update packages/kit/src/exports/vite/dev/server.js
teemingc Jul 23, 2026
da976b8
add again
teemingc Jul 23, 2026
fc5d32f
Update packages/kit/src/exports/vite/dev/index.js
teemingc Jul 23, 2026
9bfd903
Update packages/kit/src/exports/vite/dev/index.js
teemingc Jul 23, 2026
e2cafbe
Merge branch 'version-3' into runnable-dev-environment
Rich-Harris Jul 23, 2026
1e548d7
merge runnable-dev-environment
Rich-Harris Jul 23, 2026
28a0e73
DRY
Rich-Harris Jul 24, 2026
77883dc
Update packages/kit/src/exports/vite/dev/index.js
teemingc Jul 24, 2026
0dfd7e2
merge runnable-dev-environment
Rich-Harris Jul 24, 2026
676a9a7
merge version-3. sure hope i did this right! github: your stacked PR …
Rich-Harris Jul 24, 2026
f109f0f
Merge branch 'feat-server-entrypoint' of https://github.com/sveltejs/…
teemingc Jul 24, 2026
391049c
remove merge conflict markers
teemingc Jul 24, 2026
dbc8def
remove manifest data virtual module
teemingc Jul 24, 2026
71416d3
didnt get them all the first try
teemingc Jul 24, 2026
f2e57ad
invalidate ssr manifest module
teemingc Jul 24, 2026
42e0ef8
no longer true
teemingc Jul 24, 2026
7db1c35
vercel bot fix
teemingc Jul 24, 2026
49c0556
only reload for $app/manifest
teemingc Jul 24, 2026
36e2c21
not needed
teemingc Jul 24, 2026
c97be4d
clean up
teemingc Jul 24, 2026
87d28df
not needed anymore
teemingc Jul 24, 2026
a0828b5
log environment errors
teemingc Jul 24, 2026
9108ed4
Update utils.js
teemingc Jul 24, 2026
ba380f7
forward server errors
teemingc Jul 24, 2026
7ab3973
add types
teemingc Jul 24, 2026
ea16240
Merge branch 'version-3' into feat-server-entrypoint
teemingc Jul 24, 2026
b677c2e
Merge branch 'version-3' into feat-server-entrypoint
teemingc Jul 26, 2026
8649cf7
initial rework
teemingc Jul 27, 2026
704ef2d
make it work in prerender and preview
teemingc Jul 27, 2026
c282ab9
Update 99-writing-adapters.md
teemingc Jul 27, 2026
19a9246
update adapters
teemingc Jul 27, 2026
c4e988e
fix this needs to be an anonymous arrow function
teemingc Jul 27, 2026
9452c0f
add debug condition
teemingc Jul 27, 2026
65a276c
Merge branch 'version-3' into feat-server-entrypoint
teemingc Jul 27, 2026
d23caf0
set the right this context
teemingc Jul 27, 2026
497ac15
use bind
teemingc Jul 27, 2026
681b579
missed a spot
teemingc Jul 27, 2026
d6cb109
fine we'll keep supporting emulate
teemingc Jul 27, 2026
f7bf2ae
log error
teemingc Jul 27, 2026
d7246bb
oopsie
teemingc Jul 27, 2026
3c506e9
Update index.js
teemingc Jul 28, 2026
b6c1196
Update 99-writing-adapters.md
teemingc Jul 28, 2026
26c78ec
try fix docs
teemingc Jul 28, 2026
a55dfa4
format
teemingc Jul 28, 2026
9d7d821
disable vite error overlay during tests
teemingc Jul 28, 2026
db3e67b
fix docs
teemingc Jul 28, 2026
cba1b7b
format again? what's wrong with prettier
teemingc Jul 28, 2026
c5b8c2a
chore: autofix lint
github-actions[bot] Jul 28, 2026
20698cd
try fix docs
teemingc Jul 28, 2026
a726414
order imports
teemingc Jul 28, 2026
9fe12dd
Merge branch 'feat-server-entrypoint' of https://github.com/sveltejs/…
teemingc Jul 28, 2026
e5d69b2
split out change
teemingc Jul 28, 2026
2023400
split out
teemingc Jul 28, 2026
e8a1129
split out
teemingc Jul 28, 2026
0329d9e
ditto
teemingc Jul 28, 2026
03b5ff4
split out
teemingc Jul 28, 2026
d99d5fd
revert
teemingc Jul 28, 2026
e4bee61
add function name to docs snippet
teemingc Jul 28, 2026
793c618
oops
teemingc Jul 28, 2026
7ab6d0a
revert
teemingc Jul 28, 2026
7504403
revert
teemingc Jul 28, 2026
d435959
remove one step of indirection
teemingc Jul 28, 2026
05c41b2
split ssr manifest refactoring changes
teemingc Jul 28, 2026
4c67f0c
split out
teemingc Jul 28, 2026
cddfef0
unused
teemingc Jul 28, 2026
e21944d
fixes
teemingc Jul 28, 2026
4984432
add tests
teemingc Jul 28, 2026
d3ac896
Delete utils.js
teemingc Jul 28, 2026
7622a82
typo
teemingc Jul 28, 2026
040296d
keep state.read in preview
teemingc Jul 28, 2026
cd62efb
unused type
teemingc Jul 28, 2026
c1756ad
revert
teemingc Jul 28, 2026
42de023
clean up and fixes
teemingc Jul 28, 2026
c1aa38f
this goes in here
teemingc Jul 28, 2026
942d110
Merge branch 'version-3' into feat-server-entrypoint
teemingc Jul 28, 2026
1313b63
separate into its own pr
teemingc Jul 28, 2026
8e47611
make server.init optional
teemingc Jul 28, 2026
9ba8540
Merge branch 'version-3' into feat-server-entrypoint
teemingc Jul 28, 2026
c3d7734
Update documentation/docs/25-build-and-deploy/99-writing-adapters.md
teemingc Jul 29, 2026
7d70597
Update documentation/docs/25-build-and-deploy/99-writing-adapters.md
teemingc Jul 29, 2026
b8f5830
Update documentation/docs/25-build-and-deploy/99-writing-adapters.md
teemingc Jul 29, 2026
df67547
Update documentation/docs/25-build-and-deploy/99-writing-adapters.md
teemingc Jul 29, 2026
76e27c3
Merge branch 'version-3' into feat-server-entrypoint
teemingc Jul 29, 2026
cf92069
feat: simplify custom entrypoint stuff (#16560)
Rich-Harris Jul 29, 2026
bcf3ffa
Merge branch 'version-3' into feat-server-entrypoint
teemingc Jul 29, 2026
673ff67
remove
teemingc Jul 29, 2026
6ea7c35
unused
teemingc Jul 29, 2026
fe45851
revert
teemingc Jul 29, 2026
5948330
Fix: In `vite preview`, the custom/default handler's one-time setup f…
vercel[bot] Jul 29, 2026
cb408b7
Fix: The custom/default `SSRHandler` setup function `init_server` is …
vercel[bot] Jul 29, 2026
cf9539c
Update documentation/docs/25-build-and-deploy/99-writing-adapters.md
Rich-Harris Jul 29, 2026
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/custom-server-handler.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': minor
---

feat: allow adapters to provide a `customHandler` that handles requests to the server
54 changes: 53 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 @@ -40,6 +40,8 @@ export default function (options) {
// Return `false if it can't, or throw a descriptive error.
}
},
// Specify the path of a module to customise request handling
customHandler: import.meta.resolve('./handler.js'),
vite: {
plugins: [
// add plugins here to integrate with Vite
Expand All @@ -51,7 +53,7 @@ export default function (options) {
}
```

Of these, `name` and `adapt` are required. `emulate`, `vite.plugins` and `supports` are optional.
Of these, `name` and `adapt` are required. `emulate`, `customHandler`, `vite.plugins` and `supports` are optional.

Within the `adapt` method, there are a number of things that an adapter should do:

Expand All @@ -66,3 +68,53 @@ Within the `adapt` method, there are a number of things that an adapter should d
- Put the user's static files and the generated JS/CSS in the correct location for the target platform

Where possible, we recommend putting the adapter output under the `build/` directory with any intermediate output placed under `.svelte-kit/[adapter-name]`.

## Custom request handler

You can customise your server's initialisation and request handling by adding a `customHandler` property resolved to the path of your handler file.

```js
// @errors: 2322
// @filename: ambient.d.ts
type AdapterSpecificOptions = any;

// @filename: index.js
// ---cut---
/** @param {AdapterSpecificOptions} options */
export default function (options) {
/** @type {import('@sveltejs/kit').Adapter} */
const adapter = {
name: 'adapter-package-name',
async adapt(builder) {
// adapter implementation
},
+++customHandler: import.meta.resolve('./src/handler.js')+++
}

return adapter;
};
```

The handler file should export a default function that returns a request handler. The should return a `Response` such as by calling `server.respond` along with any [platform-specific context](types#Platform).

```js
/// file: src/handler.js
// @errors: 2322
/** @type {import('@sveltejs/kit').SSRHandler} */
export default async function handler(server) {
// perform setup work here

return async (request) => {
// custom request/response handling logic goes here
Comment thread
teemingc marked this conversation as resolved.
Comment thread
Rich-Harris marked this conversation as resolved.
if (new URL(request.url).pathname === '/_ping') {
return new Response('pong');
}

return await server.respond(request, {
platform: {
// the shape of `App.Platform`
}
});
};
}
```
2 changes: 1 addition & 1 deletion packages/adapter-cloudflare/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ export default function (options = {}) {
// will be interpreted as escape characters and create an incorrect import path.
// We also need to ensure the relative imports start with ./ since Wrangler
// errors if a relative import looks like a package import
SERVER: `./${posixify(path.relative(worker_dest_dir, builder.getServerDirectory()))}/index.js`,
SERVER: `./${posixify(path.relative(worker_dest_dir, builder.getServerDirectory()))}/server.js`,
MANIFEST: `./${posixify(path.relative(worker_dest_dir, tmp))}/manifest.js`,
ASSETS: assets_binding
}
Expand Down
4 changes: 2 additions & 2 deletions packages/adapter-netlify/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ function generate_serverless_functions({ builder, publish, split }) {
builder.writeServer('.netlify/v1/server');

const replace = {
'0SERVER': './server/index.js' // digit prefix prevents CJS build from using this as a variable name, which would also get replaced
'0SERVER': './server/server.js' // digit prefix prevents CJS build from using this as a variable name, which would also get replaced
};

builder.copy(files, '.netlify/v1', { replace, filter: (file) => !file.endsWith('edge.js') });
Expand Down Expand Up @@ -365,7 +365,7 @@ async function generate_edge_functions({ builder }) {

builder.copy(`${files}/edge.js`, `${tmp}/entry.js`, {
replace: {
'0SERVER': `${relativePath}/index.js`,
'0SERVER': `${relativePath}/server.js`,
MANIFEST: './manifest.js'
}
});
Expand Down
2 changes: 1 addition & 1 deletion packages/adapter-node/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export default function (opts = {}) {
// resolve the app's server and manifest, generated above
name: 'adapter-node-resolve-app',
resolveId(id) {
if (id === 'SERVER') return `${server}/index.js`;
if (id === 'SERVER') return `${server}/server.js`;
if (id === 'MANIFEST') return `${server}/manifest.js`;
}
},
Expand Down
8 changes: 4 additions & 4 deletions packages/adapter-vercel/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,15 @@ const plugin = function (defaults = {}) {
const dir = `${dirs.functions}/${name}.func`;

const relativePath = path.posix.relative(tmp, builder.getServerDirectory());
builder.copy(`${files}/serverless.js`, `${tmp}/index.js`, {
builder.copy(`${files}/serverless.js`, `${tmp}/server.js`, {
replace: {
SERVER: `${relativePath}/index.js`,
SERVER: `${relativePath}/server.js`,
MANIFEST: './manifest.js'
}
});
if (builder.hasServerInstrumentationFile()) {
builder.instrument({
entrypoint: `${tmp}/index.js`,
entrypoint: `${tmp}/server.js`,
instrumentation: `${builder.getServerDirectory()}/instrumentation.server.js`
});
}
Expand All @@ -74,7 +74,7 @@ const plugin = function (defaults = {}) {
`export const manifest = ${builder.generateManifest({ relativePath, routes })};\n`
);

await create_function_bundle(builder, `${tmp}/index.js`, dir, config);
await create_function_bundle(builder, `${tmp}/server.js`, dir, config);

for (const asset of builder.findServerAssets(routes)) {
// TODO use symlinks, once Build Output API supports doing so
Expand Down
43 changes: 30 additions & 13 deletions packages/kit/src/core/postbuild/fallback.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/** @import { SSRHandler } from '@sveltejs/kit' */
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { pathToFileURL } from 'node:url';
Expand All @@ -22,28 +23,44 @@ async function generate_fallback({ manifest_path, env, out_dir, origin, assets }
const { set_building } = await import(pathToFileURL(`${server_root}/server/internal.js`).href);

/** @type {import('types').ServerModule} */
const { Server } = await import(pathToFileURL(`${server_root}/server/index.js`).href);
const { Server } = await import(pathToFileURL(`${server_root}/server/server.js`).href);

/** @type {import('@sveltejs/kit').SSRManifest} */
const manifest = (await import(pathToFileURL(manifest_path).href)).manifest;

set_building();

const server = new Server(manifest);
await server.init({ env });

const response = await server.respond(new Request(origin + '/[fallback]'), {
getClientAddress: () => {
throw new Error('Cannot read clientAddress during prerendering');
},
prerendering: {
fallback: true,
dependencies: new Map(),
remote_responses: new Map()
},
read: (file) => readFileSync(join(assets, file))
await server.init({
env,
read: (file) => {
throw new Error(`Cannot call \`read\` for ${file} while prerendering a fallback page`);
}
});

/** @type {{ default: SSRHandler }} */
const { default: init_server } = await import(
pathToFileURL(`${server_root}/server/handler.js`).href
);
const respond = await init_server({
respond: (request, options) => {
return server.respond(request, {
...options,
getClientAddress: () => {
throw new Error('Cannot read clientAddress during prerendering');
},
prerendering: {
fallback: true,
dependencies: new Map(),
remote_responses: new Map()
},
read: (file) => readFileSync(join(assets, file))
});
}
});

const response = await respond(new Request(origin + '/[fallback]'));

if (response.ok) {
return await response.text();
}
Expand Down
71 changes: 48 additions & 23 deletions packages/kit/src/core/postbuild/prerender.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/** @import { SSRHandler } from '@sveltejs/kit' */
import { existsSync, readFileSync, statSync, writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { pathToFileURL } from 'node:url';
Expand Down Expand Up @@ -56,7 +57,10 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit
set_env(env);

/** @type {import('types').ServerModule} */
const { Server } = await import(pathToFileURL(`${out}/server/index.js`).href);
const { Server } = await import(pathToFileURL(`${out}/server/server.js`).href);

/** @type {{ default: SSRHandler }} */
const { default: init_server } = await import(pathToFileURL(`${out}/server/handler.js`).href);

const throw_handled = () => {
throw new Error('__handled__');
Expand Down Expand Up @@ -262,6 +266,13 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit
/** @type {Map<string, Promise<any>>} */
const remote_responses = new Map();

// the custom/default handler's setup phase must run only once (per the `SSRHandler`
// contract), so `respond` is created a single time below. Per-visit state (the
// `dependencies` map) is threaded through to the shared handler via this `WeakMap`,
// keyed on the `Request` that flows into `server.respond`.
/** @type {WeakMap<Request, Map<string, import('types').PrerenderDependency>>} */
const dependencies_by_request = new WeakMap();

/** @type {Map<string, Set<string>>} */
const expected_hashlinks = new Map();

Expand Down Expand Up @@ -301,29 +312,11 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit

const request = new Request(prerender_origin + encoded);

const response = await server.respond(request, {
getClientAddress() {
throw new Error('Cannot read clientAddress during prerendering');
},
prerendering: {
dependencies,
remote_responses
},
read: (file) => {
// stuff we just wrote
const filepath = saved.get(file);
if (filepath) return readFileSync(filepath);

// Static assets emitted during build
if (file.startsWith(config.appDir)) {
return readFileSync(`${out}/server/${file}`);
}
// thread the per-visit `dependencies` map through to the shared `respond`
// handler (created once, after `server.init` below)
dependencies_by_request.set(request, dependencies);

// stuff in `static`
return readFileSync(join(config.files.assets, file));
},
emulator
});
const response = await respond(request);

const encoded_id = response.headers.get('x-sveltekit-routeid');
const decoded_id = encoded_id && decode_uri(encoded_id);
Expand Down Expand Up @@ -594,6 +587,38 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit
read: (file) => createReadableStream(`${config.outDir}/output/server/${file}`)
});

// create the custom/default handler once, honouring the `SSRHandler`
// "setup runs once" contract. Per-visit `dependencies` are looked up from
// `dependencies_by_request` using the `Request` passed to `server.respond`.
const respond = await init_server({
respond: (request, options) => {
return server.respond(request, {
...options,
getClientAddress() {
throw new Error('Cannot read clientAddress during prerendering');
},
prerendering: {
dependencies: dependencies_by_request.get(request) ?? new Map(),
remote_responses
},
read: (file) => {
// stuff we just wrote
const filepath = saved.get(file);
if (filepath) return readFileSync(filepath);

// Static assets emitted during build
if (file.startsWith(config.appDir)) {
return readFileSync(`${out}/server/${file}`);
}

// stuff in `static`
return readFileSync(join(config.files.assets, file));
},
emulator
});
}
});

log.info('Prerendering');

for (const entry of config.prerender.entries) {
Expand Down
13 changes: 13 additions & 0 deletions packages/kit/src/exports/public.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ export interface Adapter {
* during dev, build and prerendering.
*/
emulate?: () => MaybePromise<Emulator>;
/**
* The path to a module whose default export is a [custom handler](https://svelte.dev/docs/kit/writing-adapters#Custom-request-handler).
* @since 3.0.0
*/
customHandler?: string;
vite?: {
/**
* Plugins provided by the adapter are placed before any of SvelteKit's own plugins.
Expand All @@ -80,6 +85,14 @@ export interface Adapter {
};
}

/**
* The [custom handler](https://svelte.dev/docs/kit/writing-adapters#Custom-request-handler) an adapter can specify to customise request handling. It is expected to return a function which returns a `Response` or calls `server.respond` with any [platform-specific context](https://svelte.dev/docs/kit/types#Platform).
* @since 3.0.0
*/
export type SSRHandler = (server: {
respond: (request: Request, options?: Pick<RequestOptions, 'platform'>) => Promise<Response>;
}) => MaybePromise<(request: Request) => MaybePromise<Response>>;

export type LoadProperties<input extends Record<string, any> | void> = input extends void
? undefined // needs to be undefined, because void will break intellisense
: input extends Record<string, any>
Expand Down
Loading
Loading