chore: Bump go-chi to version 5.3.1

This commit is contained in:
João "Pisco" Fernandes
2026-07-08 11:36:00 +01:00
parent c06b2d43e9
commit 9171757c66
10 changed files with 123 additions and 94 deletions
+1 -1
View File
@@ -10,7 +10,7 @@ require (
github.com/fortytw2/leaktest v1.3.0 github.com/fortytw2/leaktest v1.3.0
github.com/fsnotify/fsnotify v1.4.9 github.com/fsnotify/fsnotify v1.4.9
github.com/getsentry/sentry-go v0.43.0 github.com/getsentry/sentry-go v0.43.0
github.com/go-chi/chi/v5 v5.2.2 github.com/go-chi/chi/v5 v5.3.1
github.com/go-chi/cors v1.2.1 github.com/go-chi/cors v1.2.1
github.com/go-jose/go-jose/v4 v4.1.4 github.com/go-jose/go-jose/v4 v4.1.4
github.com/gobwas/ws v1.2.1 github.com/gobwas/ws v1.2.1
+2 -2
View File
@@ -54,8 +54,8 @@ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618= github.com/go-chi/chi/v5 v5.3.1 h1:3j4HZLGZQ3JpMCrPJF/Jl3mYJfWLKBfNJ6quurUGCf8=
github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/go-chi/chi/v5 v5.3.1/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
+69 -2
View File
@@ -87,7 +87,7 @@ func main() {
// A good base middleware stack // A good base middleware stack
r.Use(middleware.RequestID) 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.Logger)
r.Use(middleware.Recoverer) r.Use(middleware.Recoverer)
@@ -221,6 +221,7 @@ type Router interface {
Patch(pattern string, h http.HandlerFunc) Patch(pattern string, h http.HandlerFunc)
Post(pattern string, h http.HandlerFunc) Post(pattern string, h http.HandlerFunc)
Put(pattern string, h http.HandlerFunc) Put(pattern string, h http.HandlerFunc)
Query(pattern string, h http.HandlerFunc)
Trace(pattern string, h http.HandlerFunc) Trace(pattern string, h http.HandlerFunc)
// NotFound defines a handler to respond whenever a route could // NotFound defines a handler to respond whenever a route could
@@ -349,7 +350,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 | | [Logger] | Logs the start and end of each request with the elapsed processing time |
| [NoCache] | Sets response headers to prevent clients from caching | | [NoCache] | Sets response headers to prevent clients from caching |
| [Profiler] | Easily attach net/http/pprof to your routers | | [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 | | [Recoverer] | Gracefully absorb panics and prints the stack trace |
| [RequestID] | Injects a request ID into the context of each request | | [RequestID] | Injects a request ID into the context of each request |
| [RedirectSlashes] | Redirect slashes on routing paths | | [RedirectSlashes] | Redirect slashes on routing paths |
@@ -375,6 +380,12 @@ with `net/http` can be used with chi's mux.
[Logger]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Logger [Logger]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Logger
[NoCache]: https://pkg.go.dev/github.com/go-chi/chi/middleware#NoCache [NoCache]: https://pkg.go.dev/github.com/go-chi/chi/middleware#NoCache
[Profiler]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Profiler [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 [RealIP]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RealIP
[Recoverer]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Recoverer [Recoverer]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Recoverer
[RedirectSlashes]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RedirectSlashes [RedirectSlashes]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RedirectSlashes
@@ -402,6 +413,62 @@ with `net/http` can be used with chi's mux.
[ThrottleOpts]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ThrottleOpts [ThrottleOpts]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ThrottleOpts
[WrapResponseWriter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#WrapResponseWriter [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 ### Extra middlewares & packages
Please see https://github.com/go-chi for additional packages. Please see https://github.com/go-chi for additional packages.
+3 -2
View File
@@ -1,6 +1,6 @@
// Package chi is a small, idiomatic and composable router for building HTTP services. // Package chi is a small, idiomatic and composable router for building HTTP services.
// //
// chi requires Go 1.14 or newer. // chi supports the four most recent major versions of Go.
// //
// Example: // Example:
// //
@@ -77,7 +77,7 @@ type Router interface {
// path, with a fresh middleware stack for the inline-Router. // path, with a fresh middleware stack for the inline-Router.
Group(fn func(r Router)) 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 Route(pattern string, fn func(r Router)) Router
// Mount attaches another http.Handler along ./pattern/* // Mount attaches another http.Handler along ./pattern/*
@@ -102,6 +102,7 @@ type Router interface {
Patch(pattern string, h http.HandlerFunc) Patch(pattern string, h http.HandlerFunc)
Post(pattern string, h http.HandlerFunc) Post(pattern string, h http.HandlerFunc)
Put(pattern string, h http.HandlerFunc) Put(pattern string, h http.HandlerFunc)
Query(pattern string, h http.HandlerFunc)
Trace(pattern string, h http.HandlerFunc) Trace(pattern string, h http.HandlerFunc)
// NotFound defines a handler to respond whenever a route could // NotFound defines a handler to respond whenever a route could
+5 -4
View File
@@ -133,11 +133,12 @@ func (x *Context) RoutePattern() string {
return routePattern return routePattern
} }
// replaceWildcards takes a route pattern and recursively replaces all // replaceWildcards takes a route pattern and replaces all occurrences of
// occurrences of "/*/" to "/". // "/*/" with "/". It iteratively runs until no wildcards remain to
// correctly handle consecutive wildcards.
func replaceWildcards(p string) string { func replaceWildcards(p string) string {
if strings.Contains(p, "/*/") { for strings.Contains(p, "/*/") {
return replaceWildcards(strings.Replace(p, "/*/", "/", -1)) p = strings.ReplaceAll(p, "/*/", "/")
} }
return p return p
} }
+14 -9
View File
@@ -107,7 +107,8 @@ func (mx *Mux) Use(middlewares ...func(http.Handler) http.Handler) {
// Handle adds the route `pattern` that matches any http method to // Handle adds the route `pattern` that matches any http method to
// execute the `handler` http.Handler. // execute the `handler` http.Handler.
func (mx *Mux) Handle(pattern string, handler http.Handler) { func (mx *Mux) Handle(pattern string, handler http.Handler) {
if method, rest, found := strings.Cut(pattern, " "); found { if i := strings.IndexAny(pattern, " \t"); i >= 0 {
method, rest := pattern[:i], strings.TrimLeft(pattern[i+1:], " \t")
mx.Method(method, rest, handler) mx.Method(method, rest, handler)
return return
} }
@@ -118,12 +119,7 @@ func (mx *Mux) Handle(pattern string, handler http.Handler) {
// HandleFunc adds the route `pattern` that matches any http method to // HandleFunc adds the route `pattern` that matches any http method to
// execute the `handlerFn` http.HandlerFunc. // execute the `handlerFn` http.HandlerFunc.
func (mx *Mux) HandleFunc(pattern string, handlerFn http.HandlerFunc) { func (mx *Mux) HandleFunc(pattern string, handlerFn http.HandlerFunc) {
if method, rest, found := strings.Cut(pattern, " "); found { mx.Handle(pattern, handlerFn)
mx.Method(method, rest, handlerFn)
return
}
mx.handle(mALL, pattern, handlerFn)
} }
// Method adds the route `pattern` that matches `method` http method to // Method adds the route `pattern` that matches `method` http method to
@@ -190,6 +186,12 @@ func (mx *Mux) Put(pattern string, handlerFn http.HandlerFunc) {
mx.handle(mPUT, pattern, handlerFn) mx.handle(mPUT, pattern, handlerFn)
} }
// Query adds the route `pattern` that matches a QUERY http method to
// execute the `handlerFn` http.HandlerFunc.
func (mx *Mux) Query(pattern string, handlerFn http.HandlerFunc) {
mx.handle(mQUERY, pattern, handlerFn)
}
// Trace adds the route `pattern` that matches a TRACE http method to // Trace adds the route `pattern` that matches a TRACE http method to
// execute the `handlerFn` http.HandlerFunc. // execute the `handlerFn` http.HandlerFunc.
func (mx *Mux) Trace(pattern string, handlerFn http.HandlerFunc) { func (mx *Mux) Trace(pattern string, handlerFn http.HandlerFunc) {
@@ -471,9 +473,12 @@ func (mx *Mux) routeHTTP(w http.ResponseWriter, r *http.Request) {
// Find the route // Find the route
if _, _, h := mx.tree.FindRoute(rctx, method, routePath); h != nil { if _, _, h := mx.tree.FindRoute(rctx, method, routePath); h != nil {
if supportsPathValue { // Set http.Request path values from our request context
setPathValue(rctx, r) for i, key := range rctx.URLParams.Keys {
value := rctx.URLParams.Values[i]
r.SetPathValue(key, value)
} }
r.Pattern = rctx.RoutePattern()
h.ServeHTTP(w, r) h.ServeHTTP(w, r)
return return
-21
View File
@@ -1,21 +0,0 @@
//go:build go1.22 && !tinygo
// +build go1.22,!tinygo
package chi
import "net/http"
// supportsPathValue is true if the Go version is 1.22 and above.
//
// If this is true, `net/http.Request` has methods `SetPathValue` and `PathValue`.
const supportsPathValue = true
// setPathValue sets the path values in the Request value
// based on the provided request context.
func setPathValue(rctx *Context, r *http.Request) {
for i, key := range rctx.URLParams.Keys {
value := rctx.URLParams.Values[i]
r.SetPathValue(key, value)
}
}
-19
View File
@@ -1,19 +0,0 @@
//go:build !go1.22 || tinygo
// +build !go1.22 tinygo
package chi
import "net/http"
// supportsPathValue is true if the Go version is 1.22 and above.
//
// If this is true, `net/http.Request` has methods `SetPathValue` and `PathValue`.
const supportsPathValue = false
// setPathValue sets the path values in the Request value
// based on the provided request context.
//
// setPathValue is only supported in Go 1.22 and above so
// this is just a blank function so that it compiles.
func setPathValue(rctx *Context, r *http.Request) {
}
+27 -32
View File
@@ -8,6 +8,7 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"regexp" "regexp"
"slices"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
@@ -25,11 +26,17 @@ const (
mPATCH mPATCH
mPOST mPOST
mPUT mPUT
mQUERY
mTRACE mTRACE
) )
var mALL = mCONNECT | mDELETE | mGET | mHEAD | var mALL = mCONNECT | mDELETE | mGET | mHEAD |
mOPTIONS | mPATCH | mPOST | mPUT | mTRACE mOPTIONS | mPATCH | mPOST | mPUT | mQUERY | mTRACE
// methodQuery is the HTTP QUERY method (RFC 10008), a safe, idempotent
// method that conveys a request body. It is defined here until net/http
// provides an equivalent constant, at which point this is a 1-1 swap.
const methodQuery = "QUERY"
var methodMap = map[string]methodTyp{ var methodMap = map[string]methodTyp{
http.MethodConnect: mCONNECT, http.MethodConnect: mCONNECT,
@@ -40,6 +47,7 @@ var methodMap = map[string]methodTyp{
http.MethodPatch: mPATCH, http.MethodPatch: mPATCH,
http.MethodPost: mPOST, http.MethodPost: mPOST,
http.MethodPut: mPUT, http.MethodPut: mPUT,
methodQuery: mQUERY,
http.MethodTrace: mTRACE, http.MethodTrace: mTRACE,
} }
@@ -52,6 +60,7 @@ var reverseMethodMap = map[methodTyp]string{
mPATCH: http.MethodPatch, mPATCH: http.MethodPatch,
mPOST: http.MethodPost, mPOST: http.MethodPost,
mPUT: http.MethodPut, mPUT: http.MethodPut,
mQUERY: methodQuery,
mTRACE: http.MethodTrace, mTRACE: http.MethodTrace,
} }
@@ -71,6 +80,7 @@ func RegisterMethod(method string) {
} }
mt := methodTyp(2 << n) mt := methodTyp(2 << n)
methodMap[method] = mt methodMap[method] = mt
reverseMethodMap[mt] = method
mALL |= mt mALL |= mt
} }
@@ -328,7 +338,7 @@ func (n *node) replaceChild(label, tail byte, child *node) {
func (n *node) getEdge(ntyp nodeTyp, label, tail byte, prefix string) *node { func (n *node) getEdge(ntyp nodeTyp, label, tail byte, prefix string) *node {
nds := n.children[ntyp] nds := n.children[ntyp]
for i := 0; i < len(nds); i++ { for i := range nds {
if nds[i].label == label && nds[i].tail == tail { if nds[i].label == label && nds[i].tail == tail {
if ntyp == ntRegexp && nds[i].prefix != prefix { if ntyp == ntRegexp && nds[i].prefix != prefix {
continue continue
@@ -429,9 +439,7 @@ func (n *node) findRoute(rctx *Context, method methodTyp, path string) *node {
} }
// serially loop through each node grouped by the tail delimiter // serially loop through each node grouped by the tail delimiter
for idx := 0; idx < len(nds); idx++ { for _, xn = range nds {
xn = nds[idx]
// label for param nodes is the delimiter byte // label for param nodes is the delimiter byte
p := strings.IndexByte(xsearch, xn.tail) p := strings.IndexByte(xsearch, xn.tail)
@@ -650,11 +658,9 @@ func (n *node) routes() []Route {
if h.handler == nil { if h.handler == nil {
continue continue
} }
m := methodTypString(mt) if m, ok := reverseMethodMap[mt]; ok {
if m == "" { hs[m] = h.handler
continue
} }
hs[m] = h.handler
} }
rt := Route{subroutes, hs, p} rt := Route{subroutes, hs, p}
@@ -772,29 +778,14 @@ func patParamKeys(pattern string) []string {
} }
} }
// longestPrefix finds the length of the shared prefix // longestPrefix finds the length of the shared prefix of two strings
// of two strings func longestPrefix(k1, k2 string) (i int) {
func longestPrefix(k1, k2 string) int { for i = 0; i < min(len(k1), len(k2)); i++ {
max := len(k1)
if l := len(k2); l < max {
max = l
}
var i int
for i = 0; i < max; i++ {
if k1[i] != k2[i] { if k1[i] != k2[i] {
break break
} }
} }
return i return
}
func methodTypString(method methodTyp) string {
for s, t := range methodMap {
if method == t {
return s
}
}
return ""
} }
type nodes []*node type nodes []*node
@@ -854,11 +845,15 @@ func Walk(r Routes, walkFn WalkFunc) error {
func walk(r Routes, walkFn WalkFunc, parentRoute string, parentMw ...func(http.Handler) http.Handler) error { func walk(r Routes, walkFn WalkFunc, parentRoute string, parentMw ...func(http.Handler) http.Handler) error {
for _, route := range r.Routes() { for _, route := range r.Routes() {
mws := make([]func(http.Handler) http.Handler, len(parentMw)) mws := slices.Concat(parentMw, r.Middlewares())
copy(mws, parentMw)
mws = append(mws, r.Middlewares()...)
if route.SubRoutes != nil { 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 { if err := walk(route.SubRoutes, walkFn, parentRoute+route.Pattern, mws...); err != nil {
return err return err
} }
@@ -872,7 +867,7 @@ func walk(r Routes, walkFn WalkFunc, parentRoute string, parentMw ...func(http.H
} }
fullRoute := parentRoute + route.Pattern fullRoute := parentRoute + route.Pattern
fullRoute = strings.Replace(fullRoute, "/*/", "/", -1) fullRoute = strings.ReplaceAll(fullRoute, "/*/", "/")
if chain, ok := handler.(*ChainHandler); ok { if chain, ok := handler.(*ChainHandler); ok {
if err := walkFn(method, fullRoute, chain.Endpoint, append(mws, chain.Middlewares...)...); err != nil { if err := walkFn(method, fullRoute, chain.Endpoint, append(mws, chain.Middlewares...)...); err != nil {
+2 -2
View File
@@ -64,8 +64,8 @@ github.com/getsentry/sentry-go/internal/telemetry
github.com/getsentry/sentry-go/internal/util github.com/getsentry/sentry-go/internal/util
# github.com/gin-gonic/gin v1.9.1 # github.com/gin-gonic/gin v1.9.1
## explicit; go 1.20 ## explicit; go 1.20
# github.com/go-chi/chi/v5 v5.2.2 # github.com/go-chi/chi/v5 v5.3.1
## explicit; go 1.20 ## explicit; go 1.23
github.com/go-chi/chi/v5 github.com/go-chi/chi/v5
# github.com/go-chi/cors v1.2.1 # github.com/go-chi/cors v1.2.1
## explicit; go 1.14 ## explicit; go 1.14