Skip to content

Router Configuration

wiki edited this page Sep 4, 2026 · 1 revision

Router configuration

type RouterConfig struct {
	Addr      string
	BaseURL   string
	ListenSSL bool
	CertFile  *string
	KeyFile   *string
	TLSConfig *tls.Config

	ReadHeaderTimeout   time.Duration
	ReadTimeout         time.Duration
	WriteTimeout        time.Duration
	IdleTimeout         time.Duration
	MaxHeaderBytes      int
	MaxHeaderValueCount int
	MaxBodyBytes        int64
}

One of these configures one listener. The application's default router takes Config.DefaultRouter; an extension creating its own listener passes one to CreateRouter.

Address and base path

Field Default Notes
Addr ":8080" any net.Listen address
BaseURL "/" prefix for every route on this router

BaseURL is normalised before use — no trailing slash, "" for the root — and that normalised form is what appears in RouteInfo.BaseURL and RouterRouteRegisteredEvent.BaseURL.

TLS

TLS is opt-in. Resolution order:

  1. ListenSSL == false → plain HTTP, whatever else is set.
  2. TLSConfig != nil → used verbatim; CertFile/KeyFile are ignored.
  3. CertFile and KeyFile both set → TLS from those files.
  4. Anything else with ListenSSL enabled is an error, not a silent downgrade to plaintext.
cfg := rextension.RouterConfig{
	Addr:      ":443",
	ListenSSL: true,
	TLSConfig: &tls.Config{
		GetCertificate: certManager.GetCertificate, // per-handshake
		ClientAuth:     tls.RequireAndVerifyClientCert,
		ClientCAs:      pool,
	},
}

TLSConfig is the injection point for everything the two file paths cannot express. Setting GetCertificate makes the certificate a per-handshake decision, which is what allows a certificate to be replaced without restarting the process. Client certificate verification (mTLS) is ClientAuth plus ClientCAs — there is nowhere else to configure it.

SSLVerify is gone, removed rather than deprecated. It configured nothing: the value was stored on the router and never read. The name invited the mistake more than once — InsecureSkipVerify is read by tls.Client, never tls.Server, so a listener has no such setting. What people reached for it expecting is TLSConfig.ClientAuth, above.

Listener limits

net/http applies no timeouts and no body limit of its own. Left unset, every one of these is effectively unlimited: a single client holding a connection open without completing its request headers occupies a goroutine for as long as it cares to, and a request body is read until the client stops sending.

Zero takes the default; negative disables.

Field Default What it bounds
ReadHeaderTimeout 10s time to read request headers — the Slowloris bound
ReadTimeout 30s time to read the whole request, headers and body
WriteTimeout 0 — unset, deliberately absolute deadline on the response
IdleTimeout 120s how long a keep-alive connection may sit unused
MaxHeaderBytes 1 MiB total size of request headers
MaxHeaderValueCount 0 → net/http's own 500 number of header values
MaxBodyBytes 4 MiB request body size

ReadHeaderTimeout is the one that matters most

Slowloris works by trickling headers so the request never completes and the connection is never released. ReadHeaderTimeout is the field that closes it.

WriteTimeout is left unset on purpose

No framework default fills it in. WriteTimeout is an absolute deadline on the whole response, not an idle timeout, so any non-zero value truncates responses that are legitimately long-lived: server-sent events, long polling, large downloads, and any slow client on a fast endpoint.

Slowloris is a read attack and is already closed by ReadHeaderTimeout, so setting this buys no protection that is not already in place. Set it only on a router you know serves nothing streaming.

MaxHeaderValueCount catches what a byte cap does not

A few thousand tiny headers stay well inside 1 MiB while still forcing the server to allocate and hash every one of them. net/http applies its own limit of 500 whether or not this field is set, so leaving it zero is already safe — it exists so the limit is visible and adjustable alongside the other six rather than being the one bound nothing in the configuration mentions.

Per-route body limits

MaxBodyBytes is the router's default; a route overrides it by implementing BodyLimitedRoute:

func (r *UploadRoute) MaxBodyBytes() int64 { return 64 << 20 } // 64 MiB
func (r *IngestRoute) MaxBodyBytes() int64 { return -1 }       // no limit

Negative on the RouterConfig disables the cap for the whole router.

Constants

const (
	DefaultReadHeaderTimeout   = 10 * time.Second
	DefaultReadTimeout         = 30 * time.Second
	DefaultIdleTimeout         = 120 * time.Second
	DefaultMaxHeaderBytes      = 1 << 20 // 1 MiB
	DefaultMaxBodyBytes        = 4 << 20 // 4 MiB
	DefaultMaxHeaderValueCount = 500     // mirrors net/http; not applied by the framework
)

WriteTimeout has no constant, because it has no default.

An operational listener

Extensions that create their own router usually want different limits than the public one — a metrics or health endpoint accepts no body worth speaking of:

err := r.CreateRouter("health", rextension.RouterConfig{
	Addr:         ":9091",
	BaseURL:      "/",
	ReadTimeout:  5 * time.Second,
	MaxBodyBytes: 4 << 10, // 4 KiB
})
if err != nil && !errors.Is(err, rextension.ErrRouterExists) {
	return err
}

Clone this wiki locally