# 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 kind 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.

</tip>

### `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`.

</important>

### `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.

</note>

### `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">



</read-more>

### `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.

</note>

### `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 `.

</tip>

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

<read-more to="/guide/tls" title="TLS & mutual TLS">



</read-more>

### `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.

</note>

**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).

</note>

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

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

</note>

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

</read-more>

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

</read-more>

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

</read-more>

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

<table>
<thead>
  <tr>
    <th>
      Option / feature
    </th>
    
    <th>
      Behavior
    </th>
  </tr>
</thead>

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