Commit Graph
1338 Commits
Author SHA1 Message Date
Mohammed Al Sahaf b4f54cea53 caddyhttp: fix randString sameCase dictionary to match its docs
The doc comment says randString excludes confusing characters like
I, l, 1, 0, O. When sameCase is true, uppercase letters and the
characters l and o should be excluded. But the sameCase dictionary
still contained '0'. Drop it.

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>
2026-08-30 18:45:23 +03:00
TowyTowyandClaude Fable 5 7bf1b9057b rewrite: fix strip_path_suffix ignoring percent-encoding (#7877)
StripPathSuffix is documented to behave like StripPathPrefix: the suffix
is matched in normalized (unescaped) space except where the pattern uses
an escape sequence. But suffix stripping was implemented as

    reverse(trimPathPrefix(reverse(escapedPath), reverse(suffix)))

Reversing the strings moves the '%' to the *end* of each "%xx" escape,
which defeats trimPathPrefix's escape detection (it expects '%' to
precede the two hex digits). As a result the escape-aware, normalized
comparison never happened for suffixes: a decoded pattern failed to
match a percent-encoded path.

Concretely, StripPathPrefix "/a/b/c" strips "/a%2Fb/c/d" to "/d", but the
mirror StripPathSuffix "/b/c" left "/a/b%2Fc" untouched instead of
producing "/a"; likewise StripPathSuffix "bc" did not strip "/a%62c".
This has been the behavior since #4948, which introduced both the
escape-aware trimPathPrefix and the reverse-based suffix trimming.

Replace the reverse trick with a dedicated trimPathSuffix that iterates
from the ends of both strings and applies the same escape-aware,
case-insensitive comparison as trimPathPrefix. An escape in the pattern
itself is still compared literally, so "%2fsuffix" continues to require
the path to contain that exact escape.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-28 19:44:05 +10:00
David Carliez 3244ef4105 fileserver: reject short names in every path component (#7952)
* fileserver: reject short names in every path component
Signed-off-by: DavidCarliez <271374756+DavidCarliez@users.noreply.github.com>

* fileserver: validate short-name characters
Signed-off-by: DavidCarliez <271374756+DavidCarliez@users.noreply.github.com>

* fileserver: fail closed on extended short names
Signed-off-by: DavidCarliez <271374756+DavidCarliez@users.noreply.github.com>
2026-08-28 09:09:23 +00:00
Francis LavoieandClaude Opus 5 39af0aec31 rewrite: fix URI splitting when a query or fragment arrives via a placeholder (#7947)
* rewrite: don't drop a trailing '=' from the query string

buildQueryString scanned for '=' unconditionally when looking for the
end of a component, but '=' only delimits a key from its value once;
any further '=' bytes are literal data. When the query ended with '=',
that byte was consumed as a delimiter with nothing following it to
re-emit, so it was silently lost:

	?x=1&sig=YWJjZA==  =>  ?x=1&sig=YWJjZA=

This corrupts base64 padding in the last query parameter, which is the
shape of an S3 presigned URL (X-Amz-Signature), turning a valid
signature into a 403. Only the final '=' of the query was affected;
?sig=YWJjZA==&x=1 came through intact.

Disable the '=' search while consuming a value so that only '&' ends it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* rewrite: honor a query string injected by a replacement value

Which URI components get written back was decided from the literal
config string, before placeholders were expanded, but an injected query
is only detected after expansion. The two were never reconciled: for
`rewrite * {rp.header.X-Accel-Redirect}` there is no literal '?', so
qsStart stayed -1, and the correctly-built query string was computed and
then discarded by the `if qsStart >= 0` guard.

Only half the split was applied. The path was still truncated at the
injected '?', so the query was not preserved either -- it was dropped,
and any query already on the request survived in its place:

	GET /orig?keep=me, X-Accel-Redirect: /hello?some=param
	=> /hello?keep=me

Track whether a query was actually injected and include that in the
write-back condition. Appending a literal '?' to the rewrite value was
the known workaround precisely because it set qsStart; that keeps
working and is now unnecessary.

The flag is only set where the injected query is adopted, so an
explicitly configured query still wins, and a value with no '?' still
leaves the query untouched -- which is what the implicit rewrites of
try_files and php_fastcgi rely on. Those stay safe regardless, since
escapePathPlaceholders already escapes the two placeholders they use, so
a client-supplied %3F cannot split the URI.

Fixes #5208

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* rewrite: drop a fragment injected by a replacement value

The scan that separates path, query and fragment runs on the literal
config string, so a '#' arriving later via a replacement value was never
treated as a delimiter. It leaked into whichever component it landed in:

	X-Accel-Redirect: /hello?p=x#frag  =>  RawQuery = "p=x#frag"

Everything after '#' is fragment (RFC 3986 section 4.2) and a fragment is
never sent to the server, so drop it before the path is split, mirroring
how the scan already handles a literal '#'. An escaped %23 is unaffected,
so a real '#' in a path or query is still expressible, and a configured
fragment still wins over an injected one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 12:06:09 -06:00
Faiyaz Rahman 4974956b9c chore: fix lint errors from newer golangci-lint (#7958)
Signed-off-by: Faiyaz Rahman <faiyazrahman03@gmail.com>
2026-08-25 13:12:10 +10:00
Kévin Dunglas 0cf03d32f7 caddyhttp: mitigate slowloris via idle read/write deadlines (#7913)
* caddyhttp: mitigate slowloris via idle read/write deadlines

ReadTimeout and WriteTimeout previously applied as a single hard
deadline over the whole body/response through http.Server, so any
non-zero value also killed large transfers from legitimately slow
clients. Reset the deadline on every successful read/write instead
(via http.ResponseController), and give both a sane 1m default now
that doing so no longer penalizes slow-but-progressing clients.

* caddyhttp: split idle read/write timeouts from the existing hard ones

Reworking ReadTimeout/WriteTimeout's own semantics was an unwanted
behavior change for existing configs relying on the hard deadline.
Leave them untouched and add ReadIdleTimeout/WriteIdleTimeout instead,
reset on every successful read/write; both default to 1m since,
being new, no existing config could have depended on a different
value. Combining an idle timeout with its hard counterpart now gives
the same base+ceiling shape as Apache's mod_reqtimeout, for free.

* caddyhttp: cap idle-reset deadlines at the hard timeout ceiling

Deadlines are a single absolute value on the connection, not a min of
several: ReadTimeout/WriteTimeout's own hard deadline, set once by
net/http before the handler runs, was silently getting overwritten by
the first idle-reset Read/Write, voiding it entirely. Clamp the
idle-reset deadline to the hard one when both are set, so combining
them actually behaves like the advertised base+ceiling.

* caddyhttp: add ReadMinRate/WriteMinRate, Apache MinRate equivalent

Pure idle-reset alone doesn't bound a trickle that sends just enough
to never go idle. ReadMinRate/WriteMinRate (bytes/second) grow the
allowed deadline from a fixed start based on bytes transferred so far
instead of resetting to a flat window on every call, so a transfer
that doesn't sustain the configured rate falls behind real time and
gets cut, matching Apache mod_reqtimeout's MinRate. Zero (default)
keeps the existing flat idle-reset behavior unchanged.

* caddyhttp: use named return and consistent blank lines in idleDeadline

Matches the named-return style already used by ResponseWriterWrapper.ReadFrom.

* caddyhttp: chunk idleTimeoutWriter's Write/ReadFrom, cap at 64 KiB

SetWriteDeadline bounds the whole call it precedes, not just a stall
within it. net.Conn.Write loops internally until a buffer is fully
sent (unlike Read, which returns after one syscall), and
ResponseWriter.ReadFrom hands the entire remaining source to the
connection in one call. A single large Write, or any body copied via
io.Copy triggering the ReadFrom fast path (http.ServeContent, static
file serving), had its whole transfer bounded by one deadline,
silently truncating a slow-but-healthy transfer exactly like a hard
WriteTimeout would - the same bug found and fixed the same way in
FrankenPHP's go_ub_write (php/frankenphp#2574).

Cap each underlying call at 64 KiB and reset the deadline between
chunks instead. net/sendfile.go special-cases *io.LimitedReader, so
chunking ReadFrom still uses the sendfile fast path per chunk.

* caddyhttp: export idle-timeout types, add configurable MaxWriteChunk

Export IdleTimeoutReader/IdleTimeoutWriter/IdleDeadline so other
packages (request_body next) can reuse the same idle-reset mechanism
instead of reimplementing it, and turn the hardcoded 64 KiB write
chunk size into a configurable MaxWriteChunk field defaulting to the
same value - nginx's sendfile_max_chunk exists for the identical
reason and is admin-tunable rather than fixed.

* requestbody: idle-reset ReadTimeout/WriteTimeout, add MinRate/MaxWriteChunk

ReadTimeout/WriteTimeout set a single deadline once, so any transfer
running longer than the timeout got cut regardless of whether it was
actually stalled - the same bug the server-wide timeouts had before
switching to idle-reset. Reuse caddyhttp.IdleTimeoutReader/Writer here
too, giving per-route granularity nginx/Apache have via location/
directory scoping and Caddy's server-wide timeouts don't: a route
matching this handler can now set its own idle window independently
from the rest of the server block.

* caddyhttp: fold read/write min_rate into the idle-timeout directive

Two directives per rate (read_body_idle + read_body_min_rate) for a
value that's meaningless without the other. Fold min_rate into the
idle-timeout directive as an optional second argument instead.

* caddyhttp: split write pacing out of request_body into new timeouts handler

request_body is a request-body concern (max_size, set); ReadTimeout/
WriteTimeout/MinRate/MaxWriteChunk pace both directions, and write
pacing has nothing to do with the request body. Move all of it to a
dedicated http.handlers.timeouts module instead, mirroring the
server-wide timeouts option one level down.
2026-08-21 21:17:26 -06:00
bzyy1024 45ba3278b5 fastcgi: fix HTTPoxy vulnerability (#7934)
* Implement HTTPoxy mitigation in FastCGI
Added HTTPoxy mitigation to prevent trusting client-supplied Proxy header for HTTP_PROXY environment variable.

* Implement test for HTTPoxy vulnerability protection
Add test to ensure HTTPoxy vulnerability is mitigated by dropping client-supplied Proxy headers.

* gofmt: format code
* revert unrelated gofmt change to replacer_test.go
2026-08-19 13:07:24 +10:00
yangshenghui 0bbda5c728 fix: close resources on error paths (#7940) 2026-08-16 21:02:39 +10:00
Kévin Dunglas 789f60e337 caddyhttp: fix url_pattern authorization bypass via encoded-slash traversal (#7941)
* caddyhttp: match url_pattern against decoded, cleaned path

The url_pattern matcher evaluated the raw, percent-encoded request URI
while path-consuming handlers resolve the decoded, cleaned r.URL.Path.
An encoded-slash payload such as "..%2f" stayed a single opaque segment
for the WHATWG URLPattern parser, so "/public/..%2fadmin/secret" matched
"/public/*" while handlers decoded it to "/admin/secret", bypassing any
route-level access control built with url_pattern.

Match the same path model handlers resolve: decode the path, normalize
and clean it (mirroring the path matcher and #4407), then re-encode
through url.URL to a canonical escaped form before running the pattern.
The go-urlpattern library is spec-correct; the fix is in the integration.

* build(deps): bump github.com/dunglas/go-urlpattern to v1.0.0

Moves off the pseudo-version to the first tagged release.
2026-08-15 10:38:20 -06:00
6584c75999 reverseproxy: isolate active health-check state per distinct check config (#7916)
* reverseproxy: isolate active health-check state per distinct check config

Multiple reverse_proxy handlers configured with different active health
checks (health_uri, health_headers, ...) against the same upstream dial
address currently share a single Host in the global pool, so one
handler's failing probes mark the address unhealthy for every other
handler. Key the pool by dial address plus a stable fingerprint of the
active health-check config, so distinct checks get independent health
state.

The fingerprint is strictly internal to pool identity: the Prometheus
upstreams_healthy label and the /reverse_proxy/upstreams admin endpoint
continue to report the plain dial address, unchanged.

Dynamic upstreams are intentionally out of scope here: they resolve
through a separate per-lookup path (dynamicHosts) and collapsing there
has different lifetime semantics; noted for a follow-up.

Fixes #7870

* reverseproxy: use strings.Cut in hostKeyAddress

Satisfies the modernize linter; behaviour is unchanged, since Cut returns
the whole string when the separator is absent.

* reverseproxy: expose the health-check fingerprint as a public discriminator

Health state is now kept per (dial address, active health check config),
but both user-visible surfaces still reported address alone:

- caddy_reverse_proxy_upstreams_healthy was labeled only by upstream, so
  every handler sharing an address wrote the same series concurrently and
  the reported value was whichever updater ran last. The metric gains a
  health_check label carrying the config fingerprint ("" when no active
  checks), so each health target owns its series; aggregate across checks
  with sum/min by (upstream).

- /reverse_proxy/upstreams reported one entry per pool key but with only
  the plain address, so consumers indexing by address silently discarded
  all but one entry. Entries now carry health_check (omitted when empty),
  and the endpoint documents that (address, health_check) is the entry's
  identity — one entry per health target, deliberately not aggregated,
  since any aggregation here would be lossy and undocumented.

Tests: two handlers on one address with different checks must produce two
metric series reflecting their own state (fails if the fingerprint is
dropped from the label), and two admin entries distinguished by non-empty
fingerprints.

* reverseproxy: narrow the fix to per-Upstream active health counters

Move the consecutive active pass/fail counters from Host onto Upstream,
alongside the active unhealthy state that already lives there, instead of
re-keying the global host pool.

Host is keyed by dial address alone, but an active health check is
configured per handler, so two handlers dialing the same address with
different health_uri or health_headers share those counters and can push
each other over their own thresholds. Upstream is already per-handler and
already carries the active unhealthy flag, so the counters belong next to
it and the pool keeps its plain dial-address keys.

This drops the host key fingerprint and its exposure in the metric label
and the admin upstreams endpoint; the metric series identity is left for
separate consideration.

---------

Co-authored-by: SillyZir <269283839+SillyZir@users.noreply.github.com>
Co-authored-by: Zen Dodd <mail@steadytao.com>
2026-08-12 15:20:15 +10:00
1fed032234 caddyhttp: shield specific hostnames from a covering wildcard's client auth (#7920)
* 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>
2026-08-12 15:09:58 +10:00
Kévin Dunglas d6f7f18b04 Merge commit from fork
* fileserver: add failing test for calculateEtag collision

calculateEtag concatenates base36(mtime) and base36(size) with no
separator, so distinct (mtime, size) pairs can yield identical digit
strings and thus identical ETags.

* fileserver: prevent ETag collisions by separating mtime and size components

calculateEtag concatenated base36(mtime) and base36(size) with no
separator, so distinct (mtime, size) pairs could decode to the same
digit string and produce identical ETags.
2026-08-11 13:23:18 -06:00
Kévin Dunglas 947087cadd Merge commit from fork
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.
2026-08-11 08:00:15 -06:00
Julien Voisin 63f4e387c3 caddyhttp: allocate the request UUID lazily instead of on every request (#7936)
addHTTPVarsToReplacer ran SetVar(ctx, "uuid", new(requestID)) for every
request during replacer setup, allocating a *requestID unconditionally even
though it is only ever read by the {http.request.uuid} placeholder. The UUID
value itself was already generated lazily (requestID.String caches on first
call), but the container was not.

Allocate the *requestID on first access instead: the uuid placeholder now
creates and stores it in the request's vars table when absent, so repeated
references within a request still share one instance (and thus one UUID),
while requests that never reference the UUID pay nothing.

Add a benchmark for the per-request replacer setup and a regression test for
the uuid placeholder, which previously had no coverage.

Benchmark (BenchmarkAddHTTPVarsToReplacer, linux/arm64, count=6), common path
where the UUID is never referenced:

    before: ~700 ns/op   352 B/op   9 allocs/op
    after:  ~690 ns/op   336 B/op   8 allocs/op

One 16-byte allocation removed per request; a GC-pressure win rather than a
latency one.
2026-08-11 21:21:33 +10:00
Ben YounesandZen Dodd c290397cda caddyhttp: log recovered handler panics at ERROR level (#7924)
Co-authored-by: Zen Dodd <mail@steadytao.com>
Signed-off-by: Zen Dodd <mail@steadytao.com>
2026-08-10 02:18:17 -04:00
Julien Voisin 22e976e5ae reverseproxy: preallocate upstream slices with known sizes (#7926)
Preallocate the sources slice in MultiUpstreams.Provision and the
netAddrs slice in UpstreamResolver.ParseAddresses now that their final
lengths are known, avoiding the intermediate append regrowth
allocations. For a 4-address resolver this reduces ParseAddresses from
11 to 9 allocs/op and 528 to 384 B/op; the benefit scales with the
number of sources/addresses.

                                 |  before  |            after            |
                                 |  sec/op  |  sec/op     vs base         |
UpstreamResolverParseAddresses-8 | 2.289µ   |1.946µ     ~ (p=0.165) noisy |

B/op:      528 → 384   -27.27%  (p=0.000)
allocs/op:  11 →   9   -18.18%  (p=0.000)

Adds BenchmarkUpstreamResolverParseAddresses as per policy.
2026-08-10 15:51:50 +10:00
Julien Voisin 0476754858 reverseproxy: reduce allocation/CPU overhead in request hot paths (#7925)
Build the Via request and response headers with strconv-based
concatenation instead of fmt.Sprintf, avoiding fmt's reflection and
formatting overhead on every proxied request/response. Same single
allocation, ~40% faster for that construction in isolation.

                 | before (fmt) |            after (strconv)          |
                 |    sec/op    |    sec/op     vs base               |
PrepareRequest-8 | 6.136µ ± 11% | 4.491µ ± 33%  -26.80% (p=0.007 n=10)|

B/op:     1008 -> 1008   (unchanged)
allocs/op:  12 -> 12     (unchanged)

No behavior change. Adds BenchmarkPrepareRequest, as per policy, exercising the
request preparation path.
2026-08-10 15:49:55 +10:00
Renich Bon Ciric 546752e214 feat(fastcgi): populate SERVER_ADDR by default (#7912)
* feat(fastcgi): populate SERVER_ADDR by default

Populate the SERVER_ADDR FastCGI environment variable using the local socket address of the incoming connection from the request context. This improves out-of-the-box compatibility for PHP applications that rely on $_SERVER['SERVER_ADDR'] (such as legacy frameworks and custom telemetry packages) when migrating from Nginx or Apache.

Co-developed-by: Gemini AI <renich+gemini@woralelandia.com>
Signed-off-by: Rénich Bon Ćirić <renich@woralelandia.com>

* fix(fastcgi): validate SERVER_ADDR IP format and add test coverage

Validate that SERVER_ADDR is only populated when the local address parses as a valid IP address, preventing Unix socket paths from being assigned if SplitHostPort fails. Add test cases covering IPv4, IPv6, missing context, Unix sockets, and explicit overrides.

Co-developed-by: Gemini AI <renich+gemini@woralelandia.com>
Signed-off-by: Rénich Bon Ćirić <renich@woralelandia.com>
2026-08-10 15:24:13 +10:00
SillyZir e096ca9503 caddyauth: isolate provider responses to prevent cross-provider clobbering (#7904)
* caddyauth: isolate provider responses to prevent cross-provider clobbering

When multiple authentication providers are configured, each was handed the
real ResponseWriter, so a failing provider that wrote to the response (a
401 challenge or a login redirect) could clobber the response of another
provider or of the successful handler chain. Because provider map iteration
order is randomized, which provider's side effects won was nondeterministic.

A single provider now receives the real ResponseWriter unchanged — no
buffering, and Flusher/Hijacker/Pusher/ReaderFrom preserved exactly as
before. Only with multiple providers does each get its own buffered writer;
those writers embed caddyhttp.ResponseWriterWrapper so the underlying
capabilities remain type-assertable via http.ResponseController (Flush is
suppressed while buffering so a provider cannot prematurely commit the
response), and the buffered body is size-capped to avoid unbounded memory.

On success the winning provider's headers (e.g. a Set-Cookie) are copied to
the real writer and the chain proceeds. On total failure one provider's
challenge headers are applied (a redirect is sent as a full response),
otherwise the auth error is returned so handle_errors runs and a
header-only challenge (like basic auth) still returns 401.

Fixes #5190

* caddyauth: return total consumed bytes and drain error from ReadFrom

ReadFrom drained a source that exceeded the buffer cap but reported only
the bytes retained in the buffer and dropped any error from the drain,
violating the io.ReaderFrom contract: a caller such as io.Copy would see
fewer bytes than were actually consumed from the source, and a read
failure during the drain was silently swallowed. Return the
retained-plus-drained total and propagate the drain error.

* caddyauth: preserve Flusher and Hijacker on the buffered writer

The buffered writer used for multi-provider isolation embeds
caddyhttp.ResponseWriterWrapper, which promotes only Header, Write and
WriteHeader from the wrapped ResponseWriter and adds Push, ReadFrom and
Unwrap. Flush and Hijack were therefore reachable only through
http.ResponseController; a provider doing a plain w.(http.Flusher) or
w.(http.Hijacker) assertion silently lost them once a second provider was
configured.

Declare both on bufferedResponseWriter. Flush is a no-op so a provider
cannot prematurely commit a buffered response, and FlushError keeps the
same suppression for ResponseController, which prefers it over Flush.
Hijack delegates through the embedded wrapper, mirroring
responseRecorder.Hijack in the caddyhttp package.

The existing capability test only probed via http.ResponseController,
which is why this went unnoticed. It now asserts each capability both by
direct type assertion and through the controller, and uses two
non-authenticating providers so the probe is guaranteed to run — map
iteration order previously allowed a succeeding provider to break out of
the loop before the probe executed.

Also replace two header copy loops with maps.Copy, fixing the mapsloop
lint failures.
2026-08-01 12:19:27 +10:00
Christian Mehlmauer 2adc763020 fileserver: speed up directory browsing for large directories (#7903)
* improve fileserver performance and allocations
* skip instead of fatal
* fix comment
* implement more suggestions
* use filesystems.OsFS instead of os.DirFS
* fmt
* remove outdated benchmarks
2026-08-01 00:38:04 +10:00
df44d6c383 encode: flush headers immediately for server-sent events responses (#7905)
* encode: flush headers immediately for server-sent events responses

The encode middleware withholds the response header until the first body
write so it can sniff content-type and apply the minimum_length threshold.
For a text/event-stream response the upstream typically writes headers and
flushes to establish the event stream before any event body is available,
so the client never received the headers and the stream stalled; the same
buffering also delayed individual events.

When WriteHeader sees a text/event-stream content type, initialize encoding
and write the header through immediately. Forcing the header out also marks
the response as started, so subsequent event writes bypass the minimum_length
buffering and stream to the client as they arrive.

Fixes #6293

* encode: add WriteHeader benchmark covering SSE fast path

* encode: replace mime.ParseMediaType with bound-checked SSE check

WriteHeader runs an SSE Content-Type check on every call once headers
haven't been written yet. mime.ParseMediaType parses the full media
type, including parameters, even when nothing matches, which shows up
on the hot header-write path.

Replace it with a bound-checked manual prefix/boundary check (isSSE),
skipping parameter parsing for the common non-SSE case.

* encode: reject content types with junk after text/event-stream

isSSE accepted any suffix after a space, so a value like
"text/event-stream nonsense" was treated as an SSE response. After the
media type, skip optional whitespace and require either the end of the
value or a parameter separator. The check remains allocation-free, so
the hot-path motivation for the manual matcher is preserved.

---------

Co-authored-by: SillyZir <269283839+SillyZir@users.noreply.github.com>
Co-authored-by: Kévin Dunglas <kevin@les-tilleuls.coop>
2026-08-01 00:27:28 +10:00
Julien Voisinand@dunglas 323e3fe4b7 encode: reduce allocations in AcceptedEncodings (#7847)
AcceptedEncodings runs for every request when response compression is enabled,
so its allocations contribute directly to per-request garbage and GC pressure.
While the gains are modest, this cuts allocations by two-thirds and bytes
nearly in half on a hot path that executes on every compressed request. Fewer
allocations means less work for the garbage collector, which at high request
rates translates into lower GC frequency and steadier tail latency, not just
faster execution of this one function in isolation.

For each token in the Accept-Encoding header it called strings.Split(accepted,
";"), allocating a throwaway slice per encoding, and grew the prefs slice from
empty. This commit replaces the per-token Split with strings.Cut (zero
allocation) and presize prefs from the comma count.

As requested per the policy, this commit adds a benchmark to exercise the
change. With header "gzip, deflate, br;q=0.9, zstd;q=0.8":

AcceptedEncodings
- sec/op  1158.5n -> 771.8n  (-33.38%)
- B/op 408 -> 216  (-47.06%)
- allocs/op 9 -> 3  (-66.67%)

Co-authored-by: @dunglas
2026-08-01 00:23:57 +10:00
Kévin Dunglas 3be8dabc89 caddyhttp: use canonical header key casing to avoid re-canonicalization (#7911)
Header.Get/Set re-canonicalize and allocate whenever the passed key
isn't already in canonical MIME header form. Sec-WebSocket-Key,
WWW-Authenticate, and content-type all miss the fast path; switch to
their canonical forms (Sec-Websocket-Key, Www-Authenticate,
Content-Type).

Enable the canonicalheader linter to catch future regressions,
excluded in test files since those already use non-canonical casing
in several places without a perf-sensitive path behind them.
2026-07-30 20:40:35 +10:00
Saleh c96bca1269 rewrite: preserve non-canonical path encoding after uri replace (#7907)
changePath cleared RawPath whenever it equalled url.Path, which
discarded a valid non-canonical percent-encoding produced by a
replacement. This compare `RawPath` against the default escaping of Path
instead.
2026-07-26 08:49:44 +10:00
Salynn c5b66cf8ea Set the QUIC InitialPacketSize to 1200 bytes (#7886)
quic-go defaults the `InitialPacketSize` to 1280 bytes. This is used for
the size of the payload plus UDP header. When quic-go creates its
initial handshake packet, it pads it to this size as an optimization
for the anti-amplification limit (which does not really apply to
Caddy's server connections).

Tailscale uses an MTU of 1280 (the minimum IPv6 MTU) because it is a
tunnel operating in unknown environments, possibly inside other
tunnels.

When wrapped in an IP header (either v4 or v6), the 1280 byte QUIC packet
constructed by quic-go exceeds the Tailscale MTU and gets dropped. This
makes it impossible to respond to an HTTP3 connection attempt over
Tailscale when using the default `InitialPacketSize`.

We explicitly set the `InitialPacketSize` to 1200 (its minimum) to support
HTTP3 connections over Tailscale and other low MTU connections.

Once the connection is established, quic-go can perform MTU discovery to
increase the packet size to the maximum supported by the connection, so
any throughput loss due to the lowered `InitialPacketSize` is
short-lived.
2026-07-26 08:48:25 +10:00
futurehua 93c0721156 chore: fix sabotage spelling in nolint comments (#7892)
Signed-off-by: futurehua <futurehua@outlook.com>
2026-07-18 08:14:38 +00:00
TowyTowyandClaude Fable 5 b2be548275 reverseproxy: fix broken reservoir sampling in random_choose policy (#7873)
The random_choose selection policy is meant to implement
power-of-d-choices: sample d available upstreams uniformly, then pick
the least-loaded of that sample. The sampling loop, however, was not a
correct reservoir sample (Algorithm R): it never filled the first k
reservoir slots unconditionally, instead writing every candidate to a
random slot j = rand(i+1), which can evict an earlier candidate while
leaving another slot nil. It also derived j from the upstream's index
in the pool rather than from the number of available upstreams seen,
skewing the sample whenever unavailable upstreams precede available
ones.

As a result the reservoir frequently held fewer than min(k, available)
upstreams, so the least-load comparison often never happened. Most
notably, with two upstreams and 'random_choose 2' (the canonical
power-of-two-choices setup), half of all requests were routed to the
more-loaded upstream even when it was saturated and the other idle --
identical behavior to plain 'random'. The nil slots this leaves behind
were also the cause of the panic reported in #3810, which was patched
by skipping nils in leastRequests rather than by fixing the sampling.

Replace the loop with a standard Algorithm R reservoir sample over the
available upstreams: fill the first k slots, then replace a random slot
with probability k/n. Every available upstream is now sampled uniformly
and the reservoir always holds min(k, available) candidates.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 09:25:31 +10:00
techknowlogick b2693fb63a build(deps): bump cel-go from v0.28.1 to v0.29.2 (#7872) 2026-07-12 09:17:08 +10:00
TowyTowyandClaude Fable 5 c1907df2ed intercept: fix misleading handle_response error for extra matcher args (#7871)
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>
2026-07-12 09:16:34 +10:00
Ta Duc Thienandthientd c6180a0852 caddyhttp: fix path_regexp (MatchPathRE) Windows backslash bypass (#7858)
* caddyhttp: normalize Windows path in path_regexp matcher, shared with path matcher

Apply the same Windows path normalization to MatchPathRE that MatchPath already had, and factor it into a shared normalizeWindowsPath helper so both matchers use one implementation.

Also strip trailing dots and spaces per path component (not only at the end of the whole path), matching how Windows resolves paths; this fixes the same gap in the path matcher too. Adds regression tests for both matchers.

See #5613.

* caddyhttp: normalize trailing dots/spaces in escaped-path branch too

The MatchPath escaped-path branch (taken when a matcher pattern contains
'%') only folded backslash separators via windowsEscapedPathSeparatorRepl;
it skipped the per-component trailing dot/space normalization now applied
on the decoded branch. On Windows this left a bypass: a matcher such as
`path /private%2f*` could be evaded by GET /private.%5csecret.txt, since
`private.` and `private` resolve to the same directory on NTFS.

Add normalizeWindowsEscapedPath, which trims trailing dots and spaces —
literal ("." / " ") and percent-encoded ("%2e" / "%20") — from every
component in raw/escaped space, splitting on both '/' and encoded '%2f'
separators while preserving them, and leaving "." / ".." for CleanPath.
Regression tests cover the literal and percent-encoded variants against a
'%'-containing matcher.

---------

Co-authored-by: thientd <thien.taduc@ninhthanh.com>
2026-07-11 08:37:44 +10:00
WeidiDeng 1830809afe reverseproxy: save dial info in a context key instead of a variable key to avoid race conditions when forward_auth is used (#7859) 2026-07-10 09:37:07 -06:00
TowyTowyandClaude Fable 5 945d199724 reverseproxy: fix misleading handle_response error for extra matcher args (#7869)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 05:40:23 +00:00
alhuda 75c988d118 reverseproxy: compare sticky-session cookie hash in constant time (#7853) 2026-07-10 09:16:25 +10:00
alhuda 08ad064160 caddyhttp: fix escaped path matcher over-matching longer paths (#7828) 2026-07-08 04:29:44 +10:00
Kévin Dunglas 13a4c3f43c caddyhttp: add URL pattern request matcher (#7787)
* 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
2026-06-22 10:25:43 -06:00
yintaisha f450068460 http: normalize method names to uppercase in MatchMethod.Provision (#7832)
Signed-off-by: yintaisha <yintaishan@outlook.com>
2026-06-21 08:19:21 +00:00
Matthew Holt 5760382213 caddyhttp: Clean up variable scope in vars matcher 2026-06-20 13:45:10 -06:00
Matthew Holt 30f0ddd912 caddyhttp: Document dropping underscore headers 2026-06-20 13:45:10 -06:00
Matthew Holt 6ab855d3c4 browse: Update Caddy logo 2026-06-20 13:45:10 -06:00
wangdongyongandZen Dodd 69d6ace32e tracing: fix BatchSpanProcessor goroutine leak on config reload (#7826)
* tracing: fix BatchSpanProcessor goroutine leak on config reload

When the `tracing` directive is enabled, each config reload leaks one
`go.opentelemetry.io/otel/sdk/trace.(*batchSpanProcessor).processQueue`
goroutine. On a server that reloads frequently (e.g. polling a remote
config source every few seconds) this accumulates into tens of thousands
of leaked goroutines over time.

A goroutine dump shows many identical stacks:

```
goroutine ... [select]:
go.opentelemetry.io/otel/sdk/trace.(*batchSpanProcessor).processQueue(...)
	.../sdk/trace/batch_span_processor.go:327
go.opentelemetry.io/otel/sdk/trace.NewBatchSpanProcessor.func2()
	.../sdk/trace/batch_span_processor.go:129
created by go.opentelemetry.io/otel/sdk/trace.NewBatchSpanProcessor
	.../sdk/trace/batch_span_processor.go:127
```

`tracing` keeps a global, reference-counted `TracerProvider` so it can be
reused across reloads (`tracerProvider.getTracerProvider`). Caddy reloads
provision the new config *before* cleaning up the old one, so the counter
never drops to 0 and the provider is correctly reused — `Shutdown` is
never called, by design.

The problem is on the caller side in `newOpenTelemetryWrapper`:

```go
traceExporter, err := autoexport.NewSpanExporter(ctx)
...
tracerProvider := globalTracerProvider.getTracerProvider(
    sdktrace.WithBatcher(traceExporter),   // evaluated on every reload
    sdktrace.WithResource(res),
)
```

`sdktrace.WithBatcher(e)` is `WithSpanProcessor(NewBatchSpanProcessor(e))`,
and `NewBatchSpanProcessor` **starts its `processQueue` goroutine eagerly at
construction time** — not when the option is applied. The option is built
on every `Provision` (every reload), but `getTracerProvider` only applies
it when it actually creates a new provider (`t.tracerProvider == nil`). On
the reuse path the option is silently discarded, so the just-started
BatchSpanProcessor goroutine is orphaned: it is never registered with any
provider and therefore never shut down. Result: one leaked goroutine (plus
a leaked exporter) per reload.

Defer construction of the exporter/batcher until a new provider is actually
needed. `getTracerProvider` now takes a `buildOpts` factory that is invoked
only on the create path, so nothing with a side effect is built on the
reuse path.

The reference counter is now incremented only after the provider is
successfully obtained, preserving the previous semantics where a failed
exporter creation did not affect the counter.

- `Test_tracersProvider_buildOptsOnlyOnCreate` — asserts `buildOpts` runs
  exactly once across one create + five reuses (the regression guard).
- `Test_tracersProvider_buildOptsError` — asserts that on a build error the
  provider stays nil and the counter is not incremented.
- Existing tracing tests updated for the new signature and still pass.

Verified manually with a reload loop: before the fix, 50 reloads leaked 50
`processQueue` goroutines; after the fix, 0 are leaked while the provider is
still reused (counter stays at 1).

* add test for buldOpts error path

---------

Co-authored-by: Zen Dodd <mail@steadytao.com>
2026-06-18 11:42:58 -06:00
Saleh d2e0ad1e92 reverseproxy: log status 499 instead of 0 when client disconnects (#7827) 2026-06-18 13:31:02 +10:00
alhuda ae9bc028e6 rewrite: scope keyed query replace to its named key (#7818)
* rewrite: scope keyed query replace to its named key

* rewrite: cover keyed search_regexp query replace in test

* rewrite: provision query replace test via Provision path
2026-06-16 01:01:05 +10:00
Luccin Masirika 39c9a85f80 fileserver: append repeated hide subdirectives instead of overwriting (#7817)
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.
2026-06-13 22:44:19 +10:00
long.blackandClaude Sonnet 4.6 16235cced5 intercept: fix replace_status being silently dropped (#7810)
* 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>
2026-06-12 15:50:58 -06:00
alhuda 52dc6709fb rewrite: fix wrong index check in trimPathPrefix (#7812) 2026-06-12 14:11:21 -06:00
Dionis Ramadani 25b3eab6db Merge commit from fork
* fix(reverseproxy): hop-by-hop header fix

* test(headers): Added unit tests for hop-by-hope header behavior
2026-06-12 12:39:01 -06:00
Bluegate Studio fcba554d65 caddyhttp: New expected_underscore_headers server option (#7809)
* caddyhttp: restore allow_underscore_in_headers server option (#7808)

* caddyhttp: mark insecure_allow_underscore_in_headers as EXPERIMENTAL

* caddyhttp: replace underscore bool with expected_underscore_headers allowlist

* fix gofmt alignment in serveroptions.go

* caddyhttp: drop repeated allowlisted underscore headers

* caddyhttp: add tests for repeated-value drop and variant-drop logging
2026-06-11 22:26:21 -06:00
Sam Ottenhoff 997d3f6b0a encode: add standard benchmark and conformance harness (#7804)
This is a shared encode_test harness with HTML/JSON/JS/CSS payloads taken from caddyserver.com

Benchmarks:
- BenchmarkStandardEncodingPayloads: raw encoder NewEncoder/Write/Close path
- BenchmarkEncodeHandlerCorpus: full Encode.ServeHTTP middleware path
- Grid: 4 payloads × gzip levels 1/5/9 × zstd fastest/default/best
- Each subtest runs with 4 parallel workers; compare runs on MB/s and allocs/op

Conformance tests:
- Encoder contract: Reset, Flush, Close, and pool-style Reset-after-Close reuse
- Corpus HTTP encoding: Content-Encoding, Vary, ETag suffix, header stripping
- Response semantics: minimum_length, 304, HEAD, range, WebSocket bypass,
  If-None-Match rewrite, Cache-Control no-transform, content-type matcher rejection
2026-06-11 17:55:18 -06:00
Rhul 0f7f8e9cf6 forwardauth: error on duplicate uri subdirective (#7814) 2026-06-10 23:31:04 -04:00
Matthew Holt 4fd8c87f56 caddyhttp: Default max_header_bytes to 16 KiB 2026-06-09 12:07:27 -06:00