Commit Graph

176 Commits (2.11)

Author SHA1 Message Date
D'Ali Ben Macha b101c6a9c8
reverseproxy: close upstream body when handle_response status is invalid (#8318)
When a handle_response status code did not evaluate to an integer, the
handler returned a 500 without closing the upstream response body,
leaking the connection.

(cherry picked from commit dccce45660)
2026-11-03 18:21:22 -05:00
Kévin Dunglas 4d38b78b4c
caddyhttp: optimise header access and fix encoding negotiation (#8025)
* perf: bypass header key normalization for canonical literals

http.Header's Get, Set, Add and Del run textproto.CanonicalMIMEHeaderKey
on every call. When the key is a canonical literal the result is the key
itself, so the scan is pure overhead. Access the map directly instead.

Only hot, allocation-free paths are converted, and only where the key is
a canonical literal. Call sites taking a key from config, a template or
the request itself still go through the normalizing methods, since they
need it for correctness.

allHeaderValues and lastHeaderValue dropped their http.CanonicalHeaderKey
call for the same reason: all callers pass canonical literals.

benchstat, Apple M5 Pro, go1.27.1:

    PrepareRequest-15      659.4n ± 2%  ->  457.7n ± 2%  -30.58% (p=0.000 n=12)
    AcceptedEncodings-15   330.8n ± 5%  ->  309.4n ± 7%   -6.44% (p=0.019 n=10)

    ResponseWriterWriteHeader/event-stream-15    1007.5n ± 3%  ->  732.5n ± 1%  -27.30%
    ResponseWriterWriteHeader/informational-15     60.0n ± 3%  ->   48.0n ± 2%  -20.01%
    ResponseWriterWriteHeader/not-modified-15     214.3n ± 6%  ->  175.3n ± 4%  -18.20%
    ResponseWriterWriteHeader/connect-15           60.2n ± 3%  ->   51.9n ± 4%  -13.85%
    ResponseWriterWriteHeader/html-15             206.5n ± 4%  ->  197.9n ± 2%   -4.19%
    geomean                                                                    -14.38%

Allocations are unchanged.

* tests: verify http.Header map keys are canonical

Indexing http.Header directly skips the canonicalization Get/Set/Add/Del
perform, which also puts the key outside what CI checks. canonicalheader
only inspects method calls. staticcheck's SA1008 covers reads, but stops
descending as soon as an assignment writes to a header, so it misses the
write, the delete builtin and the read-modify-write append. A key that is
not canonical in those positions is a silent lookup miss, not a compile
error.

Walk the loaded packages and verify every string constant used as a key in
an http.Header map operation matches http.CanonicalHeaderKey. A helper that
takes the key from its caller would hide the literal again, so a parameter
named with a canonical prefix opts its arguments into the same check.

The WebSocket headers Caddy deliberately writes with RFC 6455 casing are
allowed explicitly; they are only ever written out, never looked up against
a parsed header map.

Covers 64 operations, runs in under a second, and promotes golang.org/x/tools
from an indirect to a direct dependency at the version already in the module
graph.

* headercheck: resolve aliases of http.Header

* headercheck: check http.Header composite literal keys

* headercheck: note overlap with canonicalheader

* headercheck: drop parameter-name check

Helpers now take the indexed values, so every key sits in an index expression the analyzer and the linter already check.

* headercheck: scope RFC 6455 exceptions to their call sites

A key-based allow-list also accepted reads such as h["Sec-WebSocket-Key"], which miss because parsed headers are canonical. Honor //nolint:canonicalheader on the line instead, which the linter will use too.

* encode: read every Accept-Encoding field line

AcceptedEncodings read only the first field line, so a client that split
the list across several lines had the rest ignored. RFC 9110 section 5.3
treats repeated list-field lines as one comma-joined list. Iterate over
the items of every line instead of joining them, and skip empty items,
which used to yield an empty encoding. file_server's precompressed
selection uses the same function and is fixed with it.

Fixes https://github.com/caddyserver/caddy/issues/8272

Co-authored-by: Kağan Küçük <kaankck@gmail.com>

* encode: read every Cache-Control and Content-Encoding field line

Only the first field line was read, but RFC 9110 section 5.3 makes
multiple lines of a list-based field equivalent to one comma-joined
value:

- Cache-Control: no-transform on a later line was ignored, so the
  request or response was compressed anyway.
- Content-Encoding: an empty first line followed by a real coding let
  the handler encode the body a second time.

* encode: parse Cache-Control no-transform per RFC 9111

The substring match missed No-Transform, though RFC 9111 section 5.2 compares directive names case-insensitively, and fired on text that is not the directive: longer tokens such as x-no-transform-ext, quoted arguments such as community="no-transform", and semicolon-separated values, which are not valid Cache-Control syntax. Split the field on commas outside quoted strings and match only a well-formed no-transform element.

* encode: keep accepted encoding preferences on the stack

AcceptedEncodings counted commas to size the preference slice, and the variable capacity put it on the heap. A constant capacity covers typical Accept-Encoding headers and lets the compiler keep the slice on the stack; append still moves it to the heap for longer ones. BenchmarkAcceptedEncodings: 151.8ns -> 120.5ns (-21%), 192 -> 64 B/op, 2 -> 1 allocs/op (n=8).

* encode: build the encoded ETag without fmt

fmt.Sprintf boxed both arguments and parsed the format on every encoded response with a strong ETag. Concatenation: 47ns -> 16.5ns, 56 -> 24 B, 3 -> 1 allocs.

* reverseproxy: access response Server and Via headers directly

finalizeResponse runs on every proxied response; skip the key normalization as prepareRequest already does on the request side.

* encode: access headers directly in Write, WriteHeader and Close

These run once per response; convert the remaining literal-key Get, Set and Add calls like the rest of the handler.

* encode: match Cache-Control no-transform with httpguts

httpguts.HeaderValuesContainsToken already compares list elements case-insensitively across field lines, so the hand-written list parser is unnecessary. A no-transform inside a comma-separated quoted argument now also disables encoding, which only errs toward not transforming.

* encode: inline the no-transform check

isEncodeAllowed only wrapped one httpguts call. Its unit test now drives the response directive through ServeHTTP instead.

---------

Co-authored-by: Kağan Küçük <kaankck@gmail.com>
(cherry picked from commit 857d5731f3)
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
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 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
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
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
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
Kévin Dunglas 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
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
freya 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
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
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
Saleh d2e0ad1e92
reverseproxy: log status 499 instead of 0 when client disconnects (#7827) 2026-06-18 13:31:02 +10: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
WeidiDeng ad912569b5
reverseproxy: wraps request body to prevent closing if not read (#7719)
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2026-05-20 17:35:40 +00:00
James Hartig 77e9ce7404
reverseproxy: further prevent body closes from dial errors (#7715) 2026-05-12 12:05:50 -06:00
Matt Holt 4d6945769d
reverseproxy: Add ability to clear dynamic upstreams cache during retries (#7662)
* reverseproxy: Add ability to clear dynamic upstreams cache during retries

This is an optional interface for dynamic upstream modules to implement if they cache results.

TODO: More documentation; this is an experiment.

* Add some godoc

* Export interface; update godoc
2026-04-28 09:16:18 -06:00
Daniil Sivak aed1af5976
reverseproxy: add `lb_retry_match` condition on response status (#7569) 2026-04-21 14:59:31 -04:00
Zen Dodd ca0ca67fbd
reverseproxy: make stream copy buffer size configurable (#7627) 2026-04-10 14:49:32 -06:00
Tom Paulus a5e7c6e232
reverseproxy: prevent body close on dial-error retries (#7547) 2026-03-04 15:17:02 -05:00
Francis Lavoie db2986028f
reverseproxy: Track dynamic upstreams, enable passive healthchecking (#7539)
* reverseproxy: Track dynamic upstreams, enable passive healthchecking

* Add tests for dynamic upstream tracking, admin endpoint, health checks
2026-03-04 15:05:26 -05:00
Paulo Henrique 88616e86e6
api: Add all in-flight requests /reverse_proxy/upstreams (Fixes #7277) (#7517)
This refactors the initial approach in PR #7281, replacing the UsagePool
with a dedicated package-level sync.Map and atomic.Int64 to track
in-flight requests without global lock contention.

It also introduces a lookup map in the admin API to fix a potential
O(n^2) iteration over upstreams, ensuring that draining upstreams
are correctly exposed across config reloads without leaking memory.

Co-authored-by: Y.Horie <u5.horie@gmail.com>

reverseproxy: optimize in-flight tracking and admin API

- Replaced sync.RWMutex with sync.Map and atomic.Int64 to avoid lock contention under high RPS.
- Introduced a lookup map in the admin API to fix a potential O(n^2) iteration over upstreams.
2026-03-03 15:14:55 -07:00
WeidiDeng 2ab043b890
reverseproxy: query escape request urls when proxy protocol is enabled (#7537) 2026-03-02 02:04:06 -05:00
XYenon 03e6e439dd
reverseproxy: fix X-Forwarded-* headers for Unix socket requests (#7463)
When a request arrives via a Unix domain socket (RemoteAddr == "@"),
net.SplitHostPort fails, causing addForwardedHeaders to strip all
X-Forwarded-* headers even when the connection is trusted via
trusted_proxies_unix.

Handle Unix socket connections before parsing RemoteAddr: if untrusted,
strip headers for security; if trusted, let clientIP remain empty (no
peer IP for a Unix socket hop) and fall through to the shared header
logic, preserving the existing XFF chain without appending a spurious
entry.

Amp-Thread-ID: https://ampcode.com/threads/T-019c4225-a0ad-7283-ac56-e2c01eae1103

Co-authored-by: Amp <amp@ampcode.com>
2026-02-10 13:00:20 -07:00
Francis Lavoie 2ae0f7af69
reverseproxy: Set `Host` to `{upstream_hostport}` automatically if TLS (#7454) 2026-02-09 13:06:19 -07:00
Paulo Henrique 62134d65af
reverseproxy: fix error when remote address is not an IP (#7429) 2026-01-13 19:52:56 +00:00
Petr 67a9e0657e
reverseproxy: Fix retries for requests with bodies (#7360)
* capture the buffered body once, then reset clonedReq.Body before each retry

* no copy

* keep receiver name

* set the buf to nil after extraction and only return it to pool if not nil

---------

Co-authored-by: WeidiDeng <weidi_deng@icloud.com>
2025-11-24 12:03:18 -07:00
WeidiDeng a6da1acdc8
reverse_proxy: use interfaces to modify the behaviors of the transports (#7353) 2025-11-17 09:51:37 -07:00
WeidiDeng 8aca108d2c
reverseproxy: do not disable keepalive if proxy protocol is used (#7300) 2025-10-21 05:09:37 +00:00
Anthony Biondo 7fb39ec1e5
reverseproxy: Use http1.1 upgrade for websocket for extended connect of http2 and http3 (#7305)
Co-authored-by: WeidiDeng <weidi_deng@icloud.com>
2025-10-16 02:20:20 +00:00
WeidiDeng 2ec28bca43
reverse_proxy: use http1 for outbound tls requests with placeholder that are likely websockets (#7296) 2025-10-09 10:36:49 -06:00
joemicky 5125fbed41
use a more modern writing style to simplify code (#7182)
Signed-off-by: joemicky <joemickychang@outlook.com>
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2025-08-20 11:41:21 -06:00
Mohammed Al Sahaf e039a5bb5c
chore: upgrade .golangci.yml and workflow to v2 (#6924)
* chore: upgrade .golangci.yml and workflow to v2

run `golangci-lint fmt`

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

* run `golangci-lint run --fix`

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

* more lint fixes

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

* bring back comments to .golangci.yml

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

* appease the linter some more

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

* oops

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

* use embedded structs

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

* use embedded structs where they were used before

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

* disable rule  `-QF1006`

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

* missed a spot

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

---------

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>
2025-06-03 02:24:32 +03:00
Jesper Brix Rosenkilde 6c38ae7381
reverseproxy: Add valid Upstream to DialInfo in active health checks (#6949)
Currently if we extract the DialInfo from a Request Context during an active health check, then the Upstream in the DialInfo is nil.

This PR attempts to set the Upstream to a sensible value, based on wether or not the Upstream has been overriden in the active health check's config.
2025-04-15 08:44:53 -06:00
WeidiDeng 220cd1c2bc
reverseproxy: more comments about buffering and add new tests (#6778)
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2025-03-07 11:22:43 -07:00
Mohammed Al Sahaf 904a0fa368
reverse_proxy: re-add healthy upstreams metric (#6806)
* reverse_proxy: re-add healthy upstreams metric

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

* lint

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

---------

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>
2025-01-27 14:30:54 -07:00
Matthew Holt e7da3b267b
reverseproxy: Via header (#6275) 2025-01-17 06:49:01 -07:00
WeidiDeng 1bd567d7ad
reverseproxy: buffer requests for fastcgi by default (#6759)
* buffer requests for fastcgi by default

* fix import cycle

* fix the return value of bufferedBody

* more comments about fastcgi buffering

---------

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2025-01-02 11:18:25 -07:00
WeidiDeng c864b82ae1
reverseproxy: Set Content-Length when body is fully buffered (#6638) 2024-12-17 23:36:13 +00:00
bt90 328fb614f0
reverseproxy: Only handle websocket protocol (#6740) 2024-12-11 11:17:05 -07:00
WeidiDeng 9c0c71e577
reverseproxy: Rewrite requests and responses for websocket over http2 (#6567)
* reverse proxy: rewrite requests and responses for websocket over http2

* delete protocol pseudo-header

* modify cloned requests

* set request variable to track if it's a h2 websocket

* use request bodu

* rewrite request body

* use WebSocket instead of Websocket in the headers

* use logger check for zap loggers

* fix lint
2024-12-06 13:23:27 -07:00
Francis Lavoie 315715e90f
core: Implement FastAbs to avoid repeated os.Getwd calls (#6687)
* core: Implement FastAbs to avoid repeated os.Getwd calls

* Lint

* Rename files
2024-11-13 03:55:51 -05:00
Matthew Holt 238f1108e6
reverseproxy: Revert #4952 - don't ignore context cancellation in stream mode
i.e. Revert commit f5dce84a70

Two years ago, the patch in #4952 was a seemingly necessary way to fix an issue (sort of an edge case), but it broke other more common use cases (see #6666).

Now, as of #6669, it seems like the original issue can no longer be replicated, so we are reverting that patch, because it was incorrect anyway.

If it turns out the original issue returns, a more proper patch may be in #6669 (even if used as a baseline for a future fix). A potential future fix could be an opt-in setting.
2024-11-12 13:43:34 -07:00
Francis Lavoie 09b2cbcf4d
caddyhttp: Add `MatchWithError` to replace SetVar hack (#6596)
* caddyhttp: Add `MatchWithError` to replace SetVar hack

* Error in IP matchers on TLS handshake not complete

* Use MatchWithError everywhere possible

* Move implementations to MatchWithError versions

* Looser interface checking to allow fallback

* CEL factories can return RequestMatcherWithError

* Clarifying comment since it's subtle that an err is returned

* Return 425 Too Early status in IP matchers

* Keep AnyMatch signature the same for now

* Apparently Deprecated can't be all-uppercase to get IDE linting

* Linter
2024-11-04 23:18:50 +00:00
Francis Lavoie fbf0f4c425
reverseproxy: Sync changes from stdlib for 1xx handling (#6656)
* reverseproxy: Sync changes from stdlib for 1xx handling

Sourced from 960654be0c

* Use clear()

3bc28402fa
2024-10-22 12:10:46 -06:00
WeidiDeng 48ce47f1d4
reverseproxy: Use correct cases for websocket related headers (#6621)
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2024-10-11 09:02:23 +00:00
Kévin Dunglas f4bf4e0097
perf: use zap's Check() to prevent useless allocs (#6560)
* perf: use zap's Check() to prevent useless allocs

* fix

* fix

* fix

* fix

* restore previous replacer behavior

* fix linter
2024-09-13 11:16:37 -06:00