Commit Graph

2646 Commits (63f4e387c33e42864ad80ed704595299c3f8128b)

Author SHA1 Message Date
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 Younes 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
Rohit Behera 1880021e76
network_proxy: reject proxy URLs that resolve to a port with no host (#7922)
* network_proxy: reject proxy URLs that resolve to a port with no host

The host check after placeholder replacement had its arguments swapped:

    strings.Split("", pUrl.Host)[0] == ":"

This splits the empty string using pUrl.Host as the separator, so it
returns [""] and element 0 is always "", never ":". The comparison was
dead for every possible host value, which meant the "http://:80" case
named in the comment directly below it was never rejected. Such a URL
was handed back as the proxy, and the failure surfaced later as a
confusing dial error instead of the intended message.

Only the pUrl.Host == "" half of the condition ever did anything, so
"/some/path" was still caught.

Use url.URL.Hostname(), which returns the host with any port stripped
and is "" for exactly the two cases the comment describes -- ":80" and
"" -- while leaving IPv6 literals such as "[::1]:80" and userinfo forms
intact. That collapses both clauses into one expression.

Also adds tests for this function; the package previously had none.

* network_proxy: cover userinfo-only and IPv6 hosts in the tests

Differential-tested the old predicate against the new one across 36 URL
forms. Every behavioural change is in the same direction -- previously
accepted, now rejected -- and all of them are host-less. Nothing that was
rejected before is accepted now, and no legitimate host form changes.

Two of those forms were worth pinning down in the test:

  - "http://user:pass@:8080" parses with Host ":8080", so it is just as
    host-less as "http://:80". The comment in the source doesn't name
    this variant, but it was accepted before and is rejected now.

  - IPv6 literals are full of colons, so a repair that split Host on ":"
    rather than using Hostname() could plausibly reject them. Added
    "[::1]:8080" and "[2001:db8::1]" as regression guards; both are
    accepted before and after, which is the point.

On unfixed master the port-only and userinfo cases both fail; the IPv6
cases pass on both sides.
2026-08-10 15:58:43 +10: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
cui fliter 7d26d34ec6
caddyconfig: cancel HTTP loader requests with context (#7918) 2026-08-10 15:12:47 +10:00
Mohammed Al Sahaf 64b64c61eb
log: don't allow overwriting singly-assigned vals (#7927)
Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>
2026-08-08 00:59:12 +03: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
SillyZir 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 Voisin 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
SillyZir 54a6b8bc9e
caddyconfig: Register nested named-route invokes transitively (#7898) 2026-07-30 10:55:49 +00: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
dependabot[bot] ff6da1216b
build(deps): bump google.golang.org/grpc from 1.81.1 to 1.82.1 (#7908)
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.81.1 to 1.82.1.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.81.1...v1.82.1)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-version: 1.82.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-25 10:40:28 +00:00
Zen Dodd cba4f9aabb
deps: update GitHub Actions and Go modules (#7876)
Co-authored-by: Mohammed Al Sahaf <msaa1990@gmail.com>
2026-07-25 10:32:30 +00:00
Mohammed Al Sahaf 31daa220fc
log: support `d` (day) in `roll_interval` directive (#7900)
Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>
2026-07-22 01:39:33 +03:00
Herman Slatman 9738f49b95
pki: Handle error immediately after reading root from disk (#7896)
In https://github.com/caddyserver/caddy/pull/7057, the behavior
of `KeyPair.Load` was changed to return a certificate chain. While
the change was primarily meant for intermediates, it also affected
how a root (or roots) were loaded. The existing error handling
logic relied on a shared `err != nil` check, but with the new
behavior there's no guarantee that there's actually a root in
the PEM file on disk.

This commit handles the error immediately after reading the PEM
from disk.

Fixes: #7895
2026-07-19 23:59:46 -04: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
Pichu Chen 986753a7c6
logging: hash query parameter values in QueryFilter (#7884) 2026-07-15 16:38:58 +00:00
a 873fac5fc0
caddyfile: treat quoted braces as literal arguments (#7875) 2026-07-12 10:33:32 +10:00
TowyTowy 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
TowyTowy 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 Thien 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
TowyTowy 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
Kévin Dunglas 4e62095245
core: preserve metrics registry in Context.WithValue (#7861)
* core: preserve metrics registry in Context.WithValue

Context.WithValue rebuilt the Context without copying the unexported
metricsRegistry field, so any module provisioned under a context
derived via WithValue saw a nil registry from GetMetricsRegistry().

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Kévin Dunglas <kevin@dunglas.fr>

---------

Signed-off-by: Kévin Dunglas <kevin@dunglas.fr>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-08 04:52:36 +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 51a4bde176
Update human and agent contributing guidelines 2026-06-20 14:19:24 -06: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
wangdongyong 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
Matthew Holt ab56721a73
Remove -v from tests
Also add release notes to TODO for release proposal flow
2026-06-17 10:38:00 -06:00
Matt Holt 4dbe0a93be
readme: Update logo
Signed-off-by: Matt Holt <mholt@users.noreply.github.com>
2026-06-16 23:39:45 -06: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.black 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