-
Notifications
You must be signed in to change notification settings - Fork 0
TLS and Listener Limits
Resolution order, per router:
-
ListenSSL == false→ plain HTTP, whatever else is set. The router logs a warning if certificates are configured but unused. -
TLSConfig != nil→ used verbatim.CertFile/KeyFileare ignored. -
CertFileandKeyFileboth set → TLS from those files. - Anything else with
ListenSSLenabled is an error — not a silent downgrade to plaintext.
certFile, keyFile := "cert.pem", "key.pem"
app := rex.New(rex.WithConfig(&rex.Config{
DefaultRouter: rex.RouterConfig{
Addr: ":443",
ListenSSL: true,
CertFile: &certFile,
KeyFile: &keyFile,
},
}))TLSConfig is the injection point for everything the two file paths cannot
express.
Certificate rotation without a restart — set GetCertificate, and the
listener calls it once per handshake instead of reading a file once at start:
TLSConfig: &tls.Config{
GetCertificate: certManager.GetCertificate,
MinVersion: tls.VersionTLS12,
}Client certificate verification (mTLS) — this is the only place it is configured:
TLSConfig: &tls.Config{
Certificates: []tls.Certificate{serverCert},
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: caPool,
}
SSLVerifywas removed, not deprecated. It configured nothing — the value was stored on the router and never read, and there was no code path that could have used it. The name invited the mistake more than once:InsecureSkipVerifyis read bytls.Client, nevertls.Server, so a listener has no such setting. What people reached for it expecting isClientAuthplusClientCAs, above. That always worked;SSLVerifywas a second door onto the same room, and it did not open.
net/http applies no timeouts and no body limit of its own. Left unset,
every one of these is effectively unlimited: a 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.
rex fills them in. Zero takes the default; negative disables.
| Field | Default | Bounds |
|---|---|---|
ReadHeaderTimeout |
10s | reading request headers |
ReadTimeout |
30s | reading the whole request |
WriteTimeout |
0 — unset | writing the response (absolute) |
IdleTimeout |
120s | an unused keep-alive connection |
MaxHeaderBytes |
1 MiB | total header size |
MaxHeaderValueCount |
0 → net/http's 500 | number of header values |
MaxBodyBytes |
4 MiB | request body size |
Slowloris works by trickling headers so the request never completes and the connection is never released. This is the field that stops it, and it is the reason the framework sets a default rather than leaving the decision to you.
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, 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.
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 is set, so leaving it zero is already safe — the
field exists so the limit is visible and adjustable alongside the others rather
than being the one bound nothing in the configuration mentions.
The router enforces the cap two ways, because one is not enough:
- A declared
Content-Lengthover the cap is rejected without reading a byte — a clean413carryingmax_bytes, so a client that knows the limit can chunk or compress rather than guess. -
http.MaxBytesReadercovers chunked bodies and clients that under-declare. The read fails once the cap is passed, so a handler decoding the body gets an error rather than buffering whatever the client cared to send.
A route overrides its router's limit by implementing BodyLimitedRoute:
func (r *UploadRoute) MaxBodyBytes() int64 { return 64 << 20 } // 64 MiB
func (r *IngestRoute) MaxBodyBytes() int64 { return -1 } // streaming, no capThe value is read once, when the route table is built, not per request.
// Public API.
rex.RouterConfig{
Addr: ":443",
ListenSSL: true,
TLSConfig: tlsCfg,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 20 * time.Second,
IdleTimeout: 60 * time.Second,
MaxBodyBytes: 1 << 20,
}
// Operational listener: no bodies worth speaking of, internal only.
rex.RouterConfig{
Addr: "127.0.0.1:9091",
ReadTimeout: 5 * time.Second,
MaxBodyBytes: 4 << 10,
}
// Upload listener: long reads, big bodies, and no WriteTimeout.
rex.RouterConfig{
Addr: ":8081",
ReadTimeout: 10 * time.Minute,
MaxBodyBytes: 512 << 20,
}rex — Restful Extended eXperience · MIT · © 2026 Kryovyx · pre-1.0 (alpha), interfaces may change
Getting started
Core
Operating
Ecosystem