Commit Graph
1704 Commits
Author SHA1 Message Date
Akinyemi Toluwalase 8818870dcd 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

(cherry picked from commit 71c5812cfe)
2026-11-03 00:53:19 -05:00
Sulaiman Dauda f13be23fce 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

(cherry picked from commit b058e3f3f6)
2026-11-03 00:53:19 -05:00
tomholford be3d2c6480 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>
(cherry picked from commit 258863f6f6)
2026-11-03 00:53:19 -05:00
Rob Martin 00f8e91c94 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.

(cherry picked from commit 62bc606433)
2026-11-03 00:53:19 -05:00
Aradhya Jain cf3781aab5 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.

(cherry picked from commit b1694e7a4c)
2026-11-03 00:53:19 -05:00
Adarsh Kumar 7165dca349 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>
(cherry picked from commit f355b8687d)
2026-11-03 00:53:19 -05:00
Mohamed A. Abdel Fattah ba340b7588 reverseproxy: strip hop-by-hop headers from interim responses (#8256)
(cherry picked from commit bfdbeb4a1b)
2026-11-03 00:53:19 -05:00
Jay Hemnani c31729a506 templates: use the earliest front matter closing fence (#8259)
(cherry picked from commit 8ba327ef3f)
2026-11-03 00:53:19 -05:00
Krish Singh Hura 89af42515e reverseproxy: preserve request trailer values (#8246)
(cherry picked from commit e91867d35c)
2026-11-03 00:53:19 -05:00
Jay Hemnani 43a4ec4d8a logging: Parse request URIs in the query filter like the server does (#8255)
(cherry picked from commit 612128e3b0)
2026-11-03 00:53:19 -05:00
awss a672362b56 caddytls: skip client cert verifiers when no certificate is presented (#8251)
(cherry picked from commit 872c864c20)
2026-11-03 00:53:19 -05:00
Jay Hemnani 3a23080b14 logging: Reject out-of-range ip_mask CIDRs (#8241)
(cherry picked from commit e3649f2536)
2026-11-03 00:53:19 -05:00
Jay Hemnani 2df2df13ca 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.

(cherry picked from commit 0584875297)
2026-11-03 00:53:18 -05:00
Jay Hemnani 483ecc8974 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.

(cherry picked from commit be5fba4ff2)
2026-11-03 00:53:18 -05:00
Jay Hemnani ea8d6359d6 logging: use the default file mode for dir_mode from_file (#8225)
(cherry picked from commit a6fc144e75)
2026-11-03 00:53:18 -05:00
小莳 268b5e574c templates: preserve colons in humanize time layouts (#8215)
(cherry picked from commit af7620dcc8)
2026-11-03 00:53:18 -05:00
pamnario c4d7fa9877 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>
(cherry picked from commit 4ec5a5c39a)
2026-11-03 00:53:18 -05:00
Shane f79eb0bfc1 fileserver: return an empty listing for offsets past the end (#8213)
Signed-off-by: Shane Jonas <jonas.shane@gmail.com>
(cherry picked from commit 69f22e2442)
2026-11-03 00:53:18 -05:00
Jay Hemnani 68787f244e 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.

(cherry picked from commit 1962b20401)
2026-11-03 00:53:18 -05:00
Jay Hemnani 909adb16dc 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.

(cherry picked from commit fc3d83400b)
2026-11-03 00:53:18 -05:00
a bded5b05e9 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.

(cherry picked from commit 401582cccf)
2026-11-03 00:53:18 -05:00
Francis Lavoie 7cc957cdfc 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

(cherry picked from commit d293dcfe86)
2026-11-03 00:53:10 -05:00
Jay Hemnani bf3fa59201 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.

(cherry picked from commit d4165a4eb1)
2026-11-03 00:53:04 -05:00
Jay Hemnani 5f7d725ae7 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.

(cherry picked from commit 5538b947d2)
2026-11-03 00:53:04 -05:00
小莳 6876b4f37a 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

(cherry picked from commit c31d9a71ed)
2026-11-03 00:53:04 -05:00
Jay Hemnani 970dc7cf00 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.

(cherry picked from commit 7c7456353e)
2026-11-03 00:53:04 -05:00
tbshfr 4c3e75075b 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

(cherry picked from commit 7910dd94ee)
2026-11-03 00:53:04 -05:00
Jay Hemnani d3fb27644c 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

(cherry picked from commit 093fd4b7ec)
2026-11-03 00:53:04 -05:00
Jay Hemnani 76b93ce173 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

(cherry picked from commit 519be12306)
2026-11-03 00:53:04 -05:00
zemamba 423c934902 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>
(cherry picked from commit 221ebc4503)
2026-11-03 00:53:03 -05:00
Jay Hemnani 4e6ad67427 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.

(cherry picked from commit 6727b2f3a9)
2026-11-03 00:53:03 -05:00
ammann 5f780a34b5 fileserver: apply hide rules to browse entries and sidecars (#8155)
(cherry picked from commit d65583e6d2)
2026-11-03 00:53:03 -05:00
Kévin Dunglas 6c99b20ad8 caddyhttp: let handler write deadlines cap the idle write timeout (#8145)
A handler setting a write deadline through http.ResponseController
reached the connection directly, and the next Write or Flush through
IdleTimeoutWriter replaced it with now+write_idle_timeout. Handlers
bounding a single write, like Mercure's dispatch_timeout, which drops
SSE subscribers that can't receive an update within 5s, were silently
extended to the 1 minute default. On HTTP/2, clearing between writes
also wiped the handler deadline.

IdleTimeoutWriter now implements SetWriteDeadline, keeps the handler
deadline as a ceiling for idle resets, and restores it instead of
HardDeadline alone when clearing between writes. A handler can
tighten the idle timeout but not loosen it.

(cherry picked from commit 9c44b6685e)
2026-11-03 00:53:03 -05:00
Jay Hemnani 053173babf reverseproxy: Don't panic in weighted_round_robin with fewer weights than upstreams (#8159)
(cherry picked from commit 8ae69fae1e)
2026-11-03 00:53:03 -05:00
Jay Hemnani 80b97a436d reverseproxy: Pick the least loaded host in random_choose when all are busy (#8160)
(cherry picked from commit 036895ed4b)
2026-11-03 00:53:03 -05:00
videah d971c26192 caddytls: Send ECH retry configs when rejecting ECH (#8142)
* caddytls: Send ECH retry configs when rejecting ECH
* chore: add licence header to ECH test

(cherry picked from commit 700d60327d)
2026-11-03 00:53:03 -05:00
awss 256315af0f fileserver: stat symlink targets by decoded path in browse listings (#8152)
directoryListing built each symlink's lookup path from the escaped
request path. In a directory whose name needs escaping, such as one with
a space, the stat failed, so directory symlinks were listed as files,
file symlinks showed the link's own size, and reveal_symlinks showed no
target.

Use the decoded path, which the function already computes for the page
title. The escaped path is still used for the page path and item URLs.

(cherry picked from commit 96eadf18f6)
2026-11-03 00:53:03 -05:00
Kévin Dunglas 96199dc4dc caddyhttp: document that negative idle timeouts disable them (#8146)
read_idle_timeout and write_idle_timeout default to 1 minute when zero,
and the wrappers are only installed for positive values, so a negative
value already disables them. Document it and cover it with tests, since
it is the only way to opt out, for instance behind a reverse proxy that
already protects from slow clients.

(cherry picked from commit c65c248336)
2026-11-03 00:53:03 -05:00
Francis Lavoie 53a2833af5 caddyhttp: only bound HTTP/2 body reads in flight with the idle timeout (#8125)
* caddyhttp: only bound HTTP/2 body reads in flight with the idle timeout

On HTTP/2, the read deadline is a timer that fails the request body
when it fires, even with no read in flight. IdleTimeoutReader left the
idle deadline armed between reads, so a handler that stopped reading
for longer than read_body_idle (1 minute by default) lost the body,
although the client was only held back by flow control. For example,
reverse_proxy uploading to an upstream that's slow to accept the body
failed with a 502.

This is the read side of #8118. Like IdleTimeoutWriter does since
read on HTTP/2, so the idle deadline only bounds reads in flight. A
client that stops sending is still cut off, since the deadline is set
before each read. HTTP/1 and HTTP/3 only check the deadline during a
read, so they're unchanged.

Written with Claude Code (Claude Opus 5.5); the human submitter is
responsible for reviewing it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* caddyhttp: track a released hard read deadline as armed

From review: with HardDeadline set, releasing the idle deadline between
reads left an HTTP/2 timer armed (at HardDeadline) while deadlineSet
recorded that none was, so clearDeadlineLocked would skip clearing it.

Track which deadline is armed (none, idle, or only the hard one) instead
of a bool, and have each check ask the question it means: re-arming and
releasing only apply to an armed idle deadline, while clearing applies
to any.

The writer's deadlineSet already only means "idle deadline armed",
since nothing clears the hard deadline it leaves; document that.

Written with Claude Code (Claude Opus 5.5); the human submitter is
responsible for reviewing it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* caddyhttp: clear released hard deadline at handler end, skip no-op sets

From review:

- After a partial read releases the idle deadline to HardDeadline, a
  handler that returns without reading again left that timer armed past
  HandlerDone, which only cleared after a terminal error. HandlerDone
  now also clears a released hard deadline.
- When the idle deadline is capped to HardDeadline, the reader and
  writer set the same deadline again when releasing it, and the writer
  once more in HandlerDone. On HTTP/2 each is a message to the serve
  loop. Both now remember the deadline they armed and skip setting it
  again. Within a request, only these wrappers set the client's
  deadlines.

Replaces the earlier test that faked a terminal error with the partial
read followed directly by HandlerDone.

Written with Claude Code (Claude Opus 5.5); the human submitter is
responsible for reviewing it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* caddyhttp: only skip re-setting an armed hard deadline

Skipping any deadline equal to the one already armed also skipped idle
deadlines on Windows, where two calls to time.Now in the same clock tick
return the same time, so successive idle deadlines can be equal. That
broke tests counting deadline calls there. Only skip HardDeadline, the
case from review where the idle deadline is capped to it; other
deadlines are always set.

Written with Claude Code (Claude Opus 5.5); the human submitter is
responsible for reviewing it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* caddyhttp: restore deadlines without trusting cached transport state

Always set the requested read and write deadlines because another handler can change them through its own response controller. Remove the armedAt shortcut and cover intervening deadline changes for reads, writes, ReadFrom, flushes, and HandlerDone.

AI-assisted with OpenAI Codex (GPT-6); tested under human direction.

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 30d3bd2a89)
2026-11-03 00:53:03 -05:00
Simon Sudarushkin 45e6d61cbb caddytls: add not matcher for TLS handshakes (#8138)
(cherry picked from commit 8507b63cf0)
2026-11-03 00:53:03 -05:00
Simon Sudarushkin d598c71d65 caddytls: keep repeated matchers in nested matcher sets (#8137)
(cherry picked from commit 1706dc8f72)
2026-11-03 00:52:58 -05:00
Saleh ea0a202294 reverseproxy: log HTTP/3 client cancellations at debug level (#8135)
[backport: test adapted to the 2.11 finalizeResponse signature, which
lacks the upstream address argument added by #7649]

(cherry picked from commit f7eeb1d65c)
2026-11-03 00:52:58 -05:00
klokv d7e49a55cd fileserver: treat a path below a regular file as not found (#8131)
Since #7017, every *fs.PathError from stat becomes fs.ErrInvalid, and
that includes ENOTDIR. So a request like /index.php/foo got 400 instead
of 404, and pass_thru no longer fell through to the next handler.

Map ENOTDIR to fs.ErrNotExist before that check. Other path errors, such
as NUL bytes, overly long names, or transient I/O failures, keep their
current handling.

(cherry picked from commit 7ba5e147ac)
2026-11-03 00:52:58 -05:00
Hitanshu 4b613dc425 encode: fix ETag list matching (#8128)
(cherry picked from commit 04fe161028)
2026-11-03 00:52:58 -05:00
Francis Lavoie 1282e5892b timeouts: take precedence over server-wide and enclosing idle timeouts (#8121)
* timeouts: take precedence over server-wide and enclosing idle timeouts

The timeouts handler wrapped the request body and response writer again,
on top of the server's own idle timeout wrappers. The wrapper closest to
the connection sets the deadline last, so the server-wide read_body_idle
and write_idle (on by default since 2.11.6) silently overrode the
route's settings. The same happened between nested timeouts handlers,
where the outer, less specific one won.

The server now makes its idle timeout wrappers available through the
request context, and the timeouts handler overrides their settings while
it runs, restoring them when it returns. It only wraps when there's
nothing to override, and then registers its own wrappers so that nested
handlers override those. The server-wide read_timeout and write_timeout
still cap the whole transfer. A handler that doesn't set
max_write_chunk keeps the server-wide one.

Written with Claude Code (Claude Opus 5.5); the human submitter is
responsible for reviewing it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* timeouts: re-arm deadlines on override and suspend outer min-rate window

From review:

- On HTTP/2, a read or write deadline is a timer that fires even with
  no read or write in flight. A deadline armed before a timeouts handler
  started kept the outer timeout and could fail the body (or reset the
  stream) inside a longer inner scope before the next read reset it.
  Override and restore now move an armed deadline to the settings that
  apply.
- Time spent under an override counted against the outer MinRate
  window, so the restored deadline could already have expired. The
  outer window is now suspended while overridden: its start moves
  forward by the time spent overridden, and bytes transferred in the
  meantime don't count towards it.

Adds an HTTP/2 read-before-inner-scope regression test and nested
minimum-rate coverage.

Written with Claude Code (Claude Opus 5.5); the human submitter is
responsible for reviewing it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 8f549e6df1)
2026-11-03 00:52:58 -05:00
72dd0fb067 timeouts: fix idle writer terminate h2 response writers between writes (#8119)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2026-10-03 01:06:18 -04:00
ab7e6b6d81 caddyhttp: end request-body deadline ownership with handler (#8107)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2026-10-03 02:10:58 +00:00
Hitanshu ac63faabb8 headers: Preserve separate Set-Cookie values (#8080) 2026-10-02 22:08:23 -04:00
Zen Dodd 55a95ad253 caddyhttp: keep absent optional placeholders empty (#8019) 2026-10-03 01:54:13 +00:00
Zen Dodd f357e2b6bf reverseproxy: stabilise half-close test (#8049) 2026-10-03 01:46:35 +00:00