Merge branch 'origin/main' into 'next-release/main'

This commit is contained in:
oauth
2026-06-16 13:56:30 +00:00
14 changed files with 367 additions and 63 deletions

2
go.mod
View File

@@ -20,7 +20,7 @@ require (
github.com/dutchcoders/go-clamd v0.0.0-20170520113014-b970184f4d9e
github.com/gabriel-vasile/mimetype v1.4.13
github.com/ggwhite/go-masker v1.1.0
github.com/go-chi/chi/v5 v5.2.5
github.com/go-chi/chi/v5 v5.3.0
github.com/go-chi/render v1.0.3
github.com/go-jose/go-jose/v3 v3.0.5
github.com/go-ldap/ldap/v3 v3.4.13

4
go.sum
View File

@@ -375,8 +375,8 @@ github.com/go-asn1-ber/asn1-ber v1.4.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkPro
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
github.com/go-chi/chi/v5 v5.3.0 h1:halUjDxhshgXHMrao5bB8eNBXo/rnzwr8m5m36glehM=
github.com/go-chi/chi/v5 v5.3.0/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s=

View File

@@ -87,7 +87,7 @@ func main() {
// A good base middleware stack
r.Use(middleware.RequestID)
r.Use(middleware.RealIP)
r.Use(middleware.ClientIPFromRemoteAddr) // pick one ClientIPFrom* based on your infra, see below
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
@@ -349,7 +349,11 @@ with `net/http` can be used with chi's mux.
| [Logger] | Logs the start and end of each request with the elapsed processing time |
| [NoCache] | Sets response headers to prevent clients from caching |
| [Profiler] | Easily attach net/http/pprof to your routers |
| [RealIP] | Sets a http.Request's RemoteAddr to either X-Real-IP or X-Forwarded-For |
| [ClientIPFromHeader] | Capture client IP from a trusted single-IP header (X-Real-IP, CF-Connecting-IP, ...) |
| [ClientIPFromXFF] | Capture client IP from X-Forwarded-For, skipping listed trusted CIDR prefixes |
| [ClientIPFromXFFTrustedProxies] | Capture client IP from X-Forwarded-For given a fixed number of trusted proxies |
| [ClientIPFromRemoteAddr] | Capture client IP from the TCP RemoteAddr (server directly on the public internet) |
| [RealIP] | Deprecated — vulnerable to IP spoofing; use [ClientIPFromXFF] or another ClientIPFrom\* middleware |
| [Recoverer] | Gracefully absorb panics and prints the stack trace |
| [RequestID] | Injects a request ID into the context of each request |
| [RedirectSlashes] | Redirect slashes on routing paths |
@@ -375,6 +379,12 @@ with `net/http` can be used with chi's mux.
[Logger]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Logger
[NoCache]: https://pkg.go.dev/github.com/go-chi/chi/middleware#NoCache
[Profiler]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Profiler
[ClientIPFromHeader]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ClientIPFromHeader
[ClientIPFromXFF]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ClientIPFromXFF
[ClientIPFromXFFTrustedProxies]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ClientIPFromXFFTrustedProxies
[ClientIPFromRemoteAddr]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ClientIPFromRemoteAddr
[GetClientIP]: https://pkg.go.dev/github.com/go-chi/chi/middleware#GetClientIP
[GetClientIPAddr]: https://pkg.go.dev/github.com/go-chi/chi/middleware#GetClientIPAddr
[RealIP]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RealIP
[Recoverer]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Recoverer
[RedirectSlashes]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RedirectSlashes
@@ -402,6 +412,62 @@ with `net/http` can be used with chi's mux.
[ThrottleOpts]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ThrottleOpts
[WrapResponseWriter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#WrapResponseWriter
### Choosing a ClientIP middleware
The legacy [RealIP] middleware is deprecated — it is vulnerable to IP spoofing
(GHSA-3fxj-6jh8-hvhx, GHSA-rjr7-jggh-pgcp, GHSA-9g5q-2w5x-hmxf) and mutates
`r.RemoteAddr`. Use one of the four `ClientIPFrom*` middlewares instead — pick
exactly one based on your network setup — and read the resulting IP with
[GetClientIP] (string) or [GetClientIPAddr] (`netip.Addr`):
| Your setup | Use |
|---|---|
| Directly on the public internet, no proxy | `middleware.ClientIPFromRemoteAddr` |
| Behind nginx (`X-Real-IP`), Cloudflare (`CF-Connecting-IP`), Apache (`X-Client-IP`) | `middleware.ClientIPFromHeader("<your-trusted-header>")` |
| Behind one or more proxies whose IP ranges you can list | `middleware.ClientIPFromXFF("10.0.0.0/8", ...)` |
| Behind a known, fixed number of proxies with dynamic IPs | `middleware.ClientIPFromXFFTrustedProxies(2)` |
```go
r := chi.NewRouter()
r.Use(middleware.RequestID)
// Pick exactly one. Examples for common deployments:
// Direct internet exposure (no proxy):
// r.Use(middleware.ClientIPFromRemoteAddr)
// Behind Cloudflare:
// r.Use(middleware.ClientIPFromHeader("CF-Connecting-IP"))
// Behind AWS CloudFront (or any proxy fleet with known CIDRs):
r.Use(middleware.ClientIPFromXFF(
"13.32.0.0/15", // CloudFront IPv4
"52.46.0.0/18", // CloudFront IPv4
"2600:9000::/28", // CloudFront IPv6
))
// Behind a known number of proxies with dynamic IPs:
// r.Use(middleware.ClientIPFromXFFTrustedProxies(2))
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
clientIP := middleware.GetClientIP(r.Context()) // for logs, rate-limit keys, etc.
_ = clientIP
})
```
These middlewares never mutate `r.RemoteAddr`. They store a normalized
`netip.Addr` in the request context — IPv4-mapped IPv6 (`::ffff:a.b.c.d`)
is folded to plain IPv4, and IPv6 zone identifiers carried in headers are
stripped, so one logical client maps to a single canonical key for logs,
rate limits, and ACLs.
See the per-function godoc for the full semantics of each middleware, and
[adam-p's "The perils of the 'real' client IP"](https://adam-p.ca/blog/2022/03/x-forwarded-for/)
for the underlying threat model.
### Extra middlewares & packages
Please see https://github.com/go-chi for additional packages.

View File

@@ -77,7 +77,7 @@ type Router interface {
// path, with a fresh middleware stack for the inline-Router.
Group(fn func(r Router)) Router
// Route mounts a sub-Router along a `pattern`` string.
// Route mounts a sub-Router along a `pattern` string.
Route(pattern string, fn func(r Router)) Router
// Mount attaches another http.Handler along ./pattern/*

263
vendor/github.com/go-chi/chi/v5/middleware/client_ip.go generated vendored Normal file
View File

@@ -0,0 +1,263 @@
package middleware
import (
"context"
"net"
"net/http"
"net/netip"
"strings"
)
// clientIPCtxKey stores the client IP set by any of the ClientIPFrom* middlewares.
var clientIPCtxKey = &contextKey{"clientIP"}
// xForwardedForHeader is the canonical form of the X-Forwarded-For header
// name, used by the XFF-based middlewares.
const xForwardedForHeader = "X-Forwarded-For"
// ClientIPFromHeader stores the client IP from a single-IP header set by
// your reverse proxy. Read it with [GetClientIP].
//
// Only safe with headers your proxy unconditionally OVERWRITES on every
// request, e.g.:
//
// - X-Real-IP — Nginx with ngx_http_realip_module
// - X-Client-IP — Apache with mod_remoteip
// - CF-Connecting-IP — Cloudflare
//
// True-Client-IP, X-Azure-ClientIP, and Fastly-Client-IP look similar but
// pass through from the client by default in those products; don't use them
// unless your edge strips the inbound value.
//
// If the header reaches us with multiple values (misconfigured proxy that
// appends, or a downstream proxy not stripping a client-supplied value),
// the LAST value wins — that's the one set by the hop closest to us, and
// therefore the most trusted. Fail-closed if the last value doesn't parse:
// no client IP is set rather than falling back to earlier (less-trusted)
// values.
//
// v4-mapped IPv6 (::ffff:a.b.c.d) folds to plain v4 and IPv6 zones are
// stripped before storage.
func ClientIPFromHeader(trustedHeader string) func(http.Handler) http.Handler {
header := http.CanonicalHeaderKey(trustedHeader)
return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
values := r.Header.Values(header)
if len(values) > 0 {
if ip, ok := parseHeaderAddr(values[len(values)-1]); ok {
r = r.WithContext(context.WithValue(r.Context(), clientIPCtxKey, ip))
}
}
h.ServeHTTP(w, r)
})
}
}
// ClientIPFromXFF stores the client IP read from the X-Forwarded-For header,
// walking the chain right-to-left and skipping any IP that falls within one
// of the given trusted CIDR prefixes. The first IP that is not trusted is
// the client. Read it with [GetClientIP].
//
// An unparseable entry mid-chain aborts the walk and leaves no client IP
// set (fail-closed) — we can't safely trust anything left of garbage.
//
// Use this when you sit behind one or more reverse proxies whose IP ranges
// you can enumerate as CIDRs:
//
// r.Use(middleware.ClientIPFromXFF(
// "13.32.0.0/15", // CloudFront IPv4
// "52.46.0.0/18", // CloudFront IPv4
// "2600:9000::/28", // CloudFront IPv6
// ))
//
// Calling with no arguments returns the rightmost XFF entry, or no IP if
// that entry doesn't parse (fail-closed) — safe only if you have exactly
// one trusted hop directly in front of this server (e.g., nginx on localhost).
//
// v4-mapped IPv6 (::ffff:a.b.c.d) folds to plain v4 and IPv6 zones are
// stripped before the prefix check and storage; otherwise an attacker
// could use either notation to alias a trusted IP past the check.
//
// If you know the number of trusted proxies but not their IPs, use
// [ClientIPFromXFFTrustedProxies] instead.
//
// Panics at startup if any prefix is invalid.
func ClientIPFromXFF(trustedIPPrefixes ...string) func(http.Handler) http.Handler {
prefixes := make([]netip.Prefix, len(trustedIPPrefixes))
for i, p := range trustedIPPrefixes {
prefixes[i] = netip.MustParsePrefix(p)
}
return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var found netip.Addr
walkXFF(r.Header[xForwardedForHeader], func(v string) bool {
ip, ok := parseHeaderAddr(v)
if !ok {
return true // fail-closed; leave found unset
}
if inAnyPrefix(ip, prefixes) {
return false // trusted hop; keep walking left
}
found = ip
return true
})
if found.IsValid() {
r = r.WithContext(context.WithValue(r.Context(), clientIPCtxKey, found))
}
h.ServeHTTP(w, r)
})
}
}
// ClientIPFromXFFTrustedProxies stores the client IP read from the
// X-Forwarded-For header, given the exact number of trusted reverse proxies
// between this server and the public internet. It returns the IP at position
// len(xff) - numTrustedProxies in the merged X-Forwarded-For list — the IP
// added by the outermost of your trusted proxies, the only IP in the chain
// that none of your proxies have allowed an attacker to forge. Read it with
// [GetClientIP].
//
// Use this when:
// - You know exactly how many proxies you sit behind, AND
// - Their IP addresses are dynamic (autoscaling proxy pools, ephemeral
// containers, dynamic CDN edges) so listing CIDRs with [ClientIPFromXFF]
// is impractical.
//
// WARNING: This variant is brittle to network architecture changes. If you
// add or remove a proxy level, numTrustedProxies silently becomes wrong and
// you may start trusting an attacker-supplied IP. Prefer [ClientIPFromXFF]
// with explicit trusted CIDRs whenever you can.
//
// If the XFF chain has fewer than numTrustedProxies entries (header missing
// or architecture changed), no client IP is set and [GetClientIP] returns "".
//
// Like [ClientIPFromXFF], v4-mapped IPv6 folds to plain v4 and IPv6 zones
// are stripped before storage.
//
// Panics at startup if numTrustedProxies < 1.
func ClientIPFromXFFTrustedProxies(numTrustedProxies int) func(http.Handler) http.Handler {
if numTrustedProxies < 1 {
panic("middleware.ClientIPFromXFFTrustedProxies: numTrustedProxies must be >= 1")
}
return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
n := numTrustedProxies
var entry string
walkXFF(r.Header[xForwardedForHeader], func(v string) bool {
n--
if n == 0 {
entry = v
return true
}
return false
})
if entry != "" {
if ip, ok := parseHeaderAddr(entry); ok {
r = r.WithContext(context.WithValue(r.Context(), clientIPCtxKey, ip))
}
}
h.ServeHTTP(w, r)
})
}
}
// ClientIPFromRemoteAddr stores the client IP read from the TCP RemoteAddr
// of the incoming request — the IP address of whoever opened the connection
// to this server. Read it with [GetClientIP].
//
// Use this when this server is directly connected to the public internet
// with NO reverse proxy in front of it. Behind a reverse proxy, RemoteAddr
// is the proxy's IP, not the client's — use [ClientIPFromHeader] or
// [ClientIPFromXFF] instead.
//
// IPv4 clients on a dual-stack listener surface as ::ffff:a.b.c.d; they
// fold to plain v4 before storage so one logical client maps to one key.
// IPv6 zones are preserved (link-local connections may legitimately have one).
func ClientIPFromRemoteAddr(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
host = r.RemoteAddr // RemoteAddr may already be a bare IP (e.g. in tests).
}
if ip, err := netip.ParseAddr(host); err == nil {
r = r.WithContext(context.WithValue(r.Context(), clientIPCtxKey, ip.Unmap()))
}
h.ServeHTTP(w, r)
})
}
// GetClientIP returns the client IP as a string, as set by one of the
// ClientIPFrom* middlewares. Returns "" if no valid IP was set.
// Convenient for logging, rate-limit keys, etc.
func GetClientIP(ctx context.Context) string {
ip := GetClientIPAddr(ctx)
if !ip.IsValid() {
return ""
}
return ip.String()
}
// GetClientIPAddr returns the client IP as a [netip.Addr], as set by one of
// the ClientIPFrom* middlewares. The returned Addr is the zero value if not
// set; use [netip.Addr.IsValid] to check. Useful when you need typed work —
// prefix containment, Is4/Is6, etc. — without re-parsing the string.
func GetClientIPAddr(ctx context.Context) netip.Addr {
ip, _ := ctx.Value(clientIPCtxKey).(netip.Addr)
return ip
}
// walkXFF walks the entries of the merged X-Forwarded-For chain
// RIGHT-TO-LEFT, invoking visit on each trimmed non-empty entry. visit
// returns true to stop the walk. Lazy walk, zero allocations (entries
// are substrings of the input headers).
//
// Multiple XFF headers are merged per RFC 2616 — each header's
// comma-separated entries in order received — so an attacker cannot pick
// which value security logic sees by sending a duplicate header.
func walkXFF(headers []string, visit func(entry string) bool) {
for hi := len(headers) - 1; hi >= 0; hi-- {
h := headers[hi]
for h != "" {
var v string
if i := strings.LastIndexByte(h, ','); i >= 0 {
v, h = h[i+1:], h[:i]
} else {
v, h = h, ""
}
v = strings.TrimSpace(v)
if v == "" {
continue
}
if visit(v) {
return
}
}
}
}
// inAnyPrefix reports whether ip falls within any of the given prefixes.
func inAnyPrefix(ip netip.Addr, prefixes []netip.Prefix) bool {
for _, p := range prefixes {
if p.Contains(ip) {
return true
}
}
return false
}
// parseHeaderAddr parses s and normalizes for storage: v4-mapped IPv6
// (::ffff:a.b.c.d) folds to plain v4, IPv6 zone is stripped. Both defend the
// trust-prefix check against attacker-injected aliases — [netip.Prefix.Contains]
// returns false for v4-mapped addresses vs v4 prefixes and for any zoned
// address, so without folding/stripping an attacker could escape an
// otherwise valid trust list.
//
// Header-sourced IPs only. [ClientIPFromRemoteAddr] normalizes inline
// (Unmap, but zone preserved for legitimate link-local connections).
func parseHeaderAddr(s string) (netip.Addr, bool) {
ip, err := netip.ParseAddr(s)
if err != nil {
return netip.Addr{}, false
}
return ip.Unmap().WithZone(""), true
}

View File

@@ -70,8 +70,8 @@ func NewCompressor(level int, types ...string) *Compressor {
if strings.Contains(strings.TrimSuffix(t, "/*"), "*") {
panic(fmt.Sprintf("middleware/compress: Unsupported content-type wildcard pattern '%s'. Only '/*' supported", t))
}
if strings.HasSuffix(t, "/*") {
allowedWildcards[strings.TrimSuffix(t, "/*")] = struct{}{}
if before, ok := strings.CutSuffix(t, "/*"); ok {
allowedWildcards[before] = struct{}{}
} else {
allowedTypes[t] = struct{}{}
}

View File

@@ -96,6 +96,8 @@ type DefaultLogFormatter struct {
// NewLogEntry creates a new LogEntry for the request.
func (l *DefaultLogFormatter) NewLogEntry(r *http.Request) LogEntry {
ctx := r.Context()
useColor := !l.NoColor
entry := &defaultLogEntry{
DefaultLogFormatter: l,
@@ -104,7 +106,7 @@ func (l *DefaultLogFormatter) NewLogEntry(r *http.Request) LogEntry {
useColor: useColor,
}
reqID := GetReqID(r.Context())
reqID := GetReqID(ctx)
if reqID != "" {
cW(entry.buf, useColor, nYellow, "[%s] ", reqID)
}
@@ -118,7 +120,11 @@ func (l *DefaultLogFormatter) NewLogEntry(r *http.Request) LogEntry {
cW(entry.buf, useColor, nCyan, "%s://%s%s %s\" ", scheme, r.Host, r.RequestURI, r.Proto)
entry.buf.WriteString("from ")
entry.buf.WriteString(r.RemoteAddr)
clientIP := GetClientIP(ctx)
if clientIP == "" {
clientIP = r.RemoteAddr
}
entry.buf.WriteString(clientIP)
entry.buf.WriteString(" - ")
return entry

View File

@@ -17,17 +17,14 @@ var xRealIP = http.CanonicalHeaderKey("X-Real-IP")
// of parsing either the True-Client-IP, X-Real-IP or the X-Forwarded-For headers
// (in that order).
//
// This middleware should be inserted fairly early in the middleware stack to
// ensure that subsequent layers (e.g., request loggers) which examine the
// RemoteAddr will see the intended value.
// Deprecated: RealIP is vulnerable to IP spoofing — it mutates r.RemoteAddr
// to the leftmost X-Forwarded-For value, or to True-Client-IP / X-Real-IP
// whether or not your infrastructure actually sets them. See
// GHSA-3fxj-6jh8-hvhx, GHSA-rjr7-jggh-pgcp, GHSA-9g5q-2w5x-hmxf.
//
// You should only use this middleware if you can trust the headers passed to
// you (in particular, the three headers this middleware uses), for example
// because you have placed a reverse proxy like HAProxy or nginx in front of
// chi. If your reverse proxies are configured to pass along arbitrary header
// values from the client, or if you use this middleware without a reverse
// proxy, malicious clients will be able to make you very sad (or, depending on
// how you're using RemoteAddr, vulnerable to an attack of some sort).
// Use [ClientIPFromHeader], [ClientIPFromXFF], [ClientIPFromXFFTrustedProxies]
// or [ClientIPFromRemoteAddr] and read the IP with [GetClientIP] instead.
// These never mutate r.RemoteAddr.
func RealIP(h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
if rip := realIP(r); rip != "" {

View File

@@ -208,8 +208,10 @@ func (f *http2FancyWriter) Push(target string, opts *http.PushOptions) error {
func (f *httpFancyWriter) ReadFrom(r io.Reader) (int64, error) {
if f.basicWriter.tee != nil {
// Route through basicWriter.Write so that data is also written to the
// tee writer. basicWriter.Write already increments basicWriter.bytes,
// so we must NOT add n again here (that would double-count).
n, err := io.Copy(&f.basicWriter, r)
f.basicWriter.bytes += int(n)
return n, err
}
rf := f.basicWriter.ResponseWriter.(io.ReaderFrom)

View File

@@ -472,9 +472,7 @@ func (mx *Mux) routeHTTP(w http.ResponseWriter, r *http.Request) {
value := rctx.URLParams.Values[i]
r.SetPathValue(key, value)
}
if supportsPattern {
setPattern(rctx, r)
}
r.Pattern = rctx.RoutePattern()
h.ServeHTTP(w, r)
return

View File

@@ -1,16 +0,0 @@
//go:build go1.23 && !tinygo
// +build go1.23,!tinygo
package chi
import "net/http"
// supportsPattern is true if the Go version is 1.23 and above.
//
// If this is true, `net/http.Request` has field `Pattern`.
const supportsPattern = true
// setPattern sets the mux matched pattern in the http Request.
func setPattern(rctx *Context, r *http.Request) {
r.Pattern = rctx.routePattern
}

View File

@@ -1,17 +0,0 @@
//go:build !go1.23 || tinygo
// +build !go1.23 tinygo
package chi
import "net/http"
// supportsPattern is true if the Go version is 1.23 and above.
//
// If this is true, `net/http.Request` has field `Pattern`.
const supportsPattern = false
// setPattern sets the mux matched pattern in the http Request.
//
// setPattern is only supported in Go 1.23 and above so
// this is just a blank function so that it compiles.
func setPattern(rctx *Context, r *http.Request) {}

View File

@@ -8,6 +8,7 @@ import (
"fmt"
"net/http"
"regexp"
"slices"
"sort"
"strconv"
"strings"
@@ -836,11 +837,15 @@ func Walk(r Routes, walkFn WalkFunc) error {
func walk(r Routes, walkFn WalkFunc, parentRoute string, parentMw ...func(http.Handler) http.Handler) error {
for _, route := range r.Routes() {
mws := make([]func(http.Handler) http.Handler, len(parentMw))
copy(mws, parentMw)
mws = append(mws, r.Middlewares()...)
mws := slices.Concat(parentMw, r.Middlewares())
if route.SubRoutes != nil {
if handler, ok := route.Handlers["*"]; ok {
if chain, ok := handler.(*ChainHandler); ok {
mws = append(mws, chain.Middlewares...)
}
}
if err := walk(route.SubRoutes, walkFn, parentRoute+route.Pattern, mws...); err != nil {
return err
}
@@ -854,7 +859,7 @@ func walk(r Routes, walkFn WalkFunc, parentRoute string, parentMw ...func(http.H
}
fullRoute := parentRoute + route.Pattern
fullRoute = strings.Replace(fullRoute, "/*/", "/", -1)
fullRoute = strings.ReplaceAll(fullRoute, "/*/", "/")
if chain, ok := handler.(*ChainHandler); ok {
if err := walkFn(method, fullRoute, chain.Endpoint, append(mws, chain.Middlewares...)...); err != nil {

4
vendor/modules.txt vendored
View File

@@ -452,8 +452,8 @@ github.com/go-acme/lego/v4/challenge
# github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667
## explicit; go 1.13
github.com/go-asn1-ber/asn1-ber
# github.com/go-chi/chi/v5 v5.2.5
## explicit; go 1.22
# github.com/go-chi/chi/v5 v5.3.0
## explicit; go 1.23
github.com/go-chi/chi/v5
github.com/go-chi/chi/v5/middleware
# github.com/go-chi/render v1.0.3