* caddyhttp: let handler read deadlines cap the idle read timeout
IdleTimeoutReader sets the read deadline before every body read, so a
deadline set by a handler with http.ResponseController.SetReadDeadline()
was replaced before it could take effect. Since #7913, handlers can't
bound a stalled request body more tightly than read_body.
A handler's read deadline is now routed to the IdleTimeoutReader, which
applies it right away and keeps it as a ceiling for every idle reset and
for the deadline put back between HTTP/2 reads. A handler can make the
idle timeout stricter but can't loosen it, and a zero deadline removes
the ceiling, as #8145 does for write deadlines.
* caddyhttp: keep the hard read deadline and the drain deadline with handler deadlines
SetReadDeadline installed the handler's deadline as is, so a later or zero
deadline lifted read_timeout. It now installs the earlier of the two.
HandlerDone cleared a deadline set by the handler before arming the HTTP/1
drain deadline, leaving the post-handler drain unbounded.
* caddyhttp: keep the idle deadline of a read in flight on handler deadlines
SetReadDeadline replaced the idle deadline armed for a blocked read with the
handler's ceiling, so a zero or later deadline lifted the idle timeout for
that read. It now installs the earlier of the two until the read returns.
* caddyhttp: keep the expired drain deadline of a timed-out body on handler deadlines
After a body read timed out on HTTP/1, a zero or later deadline set by the
handler replaced the expired one kept to bound net/http's post-handler drain.
(cherry picked from commit a28f061462)
read_idle_timeout and write_idle_timeout default to 1 minute when zero,
and the wrappers are only installed for positive values, so a negative
value already disables them. Document it and cover it with tests, since
it is the only way to opt out, for instance behind a reverse proxy that
already protects from slow clients.
(cherry picked from commit c65c248336)
* caddyhttp: only bound HTTP/2 body reads in flight with the idle timeout
On HTTP/2, the read deadline is a timer that fails the request body
when it fires, even with no read in flight. IdleTimeoutReader left the
idle deadline armed between reads, so a handler that stopped reading
for longer than read_body_idle (1 minute by default) lost the body,
although the client was only held back by flow control. For example,
reverse_proxy uploading to an upstream that's slow to accept the body
failed with a 502.
This is the read side of #8118. Like IdleTimeoutWriter does since
read on HTTP/2, so the idle deadline only bounds reads in flight. A
client that stops sending is still cut off, since the deadline is set
before each read. HTTP/1 and HTTP/3 only check the deadline during a
read, so they're unchanged.
Written with Claude Code (Claude Opus 5.5); the human submitter is
responsible for reviewing it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* caddyhttp: track a released hard read deadline as armed
From review: with HardDeadline set, releasing the idle deadline between
reads left an HTTP/2 timer armed (at HardDeadline) while deadlineSet
recorded that none was, so clearDeadlineLocked would skip clearing it.
Track which deadline is armed (none, idle, or only the hard one) instead
of a bool, and have each check ask the question it means: re-arming and
releasing only apply to an armed idle deadline, while clearing applies
to any.
The writer's deadlineSet already only means "idle deadline armed",
since nothing clears the hard deadline it leaves; document that.
Written with Claude Code (Claude Opus 5.5); the human submitter is
responsible for reviewing it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* caddyhttp: clear released hard deadline at handler end, skip no-op sets
From review:
- After a partial read releases the idle deadline to HardDeadline, a
handler that returns without reading again left that timer armed past
HandlerDone, which only cleared after a terminal error. HandlerDone
now also clears a released hard deadline.
- When the idle deadline is capped to HardDeadline, the reader and
writer set the same deadline again when releasing it, and the writer
once more in HandlerDone. On HTTP/2 each is a message to the serve
loop. Both now remember the deadline they armed and skip setting it
again. Within a request, only these wrappers set the client's
deadlines.
Replaces the earlier test that faked a terminal error with the partial
read followed directly by HandlerDone.
Written with Claude Code (Claude Opus 5.5); the human submitter is
responsible for reviewing it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* caddyhttp: only skip re-setting an armed hard deadline
Skipping any deadline equal to the one already armed also skipped idle
deadlines on Windows, where two calls to time.Now in the same clock tick
return the same time, so successive idle deadlines can be equal. That
broke tests counting deadline calls there. Only skip HardDeadline, the
case from review where the idle deadline is capped to it; other
deadlines are always set.
Written with Claude Code (Claude Opus 5.5); the human submitter is
responsible for reviewing it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* caddyhttp: restore deadlines without trusting cached transport state
Always set the requested read and write deadlines because another handler can change them through its own response controller. Remove the armedAt shortcut and cover intervening deadline changes for reads, writes, ReadFrom, flushes, and HandlerDone.
AI-assisted with OpenAI Codex (GPT-6); tested under human direction.
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 30d3bd2a89)
* timeouts: take precedence over server-wide and enclosing idle timeouts
The timeouts handler wrapped the request body and response writer again,
on top of the server's own idle timeout wrappers. The wrapper closest to
the connection sets the deadline last, so the server-wide read_body_idle
and write_idle (on by default since 2.11.6) silently overrode the
route's settings. The same happened between nested timeouts handlers,
where the outer, less specific one won.
The server now makes its idle timeout wrappers available through the
request context, and the timeouts handler overrides their settings while
it runs, restoring them when it returns. It only wraps when there's
nothing to override, and then registers its own wrappers so that nested
handlers override those. The server-wide read_timeout and write_timeout
still cap the whole transfer. A handler that doesn't set
max_write_chunk keeps the server-wide one.
Written with Claude Code (Claude Opus 5.5); the human submitter is
responsible for reviewing it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* timeouts: re-arm deadlines on override and suspend outer min-rate window
From review:
- On HTTP/2, a read or write deadline is a timer that fires even with
no read or write in flight. A deadline armed before a timeouts handler
started kept the outer timeout and could fail the body (or reset the
stream) inside a longer inner scope before the next read reset it.
Override and restore now move an armed deadline to the settings that
apply.
- Time spent under an override counted against the outer MinRate
window, so the restored deadline could already have expired. The
outer window is now suspended while overridden: its start moves
forward by the time spent overridden, and bytes transferred in the
meantime don't count towards it.
Adds an HTTP/2 read-before-inner-scope regression test and nested
minimum-rate coverage.
Written with Claude Code (Claude Opus 5.5); the human submitter is
responsible for reviewing it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 8f549e6df1)
* caddyhttp: mitigate slowloris via idle read/write deadlines
ReadTimeout and WriteTimeout previously applied as a single hard
deadline over the whole body/response through http.Server, so any
non-zero value also killed large transfers from legitimately slow
clients. Reset the deadline on every successful read/write instead
(via http.ResponseController), and give both a sane 1m default now
that doing so no longer penalizes slow-but-progressing clients.
* caddyhttp: split idle read/write timeouts from the existing hard ones
Reworking ReadTimeout/WriteTimeout's own semantics was an unwanted
behavior change for existing configs relying on the hard deadline.
Leave them untouched and add ReadIdleTimeout/WriteIdleTimeout instead,
reset on every successful read/write; both default to 1m since,
being new, no existing config could have depended on a different
value. Combining an idle timeout with its hard counterpart now gives
the same base+ceiling shape as Apache's mod_reqtimeout, for free.
* caddyhttp: cap idle-reset deadlines at the hard timeout ceiling
Deadlines are a single absolute value on the connection, not a min of
several: ReadTimeout/WriteTimeout's own hard deadline, set once by
net/http before the handler runs, was silently getting overwritten by
the first idle-reset Read/Write, voiding it entirely. Clamp the
idle-reset deadline to the hard one when both are set, so combining
them actually behaves like the advertised base+ceiling.
* caddyhttp: add ReadMinRate/WriteMinRate, Apache MinRate equivalent
Pure idle-reset alone doesn't bound a trickle that sends just enough
to never go idle. ReadMinRate/WriteMinRate (bytes/second) grow the
allowed deadline from a fixed start based on bytes transferred so far
instead of resetting to a flat window on every call, so a transfer
that doesn't sustain the configured rate falls behind real time and
gets cut, matching Apache mod_reqtimeout's MinRate. Zero (default)
keeps the existing flat idle-reset behavior unchanged.
* caddyhttp: use named return and consistent blank lines in idleDeadline
Matches the named-return style already used by ResponseWriterWrapper.ReadFrom.
* caddyhttp: chunk idleTimeoutWriter's Write/ReadFrom, cap at 64 KiB
SetWriteDeadline bounds the whole call it precedes, not just a stall
within it. net.Conn.Write loops internally until a buffer is fully
sent (unlike Read, which returns after one syscall), and
ResponseWriter.ReadFrom hands the entire remaining source to the
connection in one call. A single large Write, or any body copied via
io.Copy triggering the ReadFrom fast path (http.ServeContent, static
file serving), had its whole transfer bounded by one deadline,
silently truncating a slow-but-healthy transfer exactly like a hard
WriteTimeout would - the same bug found and fixed the same way in
FrankenPHP's go_ub_write (php/frankenphp#2574).
Cap each underlying call at 64 KiB and reset the deadline between
chunks instead. net/sendfile.go special-cases *io.LimitedReader, so
chunking ReadFrom still uses the sendfile fast path per chunk.
* caddyhttp: export idle-timeout types, add configurable MaxWriteChunk
Export IdleTimeoutReader/IdleTimeoutWriter/IdleDeadline so other
packages (request_body next) can reuse the same idle-reset mechanism
instead of reimplementing it, and turn the hardcoded 64 KiB write
chunk size into a configurable MaxWriteChunk field defaulting to the
same value - nginx's sendfile_max_chunk exists for the identical
reason and is admin-tunable rather than fixed.
* requestbody: idle-reset ReadTimeout/WriteTimeout, add MinRate/MaxWriteChunk
ReadTimeout/WriteTimeout set a single deadline once, so any transfer
running longer than the timeout got cut regardless of whether it was
actually stalled - the same bug the server-wide timeouts had before
switching to idle-reset. Reuse caddyhttp.IdleTimeoutReader/Writer here
too, giving per-route granularity nginx/Apache have via location/
directory scoping and Caddy's server-wide timeouts don't: a route
matching this handler can now set its own idle window independently
from the rest of the server block.
* caddyhttp: fold read/write min_rate into the idle-timeout directive
Two directives per rate (read_body_idle + read_body_min_rate) for a
value that's meaningless without the other. Fold min_rate into the
idle-timeout directive as an optional second argument instead.
* caddyhttp: split write pacing out of request_body into new timeouts handler
request_body is a request-body concern (max_size, set); ReadTimeout/
WriteTimeout/MinRate/MaxWriteChunk pace both directions, and write
pacing has nothing to do with the request body. Move all of it to a
dedicated http.handlers.timeouts module instead, mirroring the
server-wide timeouts option one level down.
* caddyhttp: shield specific hostnames from a covering wildcard's client auth (@sillyzir)
A connection policy for a wildcard hostname (e.g. *.example.com with
client_auth) is first-match by SNI, so it also applied client
authentication to more specific hostnames served by their own site
blocks (public.example.com) — sites that never asked for mTLS.
Two cases produce the shielding empty policy that fixes this:
- site blocks whose TLS config yields a connection policy with no
settings (previously discarded as having no effect);
- site blocks with no TLS connection policy at all — the reported
case — for which an empty policy is now synthesized.
Either way the empty policy is hoisted directly above the first
client-auth-bearing policy whose wildcard SNI covers the hostname, so
first-match shields it from the client-auth requirement.
Deliberately scoped to client authentication: other wildcard policy
settings, such as certificate selection, are ones a covered hostname
generally WANTS to inherit (see tls_automation_wildcard_shadowing);
sni matchers that fail to decode emit an adapt warning instead of
being silently skipped.
Fixes#7860
* caddyhttp: shield preserves the wildcard policy's other settings (@sillyzir)
The hoisted shield was an empty policy, and connection policies are
first-match: it lifted the client-auth requirement but also suppressed
every other setting the covering wildcard policy carried (certificate
selection, protocol bounds, ALPN). Hoist a copy of the covering policy
with only client_authentication removed instead, so the shielded
hostname keeps inheriting the rest.
The new adapt test gives the wildcard policy protocols and alpn in
addition to client_auth and asserts the shield carries both while
dropping only client authentication; the existing test (client_auth
only) is unchanged, which is exactly why it could not catch this.
* caddyhttp: shield each covering wildcard policy separately (@sillyzir)
The hoisting loop stopped at the first client-auth policy covering any of a
site block's hostnames and gave the shield that block's whole SNI matcher, so
hostnames covered by a *different* wildcard matched it too. Because connection
policies are first-match, those hostnames then took the wrong policy's
settings and lost their own client-authentication requirement entirely.
Map each hostname to the first covering policy individually and hoist one
shield per covering policy, matching only the hostnames it covers. The
existing policies' wildcard SNI names are decoded once up front, and the
shields are inserted back to front so an insertion cannot shift the index of a
covering policy still to be shielded.
* chore: fumpt (@steadytao)
* chore: fix master lint (@steadytao)
---------
Co-authored-by: SillyZir <269283839+SillyZir@users.noreply.github.com>
Co-authored-by: Zen Dodd <mail@steadytao.com>
Caddy's underscore header filter (GHSA-f59h-q822-g45g) only checked
for `_`. PHP folds `.` to `_` when registering $_SERVER keys the same
way CGI/FastCGI folds `-` to `_`, so a dotted alias (e.g. Remote.User)
survived the filter and collided with the legitimate hyphenated
header once it reached a PHP/FastCGI backend, bypassing forward_auth
copy_headers the same way the underscore alias did.
Extends the filter to drop `.` symmetrically, adds an
`expected_dot_headers` allowlist mirroring `expected_underscore_headers`,
and handles header names containing both separators (only an exact
allowlist entry is honored there, since a prefix glob's free-form
suffix can't be vetted for an embedded second separator).
Root cause identified by @iliaal in the FrankenPHP advisory
GHSA-49wc-4hcv-v58q.
quic-go defaults the `InitialPacketSize` to 1280 bytes. This is used for
the size of the payload plus UDP header. When quic-go creates its
initial handshake packet, it pads it to this size as an optimization
for the anti-amplification limit (which does not really apply to
Caddy's server connections).
Tailscale uses an MTU of 1280 (the minimum IPv6 MTU) because it is a
tunnel operating in unknown environments, possibly inside other
tunnels.
When wrapped in an IP header (either v4 or v6), the 1280 byte QUIC packet
constructed by quic-go exceeds the Tailscale MTU and gets dropped. This
makes it impossible to respond to an HTTP3 connection attempt over
Tailscale when using the default `InitialPacketSize`.
We explicitly set the `InitialPacketSize` to 1200 (its minimum) to support
HTTP3 connections over Tailscale and other low MTU connections.
Once the connection is established, quic-go can perform MTU discovery to
increase the packet size to the maximum supported by the connection, so
any throughput loss due to the lowered `InitialPacketSize` is
short-lived.
* caddyhttp: restore allow_underscore_in_headers server option (#7808)
* caddyhttp: mark insecure_allow_underscore_in_headers as EXPERIMENTAL
* caddyhttp: replace underscore bool with expected_underscore_headers allowlist
* fix gofmt alignment in serveroptions.go
* caddyhttp: drop repeated allowlisted underscore headers
* caddyhttp: add tests for repeated-value drop and variant-drop logging
* feat: drop headers with underscore in their names
* feat: Caddyfile binding and tests for underscore-in-header drop
Add the `allow_underscore_in_headers` global server option, refine the
doc comment, and cover the filter end-to-end: server-level unit tests
(drop, opt-out, debug log, RFC-7230 space rejection), a fastcgi unit
test for the trimmed header name replacer, and forward_auth integration
tests for both the default-drop and opt-out paths.
* remove allow_underscore_in_headers option for now
* tls: add alpn to managed HTTPS records
* tls: centralise HTTPS RR ALPN defaults and registration
Reuse shared protocol defaults instead of repeating the default HTTP protocol list, unify server name registration to carry ALPN in one experimental API and reuse the TLS default ALPN ordering for HTTPS RR publication
* http: centralise effective protocol resolution for HTTPS RR ALPN
* packet_conn_wrappers: Initial changes
* packet_conn_wrappers: Unwrap a packet conn only if there are no wrappers
---------
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
* use the new http.Protocols to handle h1, h2 and h2c requests
* fix lint
* keep ConnCtxKey for now
* fix handling for h2c
* check http version while reading the connection
* check if connection implements connectionStater when it should
* add comments about either h1 or h2 must be used in the listener
* fix if check
* return a net.Conn that implements connectionStater if applicable
* remove http/1.1 from alpn if h1 is disabled
* fix matching if only h1 is enabled
---------
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
This reverts commit 932dac157a.
Somehow the code I was looking at changed when I committed, without realizing it. This has already been fixed in #6777.
* metrics: move `metrics` up, outside `servers`
This change moves the metrics configuration from per-server level to a single config knob within the `http` app. Enabling `metrics` in any of the configured servers inside `http` enables metrics for all servers.
Fix#6604
Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>
* normalize domain name
---------
Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>
* caddy adapt for listen_protocols
* adapt listen_socket
* allow multiple listen sockets for port ranges and readd socket fd listen logic
* readd logic to start servers according to listener protocols
* gofmt
* adapt caddytest
* gosec
* fmt and rename listen to listenWithSocket
* fmt and rename listen to listenWithSocket
* more consistent error msg
* non unix listenReusableWithSocketFile
* remove unused func
* doc comment typo
* nonosec
* commit
* doc comments
* more doc comments
* comment was misleading, cardinality did not change
* addressesWithProtocols
* update test
* fd/ and fdgram/
* rm addr
* actually write...
* i guess we doin' "skip": now
* wrong var in placeholder
* wrong var in placeholder II
* update param name in comment
* dont save nil file pointers
* windows
* key -> parsedKey
* osx
* multiple default_bind with protocols
* check for h1 and h2 listener netw
* ci: update the linter action version
Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>
* exclude rule `G115`; disable deprecated linter
Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>
---------
Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>