Configuration
HttpServerOptions is the whole surface. Defaults are chosen to be safe on a phone, not maximal on
a server.
var builder = HttpServer.CreateBuilder();
builder.Configure(o =>{ o.Address = IPAddress.Any; o.Port = 8080; o.Limits.MaxRequestBodySize = 200 * 1024 * 1024;});The single-endpoint shorthand
Section titled “The single-endpoint shorthand”| Property | Default | Notes |
|---|---|---|
Address |
IPAddress.Loopback |
An embedded server should not be LAN-reachable unless its author says so |
Port |
5000 |
0 lets the OS pick one — read it back from ListenUrl |
Https |
null |
Plain HTTP. See TLS & Certificates |
These three describe one endpoint and are what most embedded servers want.
Several endpoints at once
Section titled “Several endpoints at once”Options.Endpoints holds any number of address/port/TLS combinations, each bound by its own listener
and accept loop. TLS is per endpoint rather than per server because that is how it is actually used
— cleartext to the device, TLS to the network:
builder.Configure(o =>{ o.Listen(IPAddress.Loopback, 5000); o.ListenHttps(IPAddress.Any, 5001, certificate);});ListenUrls reports them all and ListenUrl the first. A partial bind failure unwinds every
endpoint that did bind rather than leaving the server half listening, and RestartAsync re-reads the
list.
Limits
Section titled “Limits”Options.Limits exists to keep a misbehaving or hostile client from exhausting memory.
| Property | Default |
|---|---|
MaxRequestLineSize |
8 KB |
MaxRequestHeadersTotalSize |
32 KB |
MaxRequestHeaderCount |
100 |
MaxRequestBodySize |
30 MB (null removes the limit) |
KeepAliveTimeout |
130 seconds |
RequestHeadersTimeout |
30 seconds |
MaxRequestsPerConnection |
1000 (null removes the limit) |
InputBufferSize |
16 KB |
A body larger than MaxRequestBodySize is a 413, not a truncated read. Raise it for an upload
endpoint, or raise it globally and bound the individual endpoint instead — see
Uploads & Downloads.
Connections
Section titled “Connections”| Property | Default | Notes |
|---|---|---|
MaxConcurrentConnections |
256 | Excess connections wait rather than being rejected. null removes the cap |
Backlog |
128 | Pending-connection queue depth handed to listen() |
NoDelay |
true |
Nagle off — request/response latency beats packet efficiency |
The connection cap is for the whole server, not per endpoint, so keep it comfortably above the number of endpoints. A cap smaller than that leaves some of them unable to accept at all until a connection elsewhere finishes.
Network changes
Section titled “Network changes”| Property | Default | Notes |
|---|---|---|
RebindOnNetworkChange |
false |
Restarts the listeners when the machine’s addresses change |
NetworkChangeDebounce |
2 seconds | One transition raises several events; this waits for them to settle |
Off by default, because a restart drops in-flight requests and a server on a fixed machine has nothing to gain. It is for a device that moves: a listener bound to the Wi-Fi address it had at startup is dead the moment the phone joins another network, and the socket stays open on an address that no longer exists without anything failing loudly.
Binding IPAddress.Any does not need this — that socket keeps working across an address change.
Binding a specific address does.
HttpServer.NetworkAddressesChanged fires either way, so a QR code or an
mDNS advertisement can be refreshed without turning rebinding on. On a MAUI
app, AddHttpServerLifecycle does the same thing from Shiny’s connectivity,
which is more reliable on a phone than the framework’s own event.
Resilience
Section titled “Resilience”| Property | Default | Notes |
|---|---|---|
StartRetryAttempts |
5 | Attempts at a start the server initiated. A start the app asked for is never retried |
StartRetryDelay |
1 second | Before the first retry; doubles per attempt |
StartRetryMaxDelay |
10 seconds | Ceiling for the doubling |
AcceptRetryAttempts |
5 | Consecutive accept failures absorbed before the listener is declared dead. Resets on every connection accepted |
AcceptRetryDelay |
100 ms | Before the first retry; doubles per consecutive failure |
AcceptRetryMaxDelay |
5 seconds | Ceiling for the doubling |
RecoverFromListenerFaults |
true |
Rebinds when the accept loop ends while the server believed it was running |
These are on by default, which is unusual for this library and deliberate. A server embedded in an app has nobody watching it: the failures they cover happen on a device in someone’s pocket, hours after the last line of app code ran, and an app that has to opt in to not-silently-dying will not have opted in.
The retried starts are the ones with nobody to throw to — the second half of a
RestartAsync, a rebind after the addresses moved, a recovery from
a dead listener. StartAsync is left alone on purpose: its caller is usually a toggle, it gets the
exception, and retrying behind it only makes the button appear stuck. Set StartRetryAttempts = 1 to
have the first failure be the last everywhere.
The accept loop counts consecutive failures rather than classifying them. Deciding transient-versus-fatal from a socket error code is a losing game across the platforms this runs on — descriptor exhaustion, an interface torn down mid-accept and a client that vanished all report differently on Android, iOS and desktop, and the list moves with the OS. Time is the honest classifier: what clears within a few attempts was transient, and what does not is fatal whatever its code claimed.
Nothing here is silent. Every retry logs a warning, every give-up logs an error, and the state transition carries the reason and the exception — see why the server stopped.
Response headers
Section titled “Response headers”| Property | Default | Notes |
|---|---|---|
ServerHeader |
"Shiny" |
null omits the header entirely |
IncludeDateHeader |
true |
RFC 9110 asks origin servers to send Date |
Forwarded headers
Section titled “Forwarded headers”o.UseForwardedHeaders = true;With this on, Request.Scheme and the client IP come from X-Forwarded-Proto / X-Forwarded-For.
ctx.GetClientIpAddress(useForwardedHeaders: true) does the same lookup for a single call site,
taking the left-most entry (the original client) and stripping an optional port.
Exception detail
Section titled “Exception detail”o.HideExceptionDetails = false; // development onlyOn by default: an unhandled handler exception produces a 500 with no detail. Turning it off returns
the exception text instead, which is a development convenience and a production disclosure. For
anything structured, use Errors & Problem Details — IncludeExceptionDetails
there does the same job for RFC 9457 bodies.
HTTP/2
Section titled “HTTP/2”Options.Http2 configures the HTTP/2 stack; see Protocols for what is
negotiated and how. The short version is that Enabled is on by default and costs nothing, because
the protocol is still chosen per connection: ALPN over TLS, the connection preface over cleartext,
and HTTP/1.1 for anything else.


