
# Server Options

> Provide additional options to customize listening server.

When starting a new server, in addition to main `fetch` handler, you can provide additional options to customize listening server.

```js
import { serve } from "srvx";

serve({
  // Generic options
  port: 3000,
  hostname: "localhost",

  // Runtime specific options
  node: {},
  bun: {},
  deno: {},

  // Main server handler
  fetch: () => new Response("👋 Hello there!"),
});
```

There are two kinds of options:

- Generic options: Top level options are intended to have exactly same functionality regardless of runtime
- Runtime specific: Allow customizing more runtime specific options

## Generic Options

### `port`

The port server should be listening to.

Default is value of `PORT` environment variable or `3000`.

> [!TIP]
> You can set the port to `0` to use a random port.

### `hostname`

The hostname (IP or resolvable host) server listener should bound to.

Default is value of the `HOST` environment variable, if set. When neither `hostname` nor `HOST` is provided, the server will **listen to all network interfaces** by default.

> [!IMPORTANT]
> If you are running a server that should not be exposed to the network, use `localhost`.

### `reusePort`

Enabling this option allows multiple processes to bind to the same port, which is useful for load balancing.

> [!NOTE]
> Despite Node.js built-in behavior that has `exclusive` flag enabled by default, srvx uses non-exclusive mode for consistency.

### `silent`

If enabled, no server listening message will be printed (enabled by default when `TEST` environment variable is set).

### `manual`

If set to `true`, the server will **not** start listening automatically. You must call [`server.serve()`](/guide/server#serverserve) yourself to begin accepting connections. Useful when you need to fully construct the server before it starts.

```js
import { serve } from "srvx";

const server = serve({
  manual: true,
  fetch: () => new Response("👋 Hello there!"),
});

// ...later
await server.serve();
await server.ready();
```

### `middleware`

An array of [middleware](/guide/middleware) handlers to run before the main `fetch` handler.

```js
import { serve } from "srvx";

serve({
  middleware: [
    (request, next) => {
      console.log(`[${request.method}] ${request.url}`);
      return next();
    },
  ],
  fetch: () => new Response("👋 Hello there!"),
});
```

:read-more{to="/guide/middleware" title="Middleware"}

### `plugins`

An array of [plugins](/guide/middleware) to extend the server. A plugin is a **synchronous** function that receives the server instance and can register middleware, hook into the lifecycle, or augment requests.

```js
import { serve } from "srvx";

const myPlugin = (server) => {
  server.options.middleware.push((request, next) => next());
};

serve({
  plugins: [myPlugin],
  fetch: () => new Response("👋 Hello there!"),
});
```

> [!NOTE]
> Plugins are sync-only (they return `void`). All plugin-registered middleware is therefore in place before the first request is handled.

### `protocol`

The protocol to use for the server.

Possible values are `http` or `https`.

If `protocol` is not set, Server will use `http` as the default protocol or `https` if both `tls.cert` and `tls.key` options are provided.

### `tls`

TLS server options.

**Example:**

```js
import { serve } from "srvx";

serve({
  tls: { cert: "./server.crt", key: "./server.key" },
  fetch: () => new Response("👋 Hello there!"),
});
```

**Options:**

- `cert`: Path or inline content for the certificate in PEM format (required).
- `key`: Path or inline content for the private key in PEM format (required).
- `passphrase`: Passphrase for the private key (optional).

> [!TIP]
> You can pass inline `cert` and `key` values in PEM format starting with `-----BEGIN `.

Client certificates (mutual TLS) are available through the [`mtlsPlugin()` plugin](/guide/tls#mutual-tls-mtls).

:read-more{to="/guide/tls" title="TLS & mutual TLS"}

### `error`

Runtime agnostic error handler. It runs whenever the `fetch` handler (or any middleware) throws or rejects, and returns the `Response` to send instead.

> [!NOTE]
>
> This handler will also take over the built-in error handlers of Deno and Bun.

**Example:**

```js
import { serve } from "srvx";

serve({
  fetch: () => new Response("👋 Hello there!"),
  error(error) {
    return new Response(`<pre>${error}\n${error.stack}</pre>`, {
      headers: { "Content-Type": "text/html" },
    });
  },
});
```

### `maxRequestBodySize`

Maximum allowed size **in bytes** for the request body. Defaults to `undefined` (no limit).

As the body is read, its accumulated length is tracked and, once it exceeds the limit, reading is aborted and rejects with a `413`-style error. The error carries `statusCode: 413`, `status: 413` and `code: "ERR_BODY_TOO_LARGE"`, so a handler (or [`error`](#error)) can map it to an HTTP `413 Payload Too Large` response.

The limit covers both buffered reads (`request.text()` / `request.json()`) and the streamed body (`request.body`, and therefore `request.arrayBuffer()` / `.blob()` / `.bytes()` / `.formData()`).

**Example:**

```js
import { serve } from "srvx";

serve({
  maxRequestBodySize: 1024 * 1024, // 1 MiB
  fetch: async (request) => {
    try {
      return Response.json(await request.json());
    } catch (error) {
      if (error.code === "ERR_BODY_TOO_LARGE") {
        return new Response("Payload Too Large", { status: 413 });
      }
      throw error;
    }
  },
});
```

> [!NOTE]
> Runtime support:
>
> - **Node**: enforced by srvx (the request body stream is size-limited).
> - **Bun**: forwarded to Bun's native [`maxRequestBodySize`](https://bun.sh/docs/api/http), enforced by Bun (responds with `413` before the handler runs).
> - **Deno**: enforced by srvx (`Deno.serve` has no native option).

The underlying helpers are also exported from [`srvx/body-limit`](/guide/body-limit) for enforcing your own (e.g. per-handler) limits with the same streaming semantics and error shape.

### `trustProxy`

Whether to trust `X-Forwarded-*` headers (`X-Forwarded-Proto`, `X-Forwarded-Host`, `X-Forwarded-For`, and the HTTP/2 `:scheme`) when deriving `request.url` and `request.ip`. Defaults to `false`.

Any client can send `X-Forwarded-Proto: https`, `X-Forwarded-Host` or `X-Forwarded-For`, so trusting them lets a request masquerade as `https:`, forge its host, or spoof its client IP. Only enable this when a proxy you control sits in front.

- `false` (default): ignore the headers; use the real connection protocol, the on-the-wire `Host` header and the socket peer address.
- `true`: trust every hop (the leftmost `X-Forwarded-For` entry is the client).
- `"loopback"`: trust hops on a loopback address (`127.0.0.0/8` or `::1`), i.e. a proxy on the same host.
- `string[]`: trust hops whose address is in the allowlist.

#### Hop-aware resolution

Real proxies **append** to `X-Forwarded-*`, so the chain grows left-to-right (outermost client first, nearest proxy last). srvx resolves it **hop-aware, right-to-left**, matching Express (`trust proxy`), nginx (`$proxy_add_x_forwarded_for`) and AWS ALB:

- Starting from the immediate peer, each address in the trusted set is treated as a proxy you control.
- **The client is the rightmost address _not_ in the trusted set.**
- If every address in the chain is trusted, the leftmost entry is the client.
- If the immediate peer is not trusted, the headers are ignored entirely.

This is why a spoofed prefix cannot poison `request.ip`: with `trustProxy: "loopback"` and a request carrying `X-Forwarded-For: 9.9.9.9`, the trusted proxy appends the real client address, so the forged `9.9.9.9` is skipped rather than returned. The same model gates the forwarded protocol and host: only the value contributed by the trusted proxy is honored, so an attacker cannot poison `request.url` (e.g. cache keys or password-reset links).

#### Absolute-form request targets

An HTTP/1.1 client may send the request target in absolute-form (`GET http://example.com/path HTTP/1.1`), which [RFC 9112 §3.2.2](https://www.rfc-editor.org/rfc/rfc9112#name-absolute-form) requires origin servers to accept. srvx accepts it and honors its authority in place of `Host`, but rebuilds the URL so it cannot be used to route around the guarantee above:

- The scheme always comes from the transport (and, behind a trusted proxy, `X-Forwarded-Proto`) — a client can never make a plaintext request look like `https:`, or downgrade a TLS request to `http:`.
- The authority is validated exactly like a `Host` header, and replaced with `_invalid_` when it is malformed.
- Any userinfo (`http://user@host/`) is dropped.
- Targets that are not `http:`/`https:` (e.g. `file://`, `ftp://`) are answered with `400`.

So `new URL(request.url).protocol` always reflects the real connection, not the request line.

**Example:**

```js
import { serve } from "srvx";

serve({
  // Behind a reverse proxy you control (e.g. Nginx, a load balancer):
  trustProxy: true,
  fetch: (request) => new Response(new URL(request.url).protocol),
});
```

> [!NOTE]
> Applies to the Node, AWS Lambda, Bun and Deno adapters.

## Runtime Specific Options

### Node.js

**Example:**

```js
import { serve } from "srvx";

serve({
  node: {
    maxHeaderSize: 16384 * 2, // Double default
    ipv6Only: true, // Disable dual-stack support
    // http2: false // Disable http2 support (enabled by default in TLS mode)
  },
  fetch: () => new Response("👋 Hello there!"),
});
```

::read-more
See Node.js documentation for [ServerOptions](https://nodejs.org/api/http.html#httpcreateserveroptions-requestlistener) and [ListenOptions](https://nodejs.org/api/net.html#serverlistenoptions-callback) for all available options.
::

### Bun

**Example:**

```js
import { serve } from "srvx";

serve({
  bun: {
    error(error) {
      return new Response(`<pre>${error}\n${error.stack}</pre>`, {
        headers: { "Content-Type": "text/html" },
      });
    },
  },
  fetch: () => new Response("👋 Hello there!"),
});
```

::read-more{to=https://bun.sh/docs/api/http}
See Bun HTTP documentation for all available options.
::

### Deno

**Example:**

```js
import { serve } from "srvx";

serve({
  deno: {
    onError(error) {
      return new Response(`<pre>${error}\n${error.stack}</pre>`, {
        headers: { "Content-Type": "text/html" },
      });
    },
  },
  fetch: () => new Response("👋 Hello there!"),
});
```

::read-more{to=https://docs.deno.com/api/deno/~/Deno.ServeOptions}
See Deno serve documentation for all available options.
::

### Service Worker

Options for the service worker adapter (`srvx/service-worker`).

```js
import { serve } from "srvx/service-worker";

serve({
  serviceWorker: {
    url: "/sw.js", // path to the service worker file to register
    scope: "/", // service worker scope
  },
  fetch: () => new Response("👋 Hello there!"),
});
```

- `url`: The path to the service worker file to be registered.
- `scope`: The scope of the service worker.

## Per-runtime Support

srvx aims for identical behavior across runtimes, but a few options depend on capabilities the underlying runtime does not expose. The table below lists the intentional differences.

| Option / feature            | Behavior                                                                                                                                                                                                                      |
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `close(true)` (force close) | Honored on Node and Bun. On **Deno** the force argument is currently ignored — `close(true)` behaves like a graceful `close()`.                                                                                               |
| `trustProxy`                | Applies to the **Node, AWS Lambda, Bun and Deno** adapters only. Ignored on Cloudflare, Bunny and the generic/service-worker adapters.                                                                                        |
| `maxRequestBodySize`        | Node/Deno enforced by srvx (unlimited by default). **Bun** forwards it to Bun's native option, which has its own **128 MiB** default even when unset. Dropped (not applied) when running under the CLI loader on any runtime. |
| `manual`                    | Meaningless on module-worker runtimes (Cloudflare module syntax, Bunny) — there is no listening step to defer.                                                                                                                |
| `gracefulShutdown`          | Supported on Node, Deno and Bun. No-op on Cloudflare, Bunny and service-worker runtimes.                                                                                                                                      |
| Cloudflare `env` bindings   | `request.runtime.cloudflare.env` is only populated in **module-worker syntax**. In service-worker syntax (the global `fetch` listener) bindings are unavailable.                                                              |
| **`upgrade` / WebSocket**   | srvx does **not** handle HTTP `upgrade` requests by default. Use [crossws](https://crossws.h3.dev/guide) for WebSocket support.                                                                                               |
