2788 Commits
Author SHA1 Message Date
Akinyemi Toluwalase 71c5812cfe caddytls: validate curve and protocol names, compare protocol IDs (#8266)
* caddytls: validate curve and protocol names, compare protocol IDs
* caddytls: Add final newline to tests and drop duplicate protocol comment
2026-10-26 20:14:59 +00:00
Sulaiman Dauda b058e3f3f6 fastcgi: stop response bodies at the declared Content-Length (#8212)
* fastcgi: Stop response bodies at the declared Content-Length
* fastcgi: Use a cancellable test context in proxy tests
2026-10-26 19:32:45 +00:00
tomholfordandGrok 4.7 258863f6f6 reverseproxy: strip hop-by-hop headers when copying response headers (#8270)
* reverseproxy: strip hop-by-hop headers when copying response headers

copy_response_headers copied the upstream header map before
finalizeResponse could strip hop-by-hop fields. Clone and strip
first, so include and exclude cannot forward them, and leave the
upstream response unchanged for later handlers in the route.

Co-authored-by: Grok 4.7 <noreply@x.ai>
2026-10-26 09:21:29 +00:00
Mohammed Al Sahaf 4e8de214bc replacer: cache file lookups and cap them (#8248)
* replacer: cache file lookups and cap them

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>

* replacer: cache repeated file placeholder lookups

Memoize file placeholder results per replacement call, using an 8 MiB LRU for contents. Preserve empty and failed-read results without changing the output for unknown placeholders.

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>

---------

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>
2026-10-24 22:32:06 +00:00
Rob Martin 62bc606433 reverseproxy: close backend body and hijacked socket on upgrade setup failures (#8269)
* reverseproxy: close backend body and hijacked socket on upgrade setup failures

When an upstream response indicates an HTTP upgrade (101 Switching Protocols),
handleUpgradeResponse takes ownership of res.Body and subsequently hijacks
the client connection.

However, if any setup check fails prior to starting handleUpgradeTunnel
(such as mismatched upgrade headers, non-writable body assertion failure,
hijack unsupported, or handshake buffer flush errors), the function returned
early without closing res.Body or the hijacked client socket.

Because calling Hijack() permanently disables net/http's automatic connection
cleanup, and raw TCP sockets are not closed by the garbage collector, these
early failure exits leaked file descriptors into CLOSE_WAIT/ESTABLISHED.

Fix this by:
1. Adding a deferred cleanup guard that closes res.Body and any hijacked
   front connection unless the tunnel successfully takes ownership.
2. Wrapping connections with onceCloseConn to ensure idempotent closure
   while preserving optional CloseWrite (TCP half-close) support.
3. Adding TestUpgradeResponseCleanupOnFailure to verify cleanup.

* reverseproxy: test post-hijack cleanup and remove once-close wrappers

Cover handshake flush and buffered backend-write failures after successful hijacking, checking that both connections close. Keep setup-failure cleanup while using the original connections directly.

Code and tests were AI-assisted using an OpenAI coding assistant in Pi.
2026-10-24 22:19:19 +00:00
Krish Singh Hura 8e237f1955 windows: serialise service status updates (#8274)
* publish service status reported before the SCM handler attaches
* notify: serialize Windows service status updates
2026-10-25 02:27:06 +10:00
Aradhya Jain b1694e7a4c caddyhttp: add CEL expression support to the tls matcher (#8088)
* caddyhttp: add CEL expression support to the tls matcher

MatchTLS was the only core HTTP matcher without a CELLibrary() implementation,
so it could not be used inside an expression matcher. Every sibling matcher
(host, path, path_regexp, method, query, header, header_regexp, protocol,
remote_ip, client_ip, url_pattern, vars, vars_regexp, and the file-server's
file matcher) already has this.

Adds CELLibrary() to MatchTLS, exposing tls_handshake_complete('true') /
tls_handshake_complete('false') in CEL expressions. Mirrors MatchProtocol's
existing string-argument-macro pattern: CELMatcherImpl with cel.StringType,
parsing the argument with strconv.ParseBool and constructing a
MatchTLS{HandshakeComplete: &b} that reuses the matcher's own
MatchWithError. Adds the corresponding CELLibraryProducer interface guard.

Adds 6 cases to celmatcher_test.go's existing table-driven
TestMatchExpressionMatch/BenchmarkMatchExpressionMatch: match on
'true'/'false' against a complete/incomplete handshake, and three
invocation-error cases (no args, wrong arg type, non-boolean string).
Required adding a tlsHandshakeComplete *bool field to the shared test table
and wiring it into req.TLS.HandshakeComplete in both the test and benchmark
request-construction helpers.

Fixes #8087

* address review: use a native bool arg, rename to match the matcher's own name

francislavoie's review on #8088:
- "A boolean as a string argument is very strange." CELMatcherImpl only
  supported string/list(string)/map argument types, so the original
  implementation parsed a string literal with strconv.ParseBool to work
  around that. Instead, extends CELMatcherImpl itself with a cel.BoolType
  case (celMatcherBoolMacroExpander + isCELBoolExpr, mirroring the existing
  string equivalents), so a matcher can accept a real CEL bool literal.
  This is shared infrastructure, so any other matcher needing a bool
  argument in the future gets it for free.
- "I'm not sure why tls_handshake_complete was chosen when the matcher is
  named tls." Renamed the CEL function to tls(...), matching every other
  matcher's naming convention (the function name mirrors the matcher's own
  directive/module name: host(), path(), method(), protocol(), etc.).

Usage is now 'expression tls(true)' / 'expression tls(false)' instead of
'expression tls_handshake_complete(\'true\')'.

Updated the 6 existing test cases accordingly; still covers match/no-match
on both true/false and the two invocation-error cases (no args, wrong arg
type -- now a string instead of a bool, confirming the new type check).

* address review: cover tls() against a plain non-TLS request

steadytao asked for a test proving tls(false) doesn't treat the absence
of TLS as an incomplete handshake. MatchWithError already handles this
correctly (r.TLS == nil returns false regardless of HandshakeComplete),
this just adds the missing coverage for both tls(true) and tls(false)
against a plain http:// request.

Confirmed these tests catch a real regression: removing the r.TLS == nil
guard causes a nil pointer dereference panic, not just a wrong result.

* address review: redesign tls() CEL matcher to preserve all three Caddyfile states

The Caddyfile tls matcher has three states: bare `tls` (any TLS connection,
regardless of handshake completion), `tls early_data` (handshake not yet
complete), and the implicit "handshake complete" case. The previous
tls(true)/tls(false) design could only express the latter two, with no way
to express "any TLS, don't care about handshake state" from CEL.

Redesigns the macro to:

  tls()               -- any TLS connection (mirrors bare `tls`)
  tls('complete')      -- handshake complete
  tls('early_data')    -- handshake not yet complete (mirrors `tls early_data`)

tls() is implemented as sugar for tls('any'): a zero-arg macro that expands
to the same 2-arg call the one-arg string macro produces, just with the
literal injected by the macro itself. Both macros share one function
registration and one matcher factory, so there's a single source of truth
for the string-to-MatchTLS mapping.

This replaces the native-bool design from the prior commit, so also removes
the generic bool-argument support (celMatcherBoolMacroExpander,
isCELBoolExpr, and the CELMatcherImpl case for cel.BoolType) added for it,
since nothing else in the codebase used it and it would otherwise ship as
unreviewed, unused surface area.

Rewrites the existing tls(...) test cases for the new string-based calls,
and adds coverage for the new tls() form: matches regardless of handshake
state (both complete and incomplete), and still correctly fails to match a
plain non-TLS request. Also covers the new invocation-error cases: a
boolean argument is no longer valid, and an unrecognized string (anything
other than 'complete'/'early_data') is now a Provision-time error instead
of silently doing the wrong thing.

Verified: full caddyhttp test suite passes, and confirmed each new/changed
test fails when its corresponding implementation piece is reverted
(the zero-arg macro, and the complete/early_data boolean mapping).

* address review: document and test tls('any') as a supported literal form

steadytao pointed out that the zero-arg tls() macro expands internally to
the same tls_request_string(req, "any") call that the one-arg form uses,
which means tls('any') was already silently reachable as a literal string
even though it was never documented or tested as part of the public API.

Rather than special-casing the macro to bypass the shared string factory
(which would need a second function/overload just for the zero-arg case),
document and test tls('any') as a real, supported third spelling alongside
tls() -- they're already fully equivalent, so there's nothing to hide.
2026-10-24 08:01:24 -04:00
a 2b47a1849f reverseproxy: emit OpenTelemetry client spans (#8177)
* reverseproxy: emit OpenTelemetry client spans for upstream requests

When the request carries a valid span context (set by the tracing
handler), each upstream round trip, including retries, is recorded as a
client span and that span's context is propagated to the upstream.
Requests that are not traced use the transport directly.

The transport is wrapped at the call site rather than replacing
h.Transport, so transport interface checks and health checks are
unaffected. The tracer provider comes from the span in the request
context, so no global provider is needed.

Fixes #7223

* reverseproxy: assert client span URL, upstream address and status

Use a provisioned HTTPTransport so the test fails if roundTrip stops
setting the scheme before the traced transport records url.full.

* reverseproxy: add license header to tracing test
2026-10-23 16:28:55 +10:00
Adarsh KumarandClaude Opus 5.5 f355b8687d reverseproxy: retry dial errors wrapped by a forward proxy (#8264)
When a forward proxy is configured (network_proxy, forward_proxy_url or
HTTP_PROXY), net/http wraps dial errors in a *net.OpError with Op
"proxyconnect". tryAgain used a type assertion to detect DialError, so it
missed the wrapped error and treated it as a failure after the request
was sent. Non-GET requests were then not retried, even though no
connection to the proxy was ever established.

Use errors.As so a DialError is recognized anywhere in the error chain.
Errors that do not contain a DialError are unaffected, so requests that
may have reached the proxy or upstream are still not retried by default.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 11:49:32 +00:00
Mohamed A. Abdel Fattah bfdbeb4a1b reverseproxy: strip hop-by-hop headers from interim responses (#8256) 2026-10-20 17:11:30 +10:00
Jay Hemnani 8ba327ef3f templates: use the earliest front matter closing fence (#8259) 2026-10-20 17:07:07 +10:00
Francis LavoieandClaude Opus 5.5 0e392c89ba core: Retry privileged binds on EACCES instead of checking port < 1024 (#8253)
WithBindCapability assumed the kernel's privileged port range ends at
1024, but it is set by net.ipv4.ip_unprivileged_port_start, which can
be anywhere from 0 (Docker's default) to 65535. With a value above
1024, a non-root bind to e.g. :1500 failed even though the capability
was permitted.

Now listen is called normally first, and only if it fails with EACCES
on a TCP/UDP port, and CAP_NET_BIND_SERVICE is permitted but not
effective, the capability is raised and the bind retried. This also
skips raising it when the range starts lower than the port.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 19:19:28 +10:00
Krish Singh Hura e91867d35c reverseproxy: preserve request trailer values (#8246) 2026-10-19 09:16:58 +00:00
Jay Hemnani 612128e3b0 logging: Parse request URIs in the query filter like the server does (#8255) 2026-10-19 07:43:29 +00:00
awss 872c864c20 caddytls: skip client cert verifiers when no certificate is presented (#8251) 2026-10-19 02:22:16 -04:00
Jay Hemnani e3649f2536 logging: Reject out-of-range ip_mask CIDRs (#8241) 2026-10-19 02:20:53 -04:00
awss 719db53e94 caddyfile: check heredoc markers without a regexp (#8249)
The formatter checked every token starting with "<<" against the heredoc
marker regexp. In a libFuzzer build each string comparison copies both
strings, and the regexp compares the remaining input on every step, so a
long marker-shaped token made the fuzz-format target time out.

A byte loop accepts the same markers in linear time. The lexer uses it
too, so there is one definition of a valid marker.
2026-10-17 09:22:41 -04:00
Zen Dodd 1619352f4e ci: fix exhaustive linting (#8238) 2026-10-17 19:49:32 +10:00
Zen Dodd 2e0f806104 Revert #8223
This reverts commit 1f099b3b86.
2026-10-17 15:28:01 +08:00
Jay Hemnani 0584875297 headers: evaluate conditional defaults independently (#8217)
* 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.
2026-10-16 22:32:44 +10:00
Jay Hemnani be5fba4ff2 caddyauth: handle CRLF and unterminated hash-password input (#8227)
A password piped with a Windows line ending was hashed with the trailing
\r, so basic_auth rejected the real password, and input without a final
newline failed with EOF.
2026-10-16 22:29:54 +10:00
Jay Hemnani a6fc144e75 logging: use the default file mode for dir_mode from_file (#8225) 2026-10-16 22:26:57 +10:00
Hitanshu 1f099b3b86 reverseproxy: honor health check redirect destinations (#8223) 2026-10-16 22:24:34 +10:00
Omar Ismail 860b4ce635 logging: include module logs from its parent namespace (#8221) 2026-10-16 11:38:15 +00:00
小莳 af7620dcc8 templates: preserve colons in humanize time layouts (#8215) 2026-10-14 17:45:33 +10:00
pamnarioandClaude Opus 5.5 4ec5a5c39a Merge commit from fork
* 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>
2026-10-13 10:13:03 -06:00
Shane 69f22e2442 fileserver: return an empty listing for offsets past the end (#8213)
Signed-off-by: Shane Jonas <jonas.shane@gmail.com>
2026-10-14 01:36:53 +10:00
Jay Hemnani 1962b20401 fastcgi: Use 302 for a Location response without Status (#8209)
A CGI script that redirects by printing only a Location header got a
200 response with that header, which clients do not follow. RFC 3875
section 6.2.3 says the server sends 302 Found for this response, and
net/http/cgi, nginx and Apache do so.
2026-10-14 00:27:51 +10:00
Jay Hemnani fc3d83400b reverseproxy: Don't drop read errors with unlimited buffers (#8207)
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.
2026-10-14 00:25:34 +10:00
a 401582cccf templates: avoid zero content length for bodyless responses (#8153)
* templates: avoid zero content length for bodyless responses

Drop Content-Length for HEAD and statuses that cannot carry a body instead of using the empty template buffer length. Continue setting the rendered length for ordinary responses.

AI-assisted-by: Jcode (OpenAI)

* templates: test Content-Length for HEAD, 204 and 304 responses

* templates: omit Content-Length for 2xx responses to CONNECT

RFC 9110 section 8.6 forbids Content-Length in any 2xx response to
CONNECT. Non-2xx CONNECT responses still report the rendered length.
2026-10-13 13:55:30 +10:00
c7b36dc55e caddyhttp, reverseproxy: experimental WebTransport passthrough (#7669)
* 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>
2026-10-12 23:24:10 -04:00
Francis Lavoie d293dcfe86 httpcaddyfile: Add log_level directive for access logs (#8201)
* httpcaddyfile: Add log_level directive for access logs

* Remove AI assistance file

* httpcaddyfile: Sort log_level with broader matchers first

* caddytest: Cover log_level Caddyfile adaptation
2026-10-12 11:54:20 -04:00
Francis Lavoie 7328273d6f caddyfile: avoid quadratic heredoc marker scanning (#8203) 2026-10-12 20:52:19 +10:00
IcyJerry 6ae02b5ccb httpcaddyfile: keep source order for vars with non-comparable matchers (#8197)
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.
2026-10-12 20:49:24 +10:00
tannevaledandFrancis Lavoie f2c20fb810 reverseproxy: deduplicate DNS SRV+A discovery into modules/internal/network (#7790)
* reverseproxy: extract DNS SRV+A discovery into a reusable package

Move the SRV and A/AAAA resolution + caching logic out of reverseproxy
into a new, transport-neutral package (dynamicupstreams) that returns
neutral targets instead of *reverseproxy.Upstream. reverseproxy's
SRVUpstreams and AUpstreams now delegate to it and build their own
upstreams from the targets; behavior is unchanged.

This lets other proxies (e.g. third-party layer4 proxies) reuse the same
DNS discovery + caching instead of copying it, reducing duplication and
maintenance burden. reverseproxy/upstreams.go shrinks substantially.

RFC for the de-duplication discussed in caddyserver/caddy-l4#429.

* test(dynamicupstreams): cover ResetAllSRV, filtered records, cache bound, concurrent refresh

Brings the package to 96.8% statement coverage. Adds:
- TestResetAllSRV: full-cache drop
- TestSRVFilteredRecords: LookupSRV partial-error semantics (usable
  records returned, error downgraded to a warning)
- TestSRVCacheBound / TestACacheBound: eviction keeps the cache bounded
  at maxCacheEntries
- TestSRVConcurrentRefreshDeduplicates / TestAConcurrentRefreshDeduplicates:
  two goroutines missing the read-lock fast path trigger only one lookup
  (verified under -race)

The grace-period and filtered-records logs now use a level-enabled logger
so the log-write branches execute. The only uncovered statements are the
two double-checked-locking re-check returns, which are reachable solely in
a narrow race window (the same untested idiom as reverseproxy's SRV/A).

* reverseproxy: move DNS SRV+A discovery into modules/internal/network

Per review feedback, the shared discovery code does not belong in a
top-level package: it is internal helper code, not part of Caddy's
runtime surface. Move it from the top-level dynamicupstreams package into
the existing modules/internal/network package and update reverse_proxy's
upstreams.go to consume it from there.

Being under modules/internal means third-party modules (e.g. layer4)
cannot import it yet; they keep their own copy until layer4 is part of
Caddy's standard distribution, at which point they can switch over.

* test(dynamicupstreams): correct the concurrent-refresh test's claim

The two concurrent-refresh tests documented themselves as covering the
write-lock re-check branch ("the second re-checks under the write lock"),
but they do not: while the first goroutine holds the write lock the second
cannot acquire the read lock either, so by the time it acquires srvMu the
cache is already populated and it is served by the read-lock fast path. The
50ms sleep was presented as letting the second goroutine "queue on the write
lock" but proved nothing.

Make the comments and failure messages honest: these tests assert
deduplication (two concurrent callers for one uncached key share a single
lookup) and, under -race, race-safety on the shared cache -- not a specific
locking branch. Ordering is enforced by the channels, so the sleep is removed.

No production code change.

* reverseproxy: preserve DNS discovery logging and grace behavior

Keep refresh and discovery logs in the DNS lookup path, retaining the original SRV field types. Preserve error suppression when an SRV lookup fails with grace enabled before any result has been cached.

Add regression tests for refresh-only logging and cold-cache grace handling.

AI-assisted-by: OpenAI Codex (GPT-6)

---------

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2026-10-12 10:37:55 +00:00
Jay Hemnani 054c92b9fa httpcaddyfile: keep handle_errors blocks in Caddyfile order (#8195) 2026-10-12 17:11:20 +10:00
Zen Dodd 25c6901e05 httpcaddyfile: isolate TLS policies across listeners (#8192) 2026-10-12 16:38:47 +10:00
Jay Hemnani d4165a4eb1 caddyhttp: Reject repeated expression matchers in a matcher set (#8183)
* caddyhttp: Combine repeated expression matchers in a matcher set

The Caddyfile parser groups repeated matchers of the same type and hands
all their segments to the module, but the expression matcher only read
the first segment, so every later `expression` line in a named matcher
or `not` block was dropped without an error. Read every segment and AND
the expressions, like the other matchers in a set.

* caddyhttp: Put each joined expression on its own line

A trailing // comment in one expression no longer swallows the closing
parenthesis and the expressions joined after it.

* caddyhttp: Reject repeated expression matchers instead of joining them

A second expression line in a named matcher or a not block is now a
Caddyfile error that points to combining them with && in one expression,
instead of being dropped or joined with an implied operator.
2026-10-11 06:50:29 +10:00
Jay Hemnani 5538b947d2 caddyhttp: skip matchers for satisfied route groups (#8189)
* 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.
2026-10-11 06:50:13 +10:00
小莳 c31d9a71ed caddyhttp: preserve expired request-body read deadlines (#8178)
* 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
2026-10-10 10:40:46 +00:00
Indra Gunawan d588d5c8f0 cmd: unbuffer default logger on startup failure (#8180)
* cmd: unbuffer default logger on startup failure

Restore default logger to unbuffered console logger on cmdRun error so WrapCommandFuncForCobra can output the error.

* add regression tests
2026-10-09 21:22:40 +00:00
Jay Hemnani 7c7456353e reverseproxy: Only retry responses for expressions that use response data (#8168)
* reverseproxy: Only retry responses for expressions that use response data

Since #7569 every lb_retry_match set with an expression is evaluated against
upstream responses. An expression that only tests the request, such as
{http.request.method} == "POST", then matched every successful response:
the request was sent again to the next upstreams and the client got a 502
once retries ran out. Only treat expressions referencing {rp.*} placeholders
as response conditions.

* reverseproxy: Only count placeholders set from the response as response data

* reverseproxy: Read response placeholders from the checked expression

The expression matcher records the placeholder names its checked CEL
expression reads, so retry_match finds direct ph() calls and ignores
escaped placeholder text in string literals. A placeholder name only
known at evaluation time counts as response data.

* reverseproxy: Match response placeholder names exactly

Only the header placeholders are matched by prefix now, and both
upstream latency placeholders are listed. MatchExpression.Placeholders
returns a copy, so callers cannot change the provisioned matcher.
2026-10-10 07:22:32 +10:00
tbshfr 7910dd94ee caddyhttp: ignore case when matching hosts in logger_names (#8176)
* 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
2026-10-09 21:15:23 +00:00
Jay Hemnani 093fd4b7ec encode: Don't write an empty encoded stream for HEAD responses (#8150)
* encode: Don't write an empty encoded stream for HEAD responses

When nothing was written (e.g. a HEAD request), Close() still closed the
encoder, which wrote the empty stream trailer to the client. The server
then set Content-Length to that size (20 for gzip, 13 for zstd) instead
of omitting it as for the matching GET response.

* encode: Check resp.ContentLength in the HEAD test

* encode: Track body writes to decide whether to close the encoder
2026-10-09 18:14:56 +10:00
awss c8591a1e46 httpcaddyfile: keep a site block's TLS policy for ECH public names (#8156)
* 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.
2026-10-09 16:48:00 +10:00
Jay Hemnani 519be12306 reverseproxy: Keep own headers after 1xx responses (#8169)
* reverseproxy: Keep own headers after 1xx responses

Since #6656 the 1xx handler clears the whole header map after writing the
informational response, which also drops headers set before proxying (for
example by the header handler). Restore those after clearing, as #5091 did.

* reverseproxy: Use maps.Copy to restore own headers

* reverseproxy: Check the 103 reaches the client in the 1xx headers test
2026-10-09 16:42:26 +10:00
a 1b3838c1fd go.mod: update golang.org/x/net to v0.60.0 (#8179) 2026-10-09 04:42:01 +00:00
zemamba 221ebc4503 fix: preserve FastCGI dial errors for retry (#8172)
Wrap backend dial failures as reverseproxy.DialError so non-GET requests can be retried when no connection was made.

AI-assisted implementation and test; reviewed by zemamba.

Signed-off-by: zemamba <zemamba@gmail.com>
2026-10-08 14:02:20 +10:00
Jay Hemnani 6727b2f3a9 templates: Delete file headers on any template error (#8149)
* templates: Delete file headers on any template error

A template that fails to parse or execute (not only {{httpError}}) left
the Content-Length, Etag and related headers set by file_server on the
error response, so clients received a 500 that declared the file's size
but had no body.

* templates: Also delete Content-Encoding and Content-Range on errors

Precompressed and ranged file responses could leave these headers on the
replacement error response.
2026-10-08 03:40:32 +00:00
ammann d65583e6d2 fileserver: apply hide rules to browse entries and sidecars (#8155) 2026-10-08 13:26:26 +10:00