* requestbody: reject oversized Content-Length before the next handler
When max_size is set and the request declares a Content-Length above it, respond 413 right away instead of letting the next handler start and fail mid-read. Bodies of unknown length are still limited by MaxBytesReader. Both paths return the same error, so handle_errors sees no difference.
* caddytest: send unknown-length bodies in max_size placeholder tests
request_body now answers 413 for a declared Content-Length over max_size before the placeholder reads the body, so these tests no longer covered the read path and the direct respond one failed.
* requestbody: keep the body limited for error routes
Error routes get the same request, so wrap the body before the early 413 to stop them from reading past max_size.
* reverseproxy: Test that WebSockets + unix-sockets works
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Close temp file before reusing its path for the socket
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Strengthen unix socket WebSocket test, reuse tempSocketName helper
Check that upstream bytes sent with the 101 are delivered, echo a payload larger than any buffer, and verify the upstream connection closes when the client disconnects. Reuse the existing tempSocketName helper for all unix socket tests, which also closes the temp file descriptor the older tests leaked.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* httpcaddyfile: Don't merge automation policies if it would shadow another
Signed-off-by: Bartlomiej Wisniowski <56162793+bwisn@users.noreply.github.com>
* httpcaddyfile: add reverse case for shadowing merge, use force_automate to match the real life usage and explain why we're not checking the inverse
Signed-off-by: Bartlomiej Wisniowski <56162793+bwisn@users.noreply.github.com>
* httpcaddyfile: Check overlap in both directions when merging policies
Signed-off-by: Bartlomiej Wisniowski <56162793+bwisn@users.noreply.github.com>
---------
Signed-off-by: Bartlomiej Wisniowski <56162793+bwisn@users.noreply.github.com>
* headers: Check each ? default on its own and keep the response matcher
Several ? defaults in one header block shared one require, so a single
field already set on the response blocked all of them. A match in the
block was also dropped for the ? fields, so they were set on every
response.
* headers: Reject a response matcher on a field that also has a ? default
A ? default already requires its field to be absent, so a match on the
same field in that block could only be dropped or never be true. It is
now a Caddyfile error.
* headers: Test that the last ? default for a field wins
* headers: Allow an absence matcher on a field with a ? default
`match header !X-Test` asks for what `?X-Test` already requires, so it
loads again. Only a matcher that needs a value on that field is an error.
* fastcgi: derive SCRIPT_FILENAME from the normalized request path
Path matchers evaluate a normalized request path — MatchPath applies
normalizeWindowsPath and then CleanPath before comparing — but the FastCGI
transport split r.URL.Path verbatim. Caddy does not rewrite r.URL.Path itself,
so the two disagreed whenever the path contained dot segments after the split
token: /admin.php/../../other.php was matched as /other.php while
SCRIPT_FILENAME resolved to <root>/admin.php, and on Windows the same held for
/admin.php\..\other.php. A route gated by a path matcher could therefore execute
a script that matcher never evaluated.
php_fastcgi generates a rewrite route that normally normalizes the path first,
but that route can fail to match, so the transport establishes the invariant
itself rather than inheriting it. The reason it can fail to match is addressed
in the following commit; this change does not depend on it.
caddyhttp.NormalizedPath is added so a consumer acting on a path a matcher has
already judged derives it exactly as the matcher did, rather than reassembling
the two steps and risking drift.
Scope — two divergences of the same class are NOT addressed here:
* SCRIPT_NAME remains a prefix of the request path, so a matcher naming a
script exactly (path /admin.php) is still reachable via PATH_INFO at
/admin.php/anything. Matchers protecting a script need a trailing wildcard
regardless; that is inherent to CGI, not to this bug.
* On Windows, NTFS 8.3 short names are not rejected here. fileserver's
ServeHTTP rejects them (and ':' for ADS) precisely because matchers cannot
see through them, so GET /ADMIN~1.PHP is judged as /admin~1.php by a
path matcher while NTFS resolves SCRIPT_FILENAME to admin.php. Closing that
means sharing fileserver's hasWindowsShortName across packages, which is a
larger change than this fix; it is reported alongside this one and a patch
is available on request.
Side effects: PATH_INFO and DOCUMENT_URI are now derived from the normalized
path, so repeated slashes are merged and "." segments removed (/index.php//a//b
yields PATH_INFO=/a/b rather than //a//b), and on Windows per-component trailing
dots and spaces are folded. An application routing on a raw PATH_INFO will see
this. An empty URL.Path stays empty rather than becoming ".". REQUEST_URI is
unchanged and still carries the original target.
Tests: a buildEnv regression over literal and percent-encoded dot segments,
covering that legitimate PATH_INFO is preserved and an empty path stays empty; a
Windows-only counterpart whose cases each fail without the Windows
normalization; and an end-to-end test of a basic_auth-protected php_fastcgi
route against a FastCGI responder reporting the SCRIPT_FILENAME it received. The
end-to-end test uses `index off` so no rewrite route exists, ensuring it
exercises the transport rather than the rewrite.
* fileserver: make first_exist_fallback fall back when the file is absent
The try_policy first_exist_fallback is documented to match unconditionally,
falling back to the last try_files entry when no earlier candidate exists. It
did not: candidates are built with fs.Glob, which returns only paths that exist,
so an absent final entry left the list empty and the fallback branch — nested
inside the loop over that list — was unreachable.
php_fastcgi hard-codes this policy so its generated rewrite route always runs;
when the fallback failed, that rewrite was silently skipped.
Keep the pattern itself as a candidate when nothing globbed. This also aligns
Linux with Windows, where globResults is populated without an existence check
and the fallback already fired.
Behaviour change worth calling out: on a php_fastcgi site whose root has no
index.php (or no file matching the configured index), the rewrite now always
runs, so a request for a path that exists neither on the filesystem nor as a PHP
script is proxied to the backend as <root>/index.php instead of falling through
to file_server and returning 404. That is what the documented fallback promises,
but for a site of standalone .php scripts plus static assets it is a visible
change: unmatched requests now reach PHP-FPM.
The other try policies still test existence explicitly, so a candidate that does
not exist is skipped — but note this applies to ANY pattern that expands to
nothing, not only a malformed glob: a file whose literal name contains glob
metacharacters can now match under first_exist, largest_size and smallest_size
where previously globbing found it only via expansion.
* fileserver: synthesize the literal candidate only for the fallback entry
Address review: the previous commit appended the pattern itself as a
candidate inside makeCandidates whenever globbing found nothing. Since
makeCandidates is shared by every try_policy, first_exist, largest_size,
smallest_size and most_recently_modified could then stat the literal
pattern, so a file whose name contains glob metacharacters (for example
"lit[1].txt") matched where it did not before.
Restore makeCandidates to its original behaviour and build the literal
candidate only for the final entry of first_exist_fallback, the one
entry that is documented to match unconditionally. Placeholders in that
entry are expanded without glob escaping, since nothing is globbed, so
a request for /a[b].txt falls back to /a[b].txt rather than /a\[b].txt.
The path is still cleaned and joined with SanitizedPathJoin, so it
cannot escape the root.
This supersedes the last paragraph of the previous commit message: the
other policies now behave exactly as they did before that commit. The
php_fastcgi behaviour change described there still applies.
Add a regression test that creates a file literally named "lit[1].txt"
and checks that only the fallback entry of first_exist_fallback matches
it by its literal name, including the split_path remainder.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fastcgi: add license headers to new test files
Address review: phpfastcgi_test.go and fastcgi_windows_test.go were
added without the Apache license header used across the codebase. On
the Windows test, the header goes above the build constraint, matching
replacer_fuzz.go.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fastcgi: avoid hardcoded Unix paths in dot-segment tests
Address review: the new tests compared SCRIPT_FILENAME against strings
with forward slashes ("/srv/other.php", a "/admin.php" suffix), but the
transport makes the root absolute and joins it with filepath.Join, so
on Windows the value is C:\srv\other.php and the assertions would fail.
Use a temporary directory as the root and build the expected paths with
filepath instead.
Also lower the bcrypt cost of the integration test's basic_auth hash to
the minimum. At cost 14 the authenticated request exceeded the 5s test
client timeout under -race.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With request_buffers or response_buffers set to unlimited, the error
from reading the body was discarded and whatever had been read was
treated as the complete body. A request cut short by request_body
max_size was forwarded truncated with a 200, and a truncated chunked
upstream response was sent to the client as complete.
Handle a read error the same way the limited case already does.
* caddy: pass ListenQUIC options as a struct
Collapse the TLS config, packet-conn wrappers, and Allow0RTT arguments
into ListenQUICOptions so the signature matches Listen's ctx/portOffset/
ListenConfig prefix. Zero-value opts keep the historical default of
allowing 0-RTT. The API remains experimental.
* caddyhttp: advertise WebTransport in HTTP/3 SETTINGS
Add github.com/quic-go/webtransport-go dep (built on quic-go, same
maintainers) and call webtransport.ConfigureHTTP3Server on the
http3.Server. This advertises WebTransport enablement in SETTINGS,
enables HTTP/3 DATAGRAMs, and stashes the *quic.Conn in each request's
context — a prerequisite for a later WebTransport-aware handler or
reverse-proxy transport to call webtransport.Server.Upgrade. Also
enable QUIC stream reset partial delivery, required by webtransport-go.
No user-visible behavior change: clients that don't speak WebTransport
ignore the extra SETTINGS, and no handler yet calls Upgrade.
Extract the http3.Server construction into buildHTTP3Server so the
SETTINGS assertions can be unit-tested without a live UDP listener.
* caddyhttp: add UnwrapResponseWriterAs helper
Go's type assertion `x.(T)` does not follow Unwrap() http.ResponseWriter
chains. Caddy wraps the writer multiple times (logging recorder,
intercept, encode, etc.), so code that needs interfaces implemented only
by the raw writer owned by the HTTP server — for example the
http3.Settingser/HTTPStreamer interfaces that webtransport.Server.Upgrade
type-asserts — cannot see through those wrappers.
UnwrapResponseWriterAs walks the Unwrap() chain and returns the first
writer that satisfies the requested interface (or the zero value if none
do). Mirrors the traversal http.ResponseController performs internally.
Used by upcoming WebTransport handler and reverse-proxy transport.
* caddyhttp: add terminating WebTransport handler
Introduces http.handlers.webtransport, an EXPERIMENTAL handler that
terminates an incoming WebTransport session on top of Caddy's HTTP/3
server and echoes bytes on each bidirectional stream. Primary use case
is as a test upstream for the forthcoming WebTransport reverse-proxy
transport; it also serves as the minimal proof that the server-side
WebTransport wiring works end-to-end.
Plumbing changes:
* caddyhttp.Server gains a *webtransport.Server field alongside
h3server. It's built in buildWebTransportServer(), wrapping the
existing http3.Server. Exposed via WebTransportServer() any on the
Server, so the caddyhttp public API does not name the upstream
webtransport-go type (per AGENTS.md).
* serveHTTP3 now runs a custom accept loop (serveH3AcceptLoop) that
dispatches each accepted QUIC connection to
webtransport.Server.ServeQUICConn instead of
http3.Server.ServeListener. The WebTransport server transparently
forwards non-WT streams to the underlying http3 request handler
(cost: one varint peek per stream), so behavior for non-WT clients
is unchanged.
* ListenQUIC enables EnableDatagrams and
EnableStreamResetPartialDelivery on the QUIC listener config.
These are capability bits negotiated during the QUIC handshake and
are prerequisites for any WebTransport session; they do not force
usage so non-WT H3 traffic is unaffected.
* Stop path closes wtServer after h3server.Shutdown to clean up any
remaining WebTransport session state.
The handler uses caddyhttp.UnwrapResponseWriterAs to reach the naked
http3.Settingser/HTTPStreamer writer through Caddy's wrapping chain
before calling webtransport.Server.Upgrade.
Includes unit tests for request-shape detection plus an integration
test (caddytest/integration/webtransport_test.go) that spins up a
Caddy HTTP/3 server with the handler, dials it with a real
webtransport.Dialer, and asserts end-to-end bidirectional-stream
echo.
* reverseproxy: add WebTransport upstream dialer helper
dialUpstreamWebTransport is a thin wrapper around webtransport.Dialer.Dial
that sets the QUIC config flags WebTransport requires (EnableDatagrams,
EnableStreamResetPartialDelivery) and forwards request headers on the
Extended CONNECT. Intended as an internal building block for the
upcoming WebTransport reverse-proxy transport; not yet wired into
ServeHTTP.
Unit-tested against an in-process webtransport.Server with a freshly
minted self-signed certificate. Covers: successful dial, header
forwarding, and connection-refused against an unbound port.
* reverseproxy: add WebTransport session pump
runWebTransportPump bridges two WebTransport sessions so every
bidirectional stream, unidirectional stream, and datagram opened on one
side is mirrored on the other. Uses six goroutines (bidi both ways, uni
both ways, datagrams both ways) and blocks until both sessions end.
Close propagation: when either session ends, the peer is closed via
CloseWithError. The code/message are read from the closing session's
stored close state (by probing AcceptStream with a short timeout),
since Receive{Datagram,UniStream} return the underlying stream error
rather than the SessionError and can win the propagation race. Close
propagation is best-effort for client-initiated close through a
Dialer-dedicated QUIC conn: webtransport-go tears down the QUIC
connection immediately after CloseWithError, so the upstream may
observe a QUIC ApplicationError before the WT_CLOSE_SESSION capsule is
parsed. The pump still closes the peer session; only the specific
error code may not survive.
Not yet wired into ServeHTTP.
Tests: topology of client -> frontend -> upstream where frontend runs
the pump. Exercises bidi both ways, uni client-to-upstream, datagram
round-trip, CloseWithError propagation both ways, and a basic
goroutine-leak check.
* reverseproxy: wire WebTransport pump into ServeHTTP
Extends the http reverse-proxy transport with a webtransport boolean
that opts the upstream into WebTransport passthrough. Must be combined
with versions: ["3"]; WebTransport rides on HTTP/3 exclusively.
When enabled, Handler.ServeHTTP detects Extended CONNECT with
:protocol=webtransport early — before any of the normal round-trip
machinery — and branches to serveWebTransport, which:
1. Pulls the *webtransport.Server off caddyhttp.Server (via
WebTransportServer()) and errors out cleanly if HTTP/3 isn't
enabled on the frontend.
2. Picks a single upstream through the configured load-balancer.
No retries: a failed dial closes the client session and returns.
3. Walks the response-writer Unwrap() chain to reach the raw http3
writer and calls webtransport.Server.Upgrade to terminate the
incoming session.
4. Uses dialUpstreamWebTransport to open a session to the selected
upstream, forwarding request headers on the Extended CONNECT.
5. Runs runWebTransportPump between the two sessions and blocks
until both close.
The transport's wtTLSConfig is built at Provision time from the
existing TLS config (same path h3Transport already uses) and reused
for every session.
Tests: adds TestWebTransport_ReverseProxyEndToEnd which spins up a
single Caddy instance with two HTTP/3 servers — one proxy on :9443,
one terminating echo upstream on :9444 — and drives a real
webtransport.Dialer through the proxy to assert end-to-end
bidirectional-stream echo.
* reverseproxy: add webtransport Caddyfile subdirective
Adds a `webtransport` subdirective to the `transport http {}` block of
reverse_proxy that sets the new WebTransport bool on the transport.
Takes no arguments; exclusivity with versions 3 is enforced at
Provision time so parse order doesn't matter.
Example:
reverse_proxy https://backend:9443 {
transport http {
versions 3
webtransport
tls_insecure_skip_verify
}
}
Includes a Caddyfile-to-JSON adapt test round-tripping the new
subdirective.
* reverseproxy: apply standard request preparation to WebTransport CONNECT
The WebTransport proxy path previously bypassed the request-preparation
pipeline that normal reverse-proxy traffic runs through. Reuse it so
`header_up`, `X-Forwarded-For`/`Host`/`Proto`, `Via`, `Rewrite`, the
`{http.reverse_proxy.upstream.*}` placeholders, dynamic upstreams,
`countFailure`, and the `{http.reverse_proxy.duration{_ms}}` timing
placeholder all behave the same as on the regular path.
Retries, `handle_response`, and response-header ops are intentionally
not run here — a WebTransport session has no HTTP response body to
post-process and is not idempotent. Integration test exercises the
header-forwarding contract end-to-end through a standalone (non-Caddy)
WebTransport upstream so the forwarded Extended CONNECT can be
inspected.
* reverseproxy: dial WebTransport upstream before upgrading client
Reorder serveWebTransport so the upstream is dialed first. If the
upstream is unreachable or refuses the CONNECT, a proper 5xx is returned
from the handler — the client's Dial() surfaces the real status instead
of a successful upgrade followed by an opaque session close.
Also apply `h.Headers.Response` (gated by `Require`, if configured)
against the upstream response status/headers; the ops run on the
client-visible response headers, which webtransport.Server.Upgrade
flushes with the 200 OK. If the client-side upgrade fails after the
upstream dial succeeded, close the upstream session cleanly.
Integration test drives a dial to an unbound loopback port and asserts
the client sees a 5xx status instead of a bare session close.
* reverseproxy: track WebTransport sessions in upstream in-flight counters
Bracket the pump's lifetime with Host.countRequest(±1) and
incInFlightRequest/decInFlightRequest so WT sessions participate in the
same accounting as the normal proxy path:
- MaxRequests gating (Upstream.Full) now blocks WT sessions past the
cap, instead of silently failing open.
- LeastConn / FirstAvailable selection sees WT load, instead of seeing
busy upstreams as idle.
- Admin /reverse_proxy/upstreams reports WT sessions under num_requests.
Integration test holds an upstream session open via a standalone WT
server, polls the admin API to assert num_requests increments during
the session and drops back to 0 after close.
* reverseproxy: extract shared upstream-selection helpers
The WebTransport proxy path in serveWebTransport duplicated the
dynamic-upstream-fallback block and the {http.reverse_proxy.upstream.*}
replacer-variable block from proxyLoopIteration. Francis flagged this
as a maintenance burden in review of #7669.
Extract two helpers:
* resolveUpstreams(r) returns the candidate upstream set — dynamic
when configured (with provisioning + fallback-on-error), static
otherwise. Caller runs the LB selection policy, since the two call
sites diverge on how selection failure is reported (retry loop vs.
fast 502 for long-lived WT sessions).
* setUpstreamReplacerVars(repl, up, di) publishes the seven
placeholders describing the selected upstream.
Both are used by proxyLoopIteration and serveWebTransport with
identical semantics to the inlined code they replace. No behavior
change for either path.
* reverseproxy: inline WebTransport protocol const and writer interface
Francis pointed out in review of #7669 that importing the whole
modules/caddyhttp/webtransport package solely to pull in one constant
and one interface wasn't worthwhile.
Move both into webtransport_transport.go as unexported identifiers
(webtransportProtocol, webtransportWriter). This removes reverseproxy's
dependency on the caddywt package and clears the way for moving the
echo handler itself out of the production module tree.
No behavior change.
* caddyhttp: move WebTransport echo handler to integration tests
Francis pointed out in review of #7669 that the echo handler — which
exists solely as a test upstream for the WebTransport reverse-proxy
tests — should not be a full-fledged module registered in every Caddy
binary. Mirroring the mockdns_test.go pattern, move it into a _test.go
file under caddytest/integration/.
The module ID http.handlers.webtransport is now registered only when
the integration test binary is built, which is when
caddytest/integration/webtransport_test.go references it by ID string
in its JSON configs. Production Caddy builds no longer include it.
Changes:
* New file: caddytest/integration/webtransport_echo_test.go —
contains the WebTransportEcho handler, its types and interface
guards, the isWebTransportEchoUpgrade helper, and the unit tests
that used to live in the deleted package's handler_test.go.
* Deleted: modules/caddyhttp/webtransport/ (handler.go + handler_test.go).
* Removed the blank import from modules/caddyhttp/standard/imports.go.
The Protocol const and Writer interface that this package used to
export were inlined into reverseproxy's own files in a preceding
commit, so nothing else depends on the deleted package.
* caddyhttp, reverseproxy: gate WebTransport behind enable_webtransport server flag
steadytao raised an architectural concern in review of #7669: the PR
put experimental WebTransport handling directly into Caddy's core
HTTP/3 accept path, so every HTTP/3 deployment paid for the feature
whether or not they used it.
Collapse the enablement surface to a single server-level opt-in that
matches Caddy's existing precedent for protocol-level features
(`protocols`, `allow_0rtt`, `enable_full_duplex`), and detect the
request shape at the handler the same way `reverse_proxy` detects a
WebSocket upgrade today — no per-handler config flag.
Core HTTP/3 path changes (modules/caddyhttp/server.go):
* New `EnableWebTransport bool` field on Server, marked EXPERIMENTAL.
* buildHTTP3Server now only calls webtransport.ConfigureHTTP3Server
and sets EnableStreamResetPartialDelivery when the flag is true.
When false, the constructed http3.Server is bit-for-bit identical
to the pre-WebTransport implementation.
* wtServer is constructed only when the flag is true.
* serveH3AcceptLoop falls back to http3.Server.ServeListener when
the flag is false — no varint peek, no per-connection dispatch.
Caddyfile wiring (caddyconfig/httpcaddyfile/serveroptions.go):
* New `enable_webtransport` global server option, modeled on
`enable_full_duplex`.
Reverse-proxy simplifications (modules/caddyhttp/reverseproxy/):
* Removed HTTPTransport.WebTransport field and its Provision-time
exclusivity check (no longer needed; H3 is validated separately).
* Removed the `webtransport` Caddyfile subdirective under
`transport http { }` — this neutralizes the prior commit that
introduced it.
* Removed Handler.webtransportEnabled cache. ServeHTTP now branches
on isWebTransportExtendedConnect(r) alone, matching how the
WebSocket upgrade branch works.
* serveWebTransport gains fail-fast guards with clear errors when
the parent server has enable_webtransport=false or when the
handler's transport does not include HTTP/3.
Tests:
* Existing TestServer_BuildHTTP3ServerEnablesWebTransport now sets
EnableWebTransport=true explicitly; new
TestServer_BuildHTTP3ServerWithoutWebTransport locks in the
regression guard that flag-off produces the pre-PR http3.Server.
* Integration tests updated: enable_webtransport: true added to
every H3 server block; "webtransport": true dropped from the
reverse_proxy transport JSON (auto-detected now).
* Caddyfile adapt test for the deleted `webtransport` subdirective
is removed; `enable_webtransport` is added to the existing
global_server_options_single adapt test alongside its peers.
No runtime behavior change when enable_webtransport is false. Diff
against master on the core HTTP/3 path is effectively zero in that
configuration.
* caddyhttp: micro-benchmark HTTP/3 server construction with/without WebTransport
Adds a hermetic pair of benchmarks on buildHTTP3Server to provide
quantitative evidence for the claim that deployments with
enable_webtransport=false pay no cost for the WebTransport feature.
Results on Apple M4, go1.25, -count=5:
BenchmarkBuildHTTP3Server_WebTransportOff ~70 ns/op 392 B/op 3 allocs/op
BenchmarkBuildHTTP3Server_WebTransportOn ~144 ns/op 600 B/op 6 allocs/op
The Off path is about half the cost on every dimension, confirming
that the work skipped when the flag is false is the
webtransport.ConfigureHTTP3Server call plus EnableStreamResetPartialDelivery.
Absolute cost is a one-time per-server setup so either branch is
negligible in practice, but the asymmetry locks in a regression guard:
a future refactor that accidentally re-enables the WT configuration
unconditionally would show up as a jump in the Off numbers.
This benchmark does not exercise the per-stream dispatch cost inside
webtransport.Server.ServeQUICConn — that would require a full QUIC
setup to measure in isolation and is follow-up work.
* reverseproxy: collapse WebTransport handler into main proxy loop
The WT path duplicated upstream resolution, LB selection, header ops,
replacer vars, and in-flight counters. Route WT through the shared
ServeHTTP -> proxyLoopIteration -> reverseProxy flow and swap RoundTrip
for a small webTransportHijack that only does WT-specific work (writer
unwrap, upstream dial, client upgrade, pump).
Rename roundtripSucceededError -> terminalError. The existing name
described when it was emitted (after a successful round-trip); the
new name describes its contract with the retry loop (stop looping,
propagate error unchanged). The WebTransport upgrade case is a second
natural caller for that same signal.
Comes with two behavior improvements that fall out of the collapse:
- WT upstream dial failures now surface as DialError, so the loop
can fail over across upstreams like normal proxies (today: 502).
- Passive health checks apply to WT dials (dial-failure countFailure
and UnhealthyLatency on dial duration) via the shared path.
Addresses reviewer feedback that the duplicated setup was a
maintenance risk.
* reverseproxy: inline upstream-resolution helpers back into the proxy loop
The earlier #7669 revision extracted resolveUpstreams and
setUpstreamReplacerVars to share between the WebTransport path and the
normal proxy path. A later commit collapsed the WebTransport path into
the main proxy loop, so the helpers became single-callers — they no
longer earn their abstraction.
Inline both back into proxyLoopIteration. The dynamic-upstream block
absorbs the CachingUpstreamSource cache-reset logic added by #7662,
which was preserved through the rebase.
No behavior change.
* caddyhttp: clarify WebTransportServer doc covers both disabled paths
WebTransportServer returns nil for two distinct configurations:
EnableWebTransport=false on the server, and HTTP/3 not in use. The
prior doc only mentioned the latter. Spell out both, per review.
* reverseproxy: drop nil-logger guard in runWebTransportPump
Production and test callers both pass a non-nil *zap.Logger. The
defensive nil check silently swapped in a no-op logger, which can mask
a real bug at the call site. Document the precondition in the doc
comment instead, per review.
* reverseproxy: use sync.WaitGroup.Go in WebTransport pump
Replace the wg.Add(6) + six "go func() { defer wg.Done(); fn() }()"
calls with sync.WaitGroup.Go (Go 1.25+, the project's minimum
version). Same number of goroutines; less boilerplate.
* caddyhttp: drop defensive checks in UnwrapResponseWriterAs
Per review, the nil-pointer guard and the next == w self-reference
guard defended against shapes that no in-tree ResponseWriter wrapper
produces. Drop both, simplifying the loop. Also remove the
self-reference test that exercised the now-removed guard.
* reverseproxy: use r.URL.RequestURI() to build the WebTransport upstream URL
Replace buildWebTransportUpstreamURL with an inline call to
r.URL.RequestURI() at the single call site. RequestURI handles the
empty-path → "/" fixup and query suffixing already, and uses
EscapedPath so encoded path segments (e.g. %2F) are preserved on the
upstream — fixing a small correctness gap where the previous helper
read r.URL.Path (decoded).
Aligns with how Caddy's normal proxy path lets stdlib render the
outbound request line via URL.RequestURI semantics.
* reverseproxy: use wg.Go in spliceBidi
* reverseproxy: expand SNI placeholders on the WebTransport upstream dial
The normal HTTP/3 path expands placeholders in tls_server_name at dial
time via a custom h3Transport.Dial hook (#7737). The WebTransport path
dials through its own webtransport.Dialer and bypasses that hook, so a
placeholder like {http.request.host} reached the QUIC ClientHello
literally. Expand it in webTransportHijack before dialing, cloning the
shared TLS config so concurrent sessions are unaffected.
Adds an integration test asserting the upstream observes the resolved
SNI rather than the literal "{...}" string.
* reverseproxy: bump webtransport-go to v0.11.0 (WebTransport draft-15)
webtransport-go v0.11.0 implements draft-15, which renames the Extended
CONNECT :protocol token from "webtransport" to "webtransport-h3" (the
server keeps accepting the legacy token for older clients). Detect both
tokens in the reverse-proxy upgrade check and in the test echo handler.
Pulls quic-go v0.59.1 -> v0.60.0, required by webtransport-go v0.11.0;
its only breaking change is requiring Go 1.25 (already met), with no API
changes affecting Caddy core. Also picks up the v0.11.0 fix for a
stream-close race on concurrent session close, relevant to the long-lived
pump.
* caddyhttp: log WebTransport QUIC connection serve errors
* caddy: gate QUIC WebTransport capabilities behind an explicit ListenQUIC parameter
* caddyhttp: fall back to plain HTTP/3 when a reused QUIC listener lacks WebTransport
* caddyhttp: enable WebTransport via a webtransport config object
Replace the enable_webtransport bool with a presence-gated
WebTransportConfig object so later options can be added without
renaming the key. JSON is "webtransport": {}, Caddyfile is
webtransport (optional empty block).
* reverseproxy: use origReq for the client WebTransport upgrade
Upgrade is the client-facing session: CheckOrigin and the CONNECT
shape must come from the downstream request. The upstream-directed
clone remains used for the dial URL, header_up, and SNI expansion.
* reverseproxy: relay WebTransport application protocol negotiation
Parse WT-Available-Protocols from the downstream request, set it on
the upstream Dialer, and copy WT-Protocol from the upstream 200 onto
the client response. The shared webtransport.Server does not select
a protocol of its own, so MOQ/MediaMTX clients receive the upstream's
choice instead of WT_ALPN_ERROR.
* caddytest: skip origin check on standalone WebTransport backends
header_up Host rewrites used by proxy tests would 400 the upstream
Upgrade via CheckOrigin. Test backends are not browsers.
* caddytest: cover MediaMTX Host-rewrite + moqt-19 through the WT proxy
Both of elee's #7669 reports fire together: header_up rewrites Host
(Upgrade must use origReq) and the client offers moqt-19 (the proxy
must relay WT-Available-Protocols / WT-Protocol). A bidi echo after
negotiation proves the session is usable, not just headers.
* caddyhttp: break self-referential Unwrap loops
A wrapper whose Unwrap returns itself would spin forever. Restore the
cycle-break mholt applied on #7669, without bringing back the nil-pointer
guard Weidi asked to drop.
* caddyhttp: close WebTransport sessions before HTTP/3 shutdown
ServeQUICConn waits for active WT sessions, and with the default
eternal grace period HTTP/3 Shutdown would hang until every client
disconnected. Close the WebTransport server first so stop/reload
can drain.
* caddyhttp: serve plain HTTP/3 when a reused listener lacks WebTransport
A pooled QUIC listener created without DATAGRAMs cannot upgrade WT
sessions. Falling back on the WT-configured http3.Server still
advertised SETTINGS the socket cannot honor. Build a separate plain
HTTP/3 server for that listener and assert it omits WT SETTINGS.
* caddyhttp: visit Unwrap writers by pointer identity
Comparing writers with == panics on uncomparable types and only
catches a one-node cycle. Track pointer identity, bound the walk,
and add tests for an uncomparable self-unwrap and an A→B→A cycle.
RecordHijackedStatus lets Upgrade write through the naked HTTP/3
writer while the access-log recorder still observes the 200.
* reverseproxy: honor prepared Host, header_down, and WT bookkeeping
Dialer.Dial always sets Host from the URL, so header_up Host never
reached the upstream. Dial the real address via DialAddr and put
the prepared Host on the CONNECT.
Parse WT-Available-Protocols from the prepared request so header_up
applies to negotiation. Copy filtered upstream response headers
before header_down, record the hijacked 200 for access logs, and
run the same latency / circuit-breaker / passive-health path as
a normal round-trip.
* caddytest: cover WT reload, access-log status, Host, and header ops
Reload with an eternal grace period must close an in-flight session.
Access logs must record 200 after Upgrade. header_up Host and
WT-Available-Protocols must reach the upstream; header_down and
non-hop upstream headers must reach the client.
* reverseproxy: build WebTransport against quic-go v0.63
webtransport-go v0.13.0 renames Dialer to Transport. v0.11.1 does not
compile against quic-go v0.63.
* reverseproxy: close the upstream body when a WebTransport dial fails
Dial can return a response with an error when the peer rejects the
CONNECT. Close that body before the error is returned.
* caddyhttp: close WebTransport sessions without closing the HTTP/3 server
webtransport.Server.Close also closes the shared HTTP/3 server and aborts
every QUIC connection before the grace period applies. Close the registered
sessions, then let HTTP/3 shutdown drain ordinary requests.
* chore: modernise
Signed-off-by: Zen Dodd <mail@steadytao.com>
* reverseproxy: preserve WT-Available-Protocols across upstream retries
The proxy loop reuses the request and only recopies headers when header
ops are configured. Deleting the offer on that map hid it from the next
upstream.
* caddyhttp: close WebTransport sessions when the grace period ends
Eternal grace leaves an active session until the peer ends it. A
configured grace period closes it at the deadline, while the socket
is still up, so the close capsule can be delivered.
* reverseproxy: use terminalError when refusing an Incremental response
The proxy-loop collapse renamed roundtripSucceededError to terminalError.
Master's Incremental refusal still returned the old type, so the rebased
package did not compile.
* caddyhttp: don't let idle HTTP/3 conns pin WebTransport shutdown
- Count in-flight requests on ServeQUICConn connections and close idle ones when shutdown starts
- Delay that close by a second so a just-finished response can be read
- Retain plain HTTP/3 fallbacks for listeners without datagrams and shut them down with the same grace period
* reverseproxy: wait for WebTransport stream workers
- Join bidi and uni splice workers after the accept loops return
- Cancel the opposite copy on a hard splice error
---------
Signed-off-by: Zen Dodd <mail@steadytao.com>
Co-authored-by: tomholford <tomholford@users.noreply.github.com>
Co-authored-by: Zen Dodd <mail@steadytao.com>
Use i/j swap instead of !sortByPath for vars, so matchers that can't
be compared keep their Caddyfile order. Path matchers still sort with
the more specific one later. Fixes the vars ordering part of #8187.
* caddyhttp: Skip matchers of grouped routes once the group is satisfied
* caddyhttp: Test that a skipped grouped route keeps regexp captures
A later rewrite in the same group must not run its path_regexp matcher
and overwrite {re.x.1} from the rewrite that was used.
* caddyhttp: preserve expired request-body read deadlines
* caddyhttp: retain only expired drain deadlines
Track the last successfully installed read deadline and retain it after a terminal timeout only when draining is enabled and the deadline has expired. Failed deadline changes preserve the previous installed value.
Cover early and wrapped timeouts, non-draining requests, failed deadline installation and replacement, and cleanup retries.
* caddyhttp: shorten installed deadline field name
* caddyhttp: ignore case when matching hosts in logger_names
* caddyhttp: test case-insensitive logger_names through App.Provision
* caddyhttp: test case-insensitive logger_names in integration harness
* httpcaddyfile: keep a site block's TLS policy for ECH public names
The global ech option added its own automation policy for every public
name, even when a site block already had a policy for that name. If the
site block set its own TLS options, adapting failed with "hostname
appears in more than one automation policy". If a hostless catch-all
with the same settings was also present, the site's policy was merged
away and the name got only the global settings.
Skip public names that already have a policy, as tls_automate_names
does, and only add the ECH policy when it still has subjects, since a
policy without subjects acts as a catch-all.
* httpcaddyfile: check ECH public names the way the TLS app matches them
The check for an existing site policy only matched a subject spelled
exactly like the ECH public name. A site policy for *.example.com, or
for example.com when the public name is written Example.COM, already
covers that name at runtime, but the global ECH policy was still added
and sorted ahead of it, so the site's issuer settings were overridden.
Lowercase and trim the public name as ECH provisioning does and compare
it with certmagic.MatchWildcard, which is how the TLS app picks a policy
for a name.
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.
* reverseproxy: Optionally detach stream (websockets) from config lifecycle
* lint
* Improved logging facilities
* Adjustments from Weidi's review
* record bytes read and written for response writers unless detached
* simplify streaming handling
* correctly close detached streams
* make handleUpgradeTunnel a standalone func
* fix tests
* fix tests
* change the log level if hijacking without writing a status code first
* only clean up connections when stopped
* Move type and const down to the bottom
* Rename to tunnelTracker, reflow some comments
* Add note about capturing h
* Rename to stream_detached
* More comments
* reverseproxy: Preserve half-close behavior after rebase
Keep master half-close propagation and its unit coverage in the refactored tunnel lifecycle. Adapt response finalization tests to the selected upstream argument.
AI-assisted-by: Codex (GPT-6)
* reverseproxy: Fix detached stream cleanup and recorder chains
Snapshot the tracker registry before closing connections, and unregister empty trackers outside their mutex. Mark trackers stopped at cleanup entry so upstream removal closes detached streams even with stream_close_delay, while attached streams retain their delay.
Detach every supported response writer in the middleware chain so inner recorders do not retain state or count detached traffic. Add regressions for all four failures and an access-log/encode/intercept reload integration test.
AI-assisted-by: Codex (GPT-6)
---------
Co-authored-by: WeidiDeng <weidi_deng@icloud.com>
* httpcaddyfile: new tls_automate_names global option
Provisioning a certificate for a name that is not served requires giving
it a site block of its own:
*.example.com {
}
foo.example.com {
respond "Real site"
}
That asks the tls app for the wildcard, but it also adds a route to the
http app, so every name pointed at the server that has no site block of
its own -- bar.example.com here -- gets an empty but valid response
rather than no match at all. Wanting a certificate and wanting to serve
a name are separate things, and the Caddyfile had no way to say only the
first.
Name them in the new global option instead:
{
tls_automate_names *.example.com
}
foo.example.com {
respond "Real site"
}
The names are added to the automate certificate loader and to an
automation policy built from the global options, so a name listed here
is managed exactly as it would be from a site block, with the same
issuers; the only difference in the adapted config is that no route is
added for it. Repeating the option appends rather than replaces, so a
long list can be split over several lines.
Names that cannot get a public certificate are given the internal
issuer, the same treatment a site block gives them. Names that already
appear in the automate list are skipped, but a name that also has a site
block is left listed here as well: a site block for http:// only is not
managed by auto-HTTPS, so dropping the name because a block exists could
silently leave it without a certificate.
The option needs no http app at all, so a config consisting only of
global options now adapts to a tls app on its own -- enough to keep
certificates renewed for a mail or XMPP server, or for names served by a
layer 4 app.
Closes#7122
* httpcaddyfile: reuse an existing policy for an automated name
A name given to tls_automate_names may already have an automation policy
from its own site block. Adding a second policy for the same subject is
not just redundant: the adapter rejects overlapping subjects, so
{
email nobody@example.com
tls_automate_names foo.example.com
}
foo.example.com {
tls {
ca https://acme.example.test/directory
}
}
failed to adapt at all, with "hostname appears in more than one
automation policy, making certificate management ambiguous".
Keep such a name in the automate loader but leave its policy alone. The
site block's policy is the more specific of the two, and the loader entry
is still wanted, since a site block served only over HTTP is not managed
by auto-HTTPS.
* Pin that tls_automate_names overrides auto_https off
The names given to the option are managed whether or not auto-HTTPS is
disabled: unlike the hostless-key block above it, that code is not gated
on auto_https, because naming a subject explicitly is a stronger signal
than the general switch. Nothing enforced it, so add the adaptation
fixture @steadytao asked for.
Provision previously only checked for a single opening brace at the
start of a destination, so values like {result}suffix, {result, and
{result}} passed validation. strings.Trim(dest, "{}") then silently
stored a wrong placeholder name (e.g. result}suffix), making the
intended value unavailable.
Require the destination to be exactly one placeholder: a single
opening brace at the start, a single closing brace at the end, and a
non-empty name.
Fixes#8073
when a request exceeds the request_body max_size limit, the request_body handler wraps the http.MaxBytesError into a caddyhttp.HandlerError carrying status 413, but the {http.request.body} and {http.request.body_base64} placeholders ran io.Copy with the error ignored, so they silently returned the truncated prefix as though it were the complete body. docs promise a 413 for reads past max_size, and silently truncating is a bad failure mode in templates and vars_regexp where the body value drives decisions. see #7691 and the narrow follow-up prescribed when #7692 was closed
reading the body now returns a dedicated RequestBodyLimitError marker instead of a generic HandlerError, and only when the read failure is actually the max_size limit. the consumers (template placeholder function, vars and vars_regexp matchers) recognize exactly that marker and wrap it in a status carrying handler error so the oversized request fails with 413, while every unrelated error value keeps its old behavior: templates still render it as text and the vars matchers still match on its error text. the surfaced error is stripped of its generated id and stack trace so a placeholder stringified into a response body cannot leak the call stack
negative regressions prove an unrelated HandlerError in templates, vars and vars_regexp does not start controlling request handling, plus integration tests for the template and vars_regexp 413 paths
ai assisted (GLM agent) under Abdel's direction and local verification
Signed-off-by: Abdel <hktitof@gmail.com>
* Move websocket header normalization later
A regression since #6621, these headers can be rewritten by cloneHeaders
and by .Add from user and transport ops. Moving this closer to where
it's round-tripped feels like the nicest solution.
I've put the harness I used to reproduce the issue at
https://codeberg.org/nfreya/caddy-websocket-headers-repro
* Integration test for normalized websocket headers
---------
Co-authored-by: Zen Dodd <mail@steadytao.com>
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
A request whose body has no length of its own — Transfer-Encoding: chunked, or
HTTP/2 and HTTP/3 requests sent without the header — cannot be forwarded over
FastCGI until it has been buffered in full, because CGI/1.1 requires
CONTENT_LENGTH and php-fpm hangs when it is absent or wrong and the body is not
empty. When request_buffers is too small to hold the whole body, the length
stays unknown and the request is refused with 411.
The refusal said none of that. RoundTrip passes r.ContentLength to Post, which
is -1 for such a request, so the operator got a bare "411 Length Required" with
either no error at all or strconv's "invalid syntax" on a value they never
wrote. On #7386 that sent two people looking for the fault in their backend.
The 411 now carries the reason and the remedy, and the unknown-length case is
told apart from a genuinely malformed value. ParseUint becomes ParseInt plus an
explicit negative check, which is strictly tighter: values above MaxInt64 used
to be accepted and are unreachable anyway, since CONTENT_LENGTH is always
FormatInt of an int64 by the time Do sees it.
No status code changes. Every request that was refused before is still refused;
raising request_buffers is still what makes a large chunked body work.
Tests: the reachable path through Post, the unusable CONTENT_LENGTH values Do
itself guards against, an integration test driving a chunked body through a
fastcgi reverse_proxy over a unix socket at each side of the buffer boundary
(including request_buffers -1, which succeeds at any size and is the remedy),
and a boundary test recording that a body exactly the size of the buffer counts
as partial.
That last one is deliberately not changed here. Telling "exactly the limit"
apart from "more to come" costs a read that a paused stream may never answer,
and bufferedBody also backs response_buffers: measured against a body that
delivers exactly the limit and stops, the current code hands the buffered
prefix on immediately, while peeking one byte first delivers nothing at all.
Co-authored-by: Aditya <205600203+Rohilalala@users.noreply.github.com>
Co-authored-by: Zen Dodd <mail@steadytao.com>
* 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.
Same obsolete two-argument check as the reverse_proxy one removed in
#7869: it referenced the long-removed inline status-code-replacement
syntax and pointed users at replace_status even when no status
replacement was involved. The generic check now rejects all
excess-argument cases with an accurate message.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* caddyhttp: add url_pattern request matcher
Match requests against a URLPattern (https://urlpattern.spec.whatwg.org/),
supporting named groups, wildcards and regexp components beyond the path
matcher. Relative patterns match any origin; absolute patterns or base_url
scope to scheme and host.
Exposes a url_pattern CEL function and publishes captured groups as
{http.url_pattern.<component>.<group>} placeholders.
* caddyhttp: add Caddyfile adapt test for url_pattern matcher
Multiple `hide` subdirectives in a file_server Caddyfile block silently overwrote each other, so only the last one took effect. Append to the list instead, so repeated entries accumulate and imported snippets can compose with site-specific hides.
* test: add failing tests for intercept replace_status (#7805)
Add integration tests that verify replace_status actually substitutes
the HTTP status code sent to the client. Currently these tests fail
because replace_status is silently a no-op due to value-receiver
boxing and shouldBuffer returning false.
Tests added:
- TestInterceptReplaceStatusWithMatcher: 500 -> 200 with @err matcher
- TestInterceptReplaceStatusWithoutMatcher: 403 -> 200 unconditionally
- TestInterceptReplaceStatusNotMatched: 200 passes through unchanged
* fix: make intercept replace_status actually substitute the status code
Fix#7805: replace_status was silently a no-op because:
1. shouldBuffer returned false when a replacement status was set,
causing the original status to be streamed directly to the wire
2. The value-receiver WriteHeader method operated on a stale copy
The fix:
- shouldBuffer now returns true when replace_status matches, so
the response is buffered instead of streamed
- After next.ServeHTTP returns, if routes are nil (replace_status
only), write the substituted status and buffered body to the client
The interceptedResponseHandler.WriteHeader substitution branch is no
longer needed for this path since substitution happens post-ServeHTTP.
* refactor: remove dead WriteHeader method and resolved TODO
The value-receiver WriteHeader on interceptedResponseHandler was
unreachable dead code — the substitution is now handled post-ServeHTTP
via buffering. Remove it along with the TODO comment that noted
status code replacement was unfinished.
* style: apply nit suggestions from dunglas code review
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* reverseproxy: validate on weighted_round_robin policy
Validate that weighted_round_robin has a non-zero total weight.
This prevents configurations such as:
weighted_round_robin 0 0
from being accepted and causing a divide-by-zero panic during request handling.
* test: validation test on zero weight upstreams.
* test: provision called instead of totalweight setting
* reverseproxy: validate on negative upstream weights
* test: regression test on weighted_round_robin selection policy
* fix: error on duplicate named_routes
Fixes issue #7798
Validate named route names before inserting them into the
named route map.
This prevents later definitions from overwriting existing
named routes and returns an error when a route name is
defined more than once.
* test: add test for duplicate named_routes
* reverseproxy: replace placeholders specified for sni while using http3
* add test for placeholder
* reverseproxy: replace placeholders specified for sni while using http3
* add test for placeholder
* reverseproxy: test HTTP/3 SNI host placeholder
---------
Co-authored-by: Zen Dodd <mail@steadytao.com>
* 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