Commit Graph
1678 Commits
Author SHA1 Message Date
Jay Hemnani 519be12306 reverseproxy: Keep own headers after 1xx responses (#8169)
* reverseproxy: Keep own headers after 1xx responses

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

* reverseproxy: Use maps.Copy to restore own headers

* reverseproxy: Check the 103 reaches the client in the 1xx headers test
2026-10-09 16:42:26 +10:00
zemamba 221ebc4503 fix: preserve FastCGI dial errors for retry (#8172)
Wrap backend dial failures as reverseproxy.DialError so non-GET requests can be retried when no connection was made.

AI-assisted implementation and test; reviewed by zemamba.

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

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

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

Precompressed and ranged file responses could leave these headers on the
replacement error response.
2026-10-08 03:40:32 +00:00
ammann d65583e6d2 fileserver: apply hide rules to browse entries and sidecars (#8155) 2026-10-08 13:26:26 +10:00
Kévin Dunglas 9c44b6685e 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.
2026-10-08 13:11:27 +10:00
Jay Hemnani 8ae69fae1e reverseproxy: Don't panic in weighted_round_robin with fewer weights than upstreams (#8159) 2026-10-07 04:36:10 +00:00
Jay Hemnani 036895ed4b reverseproxy: Pick the least loaded host in random_choose when all are busy (#8160) 2026-10-07 00:25:43 -04:00
videah 700d60327d caddytls: Send ECH retry configs when rejecting ECH (#8142)
* caddytls: Send ECH retry configs when rejecting ECH
* chore: add licence header to ECH test
2026-10-06 08:54:39 +10:00
awss 96eadf18f6 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.
2026-10-05 22:42:50 +00:00
Kévin Dunglas c65c248336 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.
2026-10-06 08:23:01 +10:00
Francis LavoieandClaude Opus 5.5 30d3bd2a89 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>
2026-10-06 07:47:14 +10:00
Simon Sudarushkin 8507b63cf0 caddytls: add not matcher for TLS handshakes (#8138) 2026-10-04 17:22:21 -04:00
Francis Lavoie 8ad83ebd43 reverseproxy: Keep streams attached when writer detachment fails (#8139) 2026-10-04 12:46:34 -04:00
Simon Sudarushkin 1706dc8f72 caddytls: keep repeated matchers in nested matcher sets (#8137) 2026-10-04 16:13:35 +00:00
Saleh f7eeb1d65c reverseproxy: log HTTP/3 client cancellations at debug level (#8135) 2026-10-04 12:48:06 +00:00
klokv 7ba5e147ac 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.
2026-10-04 22:13:24 +10:00
Francis LavoieandWeidiDeng 7ef5c60ed0 reverseproxy: Optionally detach stream (websockets) from config lifecycle (#7649)
* reverseproxy: Optionally detach stream (websockets) from config lifecycle

* lint

* Improved logging facilities

* Adjustments from Weidi's review

* record bytes read and written for response writers unless detached

* simplify streaming handling

* correctly close detached streams

* make handleUpgradeTunnel a standalone func

* fix tests

* fix tests

* change the log level if hijacking without writing a status code first

* only clean up connections when stopped

* Move type and const down to the bottom

* Rename to tunnelTracker, reflow some comments

* Add note about capturing h

* Rename to stream_detached

* More comments

* reverseproxy: Preserve half-close behavior after rebase

Keep master half-close propagation and its unit coverage in the refactored tunnel lifecycle. Adapt response finalization tests to the selected upstream argument.

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

* reverseproxy: Fix detached stream cleanup and recorder chains

Snapshot the tracker registry before closing connections, and unregister empty trackers outside their mutex. Mark trackers stopped at cleanup entry so upstream removal closes detached streams even with stream_close_delay, while attached streams retain their delay.

Detach every supported response writer in the middleware chain so inner recorders do not retain state or count detached traffic. Add regressions for all four failures and an access-log/encode/intercept reload integration test.

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

---------

Co-authored-by: WeidiDeng <weidi_deng@icloud.com>
2026-10-04 07:45:10 -04:00
Hitanshu 04fe161028 encode: fix ETag list matching (#8128) 2026-10-03 23:38:05 +10:00
Francis LavoieandClaude Opus 5.5 8f549e6df1 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>
2026-10-03 22:18:42 +10: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
Kévin DunglasandZen Dodd 2f34b936f5 feat: implement the Incremental header field (RFC 10036) (#8020)
Co-authored-by: Zen Dodd <mail@steadytao.com>
2026-10-02 20:59:34 -04:00
Y.Horie 4978c54f81 events: tell CertMagic which events are worth emitting (#8010)
CertMagic assembles an event's data before emit() can look at OnEvent,
because Go evaluates arguments first, so tls_get_certificate costs a map
and a ClientHello copy on every handshake even when nothing will read
them. caddyserver/certmagic#406 added Config.ShouldEmitFunc so an
embedder can answer that question first.

Export App.ShouldEmit and pass it along with OnEvent. The predicate has
to account for every observer, not just subscribed handlers: Emit writes
a debug line for each event, so an event is observable whenever debug
logging is on. Emit now asks the same method it exports, so the two
cannot drift.

Benchmarks are included; measurements are in the pull request.
2026-10-03 01:03:20 +10:00
Matt HoltandClaude Code 0a137e2faf caddyhttp: use errors.AsType for request body limit errors (#8090)
With the Go 1.26 floor, golangci-lint's modernize analyzer (errorsastype)
flags the three errors.As calls that look for RequestBodyLimitError, which
turned the Lint workflow red on master after #8056. Replace them with the
generic errors.AsType; behavior is unchanged.

Assistance disclosure: this change was made by an AI agent (Claude Sonnet
5.5, model ID claude-sonnet-5-5) acting as Matthew Holt's agent and under
his approval. Build, go vet and the caddyhttp and caddyhttp/templates tests
pass locally; golangci-lint itself was not run locally.

Co-authored-by: Claude Code <noreply@anthropic.com>
2026-09-30 13:50:42 -06:00
Kévin Dunglas ff112bb230 caddyhttp: wait for servers from previous configs on exit (#8009)
Track pending HTTP server shutdowns across configuration reloads so
termination waits for responses still served by an older configuration.
Bound that wait by the terminating app's grace period and retain each
reload's grace context until its servers finish shutting down.

Cover HTTP/1.1, HTTP/2, and finite termination grace periods with
regression tests.
2026-09-30 11:15:18 -06:00
Zen Dodd 90ccea768f build: bump all dependencies - (Go 1.26 floor) (#8056)
* build: bump deps

* chore: note HTTP/2 migration follow-ups
2026-09-30 10:54:12 -06:00
gelsomino 256df3c8ad map: reject malformed destination placeholders (#8074)
Provision previously only checked for a single opening brace at the
start of a destination, so values like {result}suffix, {result, and
{result}} passed validation. strings.Trim(dest, "{}") then silently
stored a wrong placeholder name (e.g. result}suffix), making the
intended value unavailable.

Require the destination to be exactly one placeholder: a single
opening brace at the start, a single closing brace at the end, and a
non-empty name.

Fixes #8073
2026-09-28 22:19:51 +10:00
Hitanshu ea1e94b899 map: Distinguish duplicate literal and regexp inputs (#8067) 2026-09-26 23:33:19 +00:00
Abdellatif Anaflous 69ec5dfedf caddyauth: only replace known placeholders in basic auth credentials (#8017)
The basic auth provider expanded account usernames and passwords with
repl.ReplaceAll, which blanks any {...} the replacer does not recognize

Credentials are config data, so a username like alice-{bogus} was stored
as alice- and a password like ab{cd}ef was quietly rewritten into
different valid base64, turning a config mistake into a working
password the admin never set

Use ReplaceKnown, matching the respond headers fix in #8014 and the
header handler fix in #4880, so unknown braces survive and real
placeholders like {env.*} still expand
2026-09-26 18:52:53 +10:00
Abdellatif Anaflousandhktitof 81a0b6d838 requestbody: replace only known placeholders in the set body (#8008)
The set body is user-supplied and commonly JSON, but it was expanded with
ReplaceAll, which blanks any {...} the replacer does not recognize, so a
plain JSON body was sent empty and a body mixing JSON with a real
placeholder was mangled

Use ReplaceKnown, matching the respond body, so unrecognized braces are
left intact and real placeholders still expand. Regression test pins both
directions

Co-authored-by: hktitof <hktitof@users.noreply.github.com>
2026-09-26 18:50:06 +10:00
5493791423 caddyhttp: fix randString sameCase dictionary to match its docs (#7972)
* 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>

* Update modules/caddyhttp/errors.go

Signed-off-by: Matt Holt <mholt@users.noreply.github.com>

* elaborate the doc, and expand tests

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

* Update modules/caddyhttp/errors_randstring_test.go

Co-authored-by: Zen Dodd <mail@steadytao.com>
Signed-off-by: Mohammed Al Sahaf <mohammed@caffeinatedwonders.com>

---------

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>
Signed-off-by: Matt Holt <mholt@users.noreply.github.com>
Signed-off-by: Mohammed Al Sahaf <mohammed@caffeinatedwonders.com>
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
Co-authored-by: Zen Dodd <mail@steadytao.com>
2026-09-25 16:01:50 +00:00
WeidiDeng 89db4b75c3 reverseproxy: Fix partial response not flushed to clients (#7849)
* flush buffer before aborting
* add tests
* close test http server after tests

---------

Signed-off-by: WeidiDeng <weidi_deng@icloud.com>
2026-09-25 22:36:28 +10:00
Abdellatif Anaflous 4845150fa6 caddyhttp: surface 413 for oversized request body placeholders (#7969)
when a request exceeds the request_body max_size limit, the request_body handler wraps the http.MaxBytesError into a caddyhttp.HandlerError carrying status 413, but the {http.request.body} and {http.request.body_base64} placeholders ran io.Copy with the error ignored, so they silently returned the truncated prefix as though it were the complete body. docs promise a 413 for reads past max_size, and silently truncating is a bad failure mode in templates and vars_regexp where the body value drives decisions. see #7691 and the narrow follow-up prescribed when #7692 was closed

reading the body now returns a dedicated RequestBodyLimitError marker instead of a generic HandlerError, and only when the read failure is actually the max_size limit. the consumers (template placeholder function, vars and vars_regexp matchers) recognize exactly that marker and wrap it in a status carrying handler error so the oversized request fails with 413, while every unrelated error value keeps its old behavior: templates still render it as text and the vars matchers still match on its error text. the surfaced error is stripped of its generated id and stack trace so a placeholder stringified into a response body cannot leak the call stack

negative regressions prove an unrelated HandlerError in templates, vars and vars_regexp does not start controlling request handling, plus integration tests for the template and vars_regexp 413 paths

ai assisted (GLM agent) under Abdel's direction and local verification

Signed-off-by: Abdel <hktitof@gmail.com>
2026-09-25 22:27:19 +10:00
7ee4441f92 reverseproxy: Move websocket header normalization later (#7921)
* Move websocket header normalization later

A regression since #6621, these headers can be rewritten by cloneHeaders
and by .Add from user and transport ops. Moving this closer to where
it's round-tripped feels like the nicest solution.

I've put the harness I used to reproduce the issue at
  https://codeberg.org/nfreya/caddy-websocket-headers-repro

* Integration test for normalized websocket headers

---------

Co-authored-by: Zen Dodd <mail@steadytao.com>
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2026-09-23 14:09:48 -06:00
c798d4c238 fastcgi: explain the 411 a body without a length gets (#7956)
A request whose body has no length of its own — Transfer-Encoding: chunked, or
HTTP/2 and HTTP/3 requests sent without the header — cannot be forwarded over
FastCGI until it has been buffered in full, because CGI/1.1 requires
CONTENT_LENGTH and php-fpm hangs when it is absent or wrong and the body is not
empty. When request_buffers is too small to hold the whole body, the length
stays unknown and the request is refused with 411.

The refusal said none of that. RoundTrip passes r.ContentLength to Post, which
is -1 for such a request, so the operator got a bare "411 Length Required" with
either no error at all or strconv's "invalid syntax" on a value they never
wrote. On #7386 that sent two people looking for the fault in their backend.

The 411 now carries the reason and the remedy, and the unknown-length case is
told apart from a genuinely malformed value. ParseUint becomes ParseInt plus an
explicit negative check, which is strictly tighter: values above MaxInt64 used
to be accepted and are unreachable anyway, since CONTENT_LENGTH is always
FormatInt of an int64 by the time Do sees it.

No status code changes. Every request that was refused before is still refused;
raising request_buffers is still what makes a large chunked body work.

Tests: the reachable path through Post, the unusable CONTENT_LENGTH values Do
itself guards against, an integration test driving a chunked body through a
fastcgi reverse_proxy over a unix socket at each side of the buffer boundary
(including request_buffers -1, which succeeds at any size and is the remedy),
and a boundary test recording that a body exactly the size of the buffer counts
as partial.

That last one is deliberately not changed here. Telling "exactly the limit"
apart from "more to come" costs a read that a paused stream may never answer,
and bufferedBody also backs response_buffers: measured against a body that
delivers exactly the limit and stops, the current code hands the buffered
prefix on immediately, while peeking one byte first delivers nothing at all.

Co-authored-by: Aditya <205600203+Rohilalala@users.noreply.github.com>
Co-authored-by: Zen Dodd <mail@steadytao.com>
2026-09-23 09:31:54 -06:00
Timo StarkandFaiyaz Rahman c18099a0af reverse_proxy with versions 3 silently ignores tls_trust_pool and verifies against the system roots (#8042)
* reverseproxy: build TLS client config once so HTTP/3 keeps tls_trust_pool

reuse the config built for the HTTP/1.1+2 transport and give the HTTP/3
transport a clone of it to honour the tls_trust_pool

Signed-off-by: Timo Stark <tippexs91@googlemail.com>
Co-authored-by: Faiyaz Rahman <faiyazrahman03@gmail.com>

* Adding issue reference

---------

Signed-off-by: Timo Stark <tippexs91@googlemail.com>
Co-authored-by: Faiyaz Rahman <faiyazrahman03@gmail.com>
2026-09-22 08:13:02 +10:00
techknowlogick df77f8bde1 chore(deps): bump cel-go and switch import to new url (#8030) 2026-09-17 04:09:23 +03:00
Turhan ACAR 9be8fb2794 reverseproxy: propagate TCP half-close on upgraded streams (#8027)
* reverseproxy: propagate TCP half-close on upgraded streams

handleUpgradeResponse starts one copy goroutine per direction but returns as
soon as the first one reports a result, and returning runs two unconditional
deferred closes (the client connection and the backend connection). A client
that ends its send direction with a TCP half-close therefore causes the whole
tunnel to be torn down: the backend connection is reset mid-conversation and
whatever it was about to send is never delivered.

Treat a clean EOF as the end of one direction rather than the end of the
tunnel. When a copy finishes without error, propagate the half-close to the
destination with CloseWrite where the connection supports it, and keep waiting
for the other direction so pending bytes can still drain. Real copy errors, the
stream timeout and request cancellation/shutdown still tear down immediately.

If the destination does not support CloseWrite, the peer cannot be told that
the direction ended, so the previous behavior is kept and the tunnel is torn
down rather than held open until the stream timeout.

This is the same class that was fixed upstream in net/http/httputil
(https://go.dev/issue/35892); Caddy has its own upgraded-stream copier, so the
standard library fix does not apply here.

Fixes #8026

* reverseproxy: follow net/http/httputil shape for the half-close

Replace the copyResult struct and the closeWriter/closeWrite helpers with
upstream's plain error channel and errCopyDone sentinel, propagating
CloseWrite inside the copier, as requested in review.

Behaviour is unchanged. The wait is repeated twice inside the existing
select because handleUpgradeResponse also selects on the stream timeout
and sizes the channel at 2 so both goroutines can exit when it fires
(#7418), which net/http/httputil has no equivalent of.

* reverseproxy: explain the repeated wait next to it

Move the reasoning for why the wait is repeated inside the select, rather
than written as upstream's two-line form, into a comment beside the loop
so a future reader does not have to find the review discussion.

* reverseproxy: add an upgraded-stream half-close integration test

Drive a real upgraded stream through the handler and check that closing one
direction is propagated to the other side instead of tearing the tunnel down.

Mirrors net/http/httputil's TestReverseProxyWebSocketHalfTCP, which covers the
same class upstream (https://go.dev/issue/35892): a backend that hijacks and
hands back a *net.TCPConn, the handler in front of it, and a client that dials
it directly, then the four close-read / close-write combinations.

Against the unfixed handler the two half-close cases fail - the surviving
direction never delivers its pending bytes - while the two close-read cases
pass, so the test isolates exactly the reported behaviour.

* reverseproxy: give the half-close test reads a deadline

Without one, a regression blocks in ReadFull until the package timeout
instead of failing the test, which is the flakiness an end-to-end test of
this kind is usually accused of. Ten seconds is far above any real latency
here and turns a hang into a clear failure.

* reverseproxy: hijack the test backend via http.NewResponseController

Matches how the rest of the tree reaches Hijack (responsewriter.go,
caddyauth.go, streaming.go itself) and keeps working if the writer is
wrapped, where a direct http.Hijacker assertion would not.
2026-09-16 10:10:24 +00:00
Zen Dodd ef1877210e Merge commit from fork
* caddyhttp: canonicalise paths after prefix stripping

* caddyhttp: cover suffix stripping in path canonicalisation
2026-09-14 12:50:44 -06:00
Abdellatif Anaflous 56e3a88efe caddyhttp: only replace known placeholders in respond headers (#8014)
The respond directive expanded header field names and values with
repl.ReplaceAll, which blanks any {...} the replacer does not recognize

Header values are config data, often JSON or carrying literal braces, so a
value like {key:value} was sent empty and a-{b}-c came out a--c

Use ReplaceKnown, matching the header handler fix in #4880 (same class as
#4418) and the respond body expansion a few lines below, so unknown braces
survive and real placeholders still expand
2026-09-13 12:08:21 +10:00
Faiyaz Rahman 425a3381fd acmeserver: say when the CA database is locked by another process (#8007)
Opening the bbolt database only reported timeout, which sounds like a
broken config. Catch bolt.ErrTimeout with errors.Is and say another
process holds the lock, and that caddy reload is the right command when
Caddy is already running. The error stays fatal.
2026-09-11 14:49:31 +10:00
Islam Elsayed 56ae39bdcc reverseproxy: replace only known placeholders in health check body (#8003)
The active health check body is user-supplied and commonly JSON, but it
was expanded with ReplaceAll, which blanks any `{...}` the replacer does
not recognize. Only globals are available here, so a JSON
health_request_body was sent as an empty body.

This is not limited to bodies without placeholders. A body that does use
one is mangled too: `{"token":"{env.SOME_VAR}"}` is sent as `"}`, since
the opening `{"token":"` is consumed as an unrecognized placeholder
before the real one is reached.

Use ReplaceKnown, matching the header values a few lines below, so
unrecognized braces are left intact and real placeholders still expand.

Only the body is changed. The Host header on the same path keeps
ReplaceAll deliberately: Go's HTTP server blanks a Host containing
braces, so leaving `{...}` intact there would send a Host the upstream
discards, where blanking it lets Go fall back to the URL host.
2026-09-09 08:48:08 -06:00
Y.Horie 769ed7e4a3 events: skip dispatch setup when nothing is subscribed (#7997)
App.Emit does a fair amount of work before it can discover that no handler
is bound: it derives three loggers, one of which formats the event's UUID
even when debug logging is off, and registers a replacer callback. Only
then does it reach "shortcut if event not bound at all".

Some events are emitted on every TLS handshake -- CertMagic emits
tls_get_certificate as the first statement of GetCertificateWithContext --
so on a server with no events configuration that work runs per handshake
and is discarded every time.

Return early when neither the event's name nor the catch-all is bound and
debug logging is off, which are exactly the conditions under which nothing
can observe the event. caddy.NewEvent still runs, so the returned Event is
unchanged for callers.

Benchmarks are included; measurements are in the pull request.
2026-09-09 13:33:54 +10:00
Jens-Uwe Mager c8f0667aee caddyhttp: demote Alt-Svc ErrNoAltSvcPort to debug level (#8000) 2026-09-08 13:26:18 +10:00
Jens-Uwe MagerandZen Dodd 8626fa3703 caddytls: synchronize storage cleaner with TLS.Stop via context and WaitGroup (#7954)
* caddytls: synchronize storage cleaner with TLS.Stop via context and WaitGroup

* Unify the storage cleanup synchronization to both tls and ech.

* Cannot embed the sync.WaitGroup directly as the TLS struct is copied.

* caddytls: propagate cancellable context to ECH rotation and add sync tests

Pass cancellable context to ECH key rotation so in-flight storage locks
and operations unblock when TLS.Stop is invoked. Add comprehensive tests
verifying TLS.Stop cleanly unblocks and waits for storage cleaner and
ECH workers.

---------

Co-authored-by: Zen Dodd <mail@steadytao.com>
2026-09-05 07:37:57 -06:00
Faiyaz RahmanandEugin Francis 8e90c9a0e1 caddyhttp: surface write timeout errors in access log (#7945)
Co-authored-by: Eugin Francis <13235453+euginfrancis@users.noreply.github.com>
Signed-off-by: Faiyaz Rahman <faiyazrahman03@gmail.com>
2026-09-04 16:56:07 -04:00
Sash 97a08d8251 fileserver: reject non-integer browse file_limit (#7988) 2026-09-04 16:37:02 +10:00