Skip to content
Shiny Controls v1.0 - The Ultra Control Suite for .NET MAUI & BlazorO...M...G!

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

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.

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.

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.

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.

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.

Property Default Notes
ServerHeader "Shiny" null omits the header entirely
IncludeDateHeader true RFC 9110 asks origin servers to send Date
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.

o.HideExceptionDetails = false; // development only

On 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 DetailsIncludeExceptionDetails there does the same job for RFC 9457 bodies.

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.