feat: custom server entry points during development - #16464
Merged
Conversation
|
Install the latest version of pnpm add https://pkg.svelte.dev/@sveltejs/kit/c/cf9539cd725bc18f1c395b97d6aaa5f55d6bc976Open in |
🦋 Changeset detectedLatest commit: cf9539c The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
teemingc
added a commit
that referenced
this pull request
Jul 28, 2026
split out from #16464 Errors that happen in the [main Vite dev server middleware](https://github.com/sveltejs/kit/blob/680cc413ea5f4f71b083d2cc306ce73b390439dd/packages/kit/src/exports/vite/dev/index.js#L496) aren't very visible since only its message appears in the browser right now. This PR fixes that by logging the error on the server and its fixed stack trace (if any). --- ### Please don't delete this checklist! Before submitting the PR, please make sure you do the following: - [ ] It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs - [x] This message body should clearly illustrate what problems it solves. - [ ] Ideally, include a test that fails without this PR but passes with it. ### Tests - [ ] Run the tests with `pnpm test` and lint the project with `pnpm lint` and `pnpm check` ### Changesets - [x] If your PR makes a change that should be noted in one or more packages' changelogs, generate a changeset by running `pnpm changeset` and following the prompts. Changesets that add features should be `minor` and those that fix bugs should be `patch`. Please prefix changeset messages with `feat:`, `fix:`, or `chore:`. ### Edits - [ ] Please ensure that 'Allow edits from maintainers' is checked. PRs without this option may be closed.
teemingc
added a commit
that referenced
this pull request
Jul 28, 2026
…d it exists (#16548) extracting changes from #16464 We currently only error during build but it would be good to error earlier so folks know their adapter doesn't support instrumentation if they created such a file. --- ### Please don't delete this checklist! Before submitting the PR, please make sure you do the following: - [ ] It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs - [x] This message body should clearly illustrate what problems it solves. - [ ] Ideally, include a test that fails without this PR but passes with it. ### Tests - [ ] Run the tests with `pnpm test` and lint the project with `pnpm lint` and `pnpm check` ### Changesets - [x] If your PR makes a change that should be noted in one or more packages' changelogs, generate a changeset by running `pnpm changeset` and following the prompts. Changesets that add features should be `minor` and those that fix bugs should be `patch`. Please prefix changeset messages with `feat:`, `fix:`, or `chore:`. ### Edits - [x] Please ensure that 'Allow edits from maintainers' is checked. PRs without this option may be closed.
Rich-Harris
requested changes
Jul 29, 2026
Rich-Harris
left a comment
Member
There was a problem hiding this comment.
I think we can simplify this a fair chunk, both API-wise (as mentioned above, making the adapter author deal with env and read etc feels unnecessary) and implementation-wise (rather than having a new plugin, we can just consistently have an entry for the handler that's basically customHandler ?? ${...}/handler.js`. Opened #16560 as a starting point
Co-authored-by: Rich Harris <richard.a.harris@gmail.com>
Co-authored-by: Rich Harris <richard.a.harris@gmail.com>
Co-authored-by: Rich Harris <richard.a.harris@gmail.com>
Co-authored-by: Rich Harris <richard.a.harris@gmail.com>
continuation of #16464. This isn't complete — the types need a little finessing (in the handler, it probably doesn't make sense for `server` to have an `init` method, and the second argument to `server.respond` should be optional and only include `platform`), and `getClientAddress` doesn't currently work. But you get the idea --------- Co-authored-by: Tee Ming <chewteeming01@gmail.com> Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
teemingc
commented
Jul 29, 2026
…unction (`init_server`) runs on every request instead of once at server startup, causing per-request re-initialization of stateful setup (DB pools, workers, listeners).
This commit fixes the issue reported at packages/kit/src/exports/vite/preview/index.js:232
## Bug
In `packages/kit/src/exports/vite/preview/index.js`, the SSR middleware imported `handler.js` and called `init_server({ respond })` **inside the per-request middleware callback**:
```js
vite.middlewares.use(async (req, res) => {
...
const { default: init_server } = await import(pathToFileURL(`
```
The `SSRHandler` contract and the `writing-adapters.md` docs explicitly document the outer function as the place to *"perform setup work here"* — i.e. one-time initialization such as opening a DB connection/pool, spawning a worker, or registering listeners. The returned inner function is the per-request handler.
In `preview`, the `Server` instance is created **once** at startup, yet `init_server` was being invoked **on every request**. This means:
* The dynamic `import()` of the handler runs per request (cheap after caching, but still wrong).
* More importantly, any stateful one-time setup inside the custom handler re-runs on every request, leaking resources (connection pools, workers) or producing incorrect behaviour.
**Concrete trigger:** an adapter with a `customHandler` that opens a resource in the setup phase, e.g.
```js
export default async function handler(server) {
const pool = createPool(); // runs once per contract
return (request) => server.respond(request, { platform: { pool } });
}
```
Under `vite preview`, `createPool()` executes for every incoming HTTP request, exhausting connections. The default handler happens to be stateless, so the bug is invisible with the default, which is why it slipped through.
The reason the call was inlined is that the `respond` closure needs the per-request `req.socket` for `getClientAddress`. Unlike dev (which recreates a fresh `Server` per request for HMR and therefore legitimately re-inits), preview reuses a single long-lived `Server`, so init must be hoisted.
## Fix
Hoisted the handler import and `init_server({ respond })` call to run **once** at preview startup (right after `emulator` is computed, before the returned config function). To preserve the per-request client address inside the now long-lived `respond` closure, I added a `WeakMap<Request, IncomingMessage>` that maps each incoming web `Request` to its originating Node request. The middleware registers the mapping (`request_sockets.set(request, req)`) before calling `respond`, and `getClientAddress` looks the socket up from the map:
```js
const respond = await init_server({
respond: (request, options) => {
const req = request_sockets.get(request);
return server.respond(request, {
...options,
getClientAddress: () => {
const remoteAddress = req?.socket.remoteAddress;
if (remoteAddress) return remoteAddress;
throw new Error('Could not determine clientAddress');
},
read: (file) => { ... },
emulator
});
}
});
```
Since `getRequest` returns the same `Request` object that flows through the default/custom handler into `server.respond`, the `WeakMap` lookup resolves correctly for the common path, and falls back to the existing "Could not determine clientAddress" error otherwise (same behaviour as before when the socket had no `remoteAddress`).
Behaviour is otherwise unchanged; only the setup-once semantics are restored.
See notes for the analogous (unfixed, lower-impact) occurrence in `prerender.js`.
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: Rich-Harris <hello@rich-harris.dev>
…called once per prerendered path inside `visit()`, re-running handler setup for every page and violating the "setup runs once" contract.
This commit fixes the issue reported at packages/kit/src/core/postbuild/prerender.js:308
## Bug
`SSRHandler` is typed as:
```ts
export type SSRHandler = (server: {
respond: (request: Request, options?: Pick<RequestOptions, 'platform'>) => Promise<Response>;
}) => MaybePromise<(request: Request) => MaybePromise<Response>>;
```
The **outer** function is the one-time setup phase (this is where an adapter's `customHandler` opens a DB pool, spawns a worker, registers listeners, etc.), and it returns the **inner** per-request handler.
In `packages/kit/src/core/postbuild/prerender.js`, the setup call
```js
const respond = await init_server({ respond: (request, options) => { ... } });
```
was placed **inside `visit()`**, which runs once per prerendered path (via the concurrency queue `q`). This means the handler's setup phase re-runs for every prerendered route. An adapter `customHandler` that allocates resources in setup would leak one set of resources per prerendered page during the build.
`git log -p` confirms this PR introduced the regression: the previous code called `server.respond` directly inline (with `custom_respond` initialized once, outside the loop), whereas the new code moved `init_server(...)` into `visit()`. This is the same class of bug as the confirmed `preview-per-request-init-server` suggestion, but occurring at build time in the prerender loop.
## Fix
Hoisted the `init_server({...})` call to run **once**, right after `server.init(...)` and before the prerender queue starts. Since the `respond` closure needs per-visit state — the `dependencies` map created fresh in each `visit()` call — I threaded that state through a `WeakMap<Request, Map<...>>` (`dependencies_by_request`) keyed on the web `Request` object that flows into `server.respond`.
* `remote_responses`, `saved`, `emulator`, `config`, `out` are all shared/stable state, so they remain captured directly.
* Only `dependencies` was per-visit; in `visit()` we now register `dependencies_by_request.set(request, dependencies)` before calling the shared `respond(request)`, and the hoisted closure looks it up via `dependencies_by_request.get(request) ?? new Map()`.
`respond` is defined in the enclosing function scope after `server.init` but before the `enqueue(...)` loops that trigger `visit()` via the queue, so it is always assigned before use. The full `tsc` type check passes with no errors.
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: Rich-Harris <hello@rich-harris.dev>
This was referenced Jul 30, 2026
Rich-Harris
pushed a commit
that referenced
this pull request
Jul 30, 2026
Merged
6 tasks
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
split off from #15574
EDIT: This PR adds a
customHandlerproperty which adapters can resolve to a module that runsserver.initandserver.respondwith additional logic. The handler runs during dev, build, prerender, and preview. It's main purpose is to set us up to unify thedevandpreviewexperiences.Please don't delete this checklist! Before submitting the PR, please make sure you do the following:
Tests
pnpm testand lint the project withpnpm lintandpnpm checkChangesets
pnpm changesetand following the prompts. Changesets that add features should beminorand those that fix bugs should bepatch. Please prefix changeset messages withfeat:,fix:, orchore:.Edits