Skip to content

feat: custom server entry points during development - #16464

Merged
Rich-Harris merged 129 commits into
version-3from
feat-server-entrypoint
Jul 29, 2026
Merged

feat: custom server entry points during development#16464
Rich-Harris merged 129 commits into
version-3from
feat-server-entrypoint

Conversation

@teemingc

@teemingc teemingc commented Jul 21, 2026

Copy link
Copy Markdown
Member

split off from #15574

EDIT: This PR adds a customHandler property which adapters can resolve to a module that runs server.init and server.respond with additional logic. The handler runs during dev, build, prerender, and preview. It's main purpose is to set us up to unify the dev and preview experiences.


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
  • 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

  • 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.

@pkg-svelte-dev

pkg-svelte-dev Bot commented Jul 21, 2026

Copy link
Copy Markdown

Install the latest version of @sveltejs/kit from cf9539c:

pnpm add https://pkg.svelte.dev/@sveltejs/kit/c/cf9539cd725bc18f1c395b97d6aaa5f55d6bc976

Open in pkg.svelte.dev: https://pkg.svelte.dev/repos/kit/pr/16464

@changeset-bot

changeset-bot Bot commented Jul 21, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: cf9539c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@sveltejs/kit Minor

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

@svelte-docs-bot

Copy link
Copy Markdown

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 Rich-Harris left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

Comment thread documentation/docs/25-build-and-deploy/99-writing-adapters.md Outdated
Comment thread documentation/docs/25-build-and-deploy/99-writing-adapters.md Outdated
Comment thread documentation/docs/25-build-and-deploy/99-writing-adapters.md Outdated
Comment thread documentation/docs/25-build-and-deploy/99-writing-adapters.md Outdated
Comment thread documentation/docs/25-build-and-deploy/99-writing-adapters.md
teemingc and others added 10 commits July 29, 2026 20:59
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>
Comment thread documentation/docs/25-build-and-deploy/99-writing-adapters.md
Comment thread packages/kit/src/exports/vite/preview/index.js Outdated
Comment thread packages/kit/src/core/postbuild/prerender.js Outdated
vercel Bot and others added 3 commits July 29, 2026 22:09
…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>

@Rich-Harris Rich-Harris left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

fantastic work!

@Rich-Harris
Rich-Harris merged commit 036ccab into version-3 Jul 29, 2026
24 checks passed
@Rich-Harris
Rich-Harris deleted the feat-server-entrypoint branch July 29, 2026 23:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants