mirror of
https://github.com/rocky-linux/peridot.git
synced 2024-10-31 20:41:22 +00:00
467 lines
15 KiB
Go
467 lines
15 KiB
Go
package chi
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
var _ Router = &Mux{}
|
|
|
|
// Mux is a simple HTTP route multiplexer that parses a request path,
|
|
// records any URL params, and executes an end handler. It implements
|
|
// the http.Handler interface and is friendly with the standard library.
|
|
//
|
|
// Mux is designed to be fast, minimal and offer a powerful API for building
|
|
// modular and composable HTTP services with a large set of handlers. It's
|
|
// particularly useful for writing large REST API services that break a handler
|
|
// into many smaller parts composed of middlewares and end handlers.
|
|
type Mux struct {
|
|
// The radix trie router
|
|
tree *node
|
|
|
|
// The middleware stack
|
|
middlewares []func(http.Handler) http.Handler
|
|
|
|
// Controls the behaviour of middleware chain generation when a mux
|
|
// is registered as an inline group inside another mux.
|
|
inline bool
|
|
parent *Mux
|
|
|
|
// The computed mux handler made of the chained middleware stack and
|
|
// the tree router
|
|
handler http.Handler
|
|
|
|
// Routing context pool
|
|
pool *sync.Pool
|
|
|
|
// Custom route not found handler
|
|
notFoundHandler http.HandlerFunc
|
|
|
|
// Custom method not allowed handler
|
|
methodNotAllowedHandler http.HandlerFunc
|
|
}
|
|
|
|
// NewMux returns a newly initialized Mux object that implements the Router
|
|
// interface.
|
|
func NewMux() *Mux {
|
|
mux := &Mux{tree: &node{}, pool: &sync.Pool{}}
|
|
mux.pool.New = func() interface{} {
|
|
return NewRouteContext()
|
|
}
|
|
return mux
|
|
}
|
|
|
|
// ServeHTTP is the single method of the http.Handler interface that makes
|
|
// Mux interoperable with the standard library. It uses a sync.Pool to get and
|
|
// reuse routing contexts for each request.
|
|
func (mx *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
// Ensure the mux has some routes defined on the mux
|
|
if mx.handler == nil {
|
|
mx.NotFoundHandler().ServeHTTP(w, r)
|
|
return
|
|
}
|
|
|
|
// Check if a routing context already exists from a parent router.
|
|
rctx, _ := r.Context().Value(RouteCtxKey).(*Context)
|
|
if rctx != nil {
|
|
mx.handler.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
|
|
// Fetch a RouteContext object from the sync pool, and call the computed
|
|
// mx.handler that is comprised of mx.middlewares + mx.routeHTTP.
|
|
// Once the request is finished, reset the routing context and put it back
|
|
// into the pool for reuse from another request.
|
|
rctx = mx.pool.Get().(*Context)
|
|
rctx.Reset()
|
|
rctx.Routes = mx
|
|
|
|
// NOTE: r.WithContext() causes 2 allocations and context.WithValue() causes 1 allocation
|
|
r = r.WithContext(context.WithValue(r.Context(), RouteCtxKey, rctx))
|
|
|
|
// Serve the request and once its done, put the request context back in the sync pool
|
|
mx.handler.ServeHTTP(w, r)
|
|
mx.pool.Put(rctx)
|
|
}
|
|
|
|
// Use appends a middleware handler to the Mux middleware stack.
|
|
//
|
|
// The middleware stack for any Mux will execute before searching for a matching
|
|
// route to a specific handler, which provides opportunity to respond early,
|
|
// change the course of the request execution, or set request-scoped values for
|
|
// the next http.Handler.
|
|
func (mx *Mux) Use(middlewares ...func(http.Handler) http.Handler) {
|
|
if mx.handler != nil {
|
|
panic("chi: all middlewares must be defined before routes on a mux")
|
|
}
|
|
mx.middlewares = append(mx.middlewares, middlewares...)
|
|
}
|
|
|
|
// Handle adds the route `pattern` that matches any http method to
|
|
// execute the `handler` http.Handler.
|
|
func (mx *Mux) Handle(pattern string, handler http.Handler) {
|
|
mx.handle(mALL, pattern, handler)
|
|
}
|
|
|
|
// HandleFunc adds the route `pattern` that matches any http method to
|
|
// execute the `handlerFn` http.HandlerFunc.
|
|
func (mx *Mux) HandleFunc(pattern string, handlerFn http.HandlerFunc) {
|
|
mx.handle(mALL, pattern, handlerFn)
|
|
}
|
|
|
|
// Method adds the route `pattern` that matches `method` http method to
|
|
// execute the `handler` http.Handler.
|
|
func (mx *Mux) Method(method, pattern string, handler http.Handler) {
|
|
m, ok := methodMap[strings.ToUpper(method)]
|
|
if !ok {
|
|
panic(fmt.Sprintf("chi: '%s' http method is not supported.", method))
|
|
}
|
|
mx.handle(m, pattern, handler)
|
|
}
|
|
|
|
// MethodFunc adds the route `pattern` that matches `method` http method to
|
|
// execute the `handlerFn` http.HandlerFunc.
|
|
func (mx *Mux) MethodFunc(method, pattern string, handlerFn http.HandlerFunc) {
|
|
mx.Method(method, pattern, handlerFn)
|
|
}
|
|
|
|
// Connect adds the route `pattern` that matches a CONNECT http method to
|
|
// execute the `handlerFn` http.HandlerFunc.
|
|
func (mx *Mux) Connect(pattern string, handlerFn http.HandlerFunc) {
|
|
mx.handle(mCONNECT, pattern, handlerFn)
|
|
}
|
|
|
|
// Delete adds the route `pattern` that matches a DELETE http method to
|
|
// execute the `handlerFn` http.HandlerFunc.
|
|
func (mx *Mux) Delete(pattern string, handlerFn http.HandlerFunc) {
|
|
mx.handle(mDELETE, pattern, handlerFn)
|
|
}
|
|
|
|
// Get adds the route `pattern` that matches a GET http method to
|
|
// execute the `handlerFn` http.HandlerFunc.
|
|
func (mx *Mux) Get(pattern string, handlerFn http.HandlerFunc) {
|
|
mx.handle(mGET, pattern, handlerFn)
|
|
}
|
|
|
|
// Head adds the route `pattern` that matches a HEAD http method to
|
|
// execute the `handlerFn` http.HandlerFunc.
|
|
func (mx *Mux) Head(pattern string, handlerFn http.HandlerFunc) {
|
|
mx.handle(mHEAD, pattern, handlerFn)
|
|
}
|
|
|
|
// Options adds the route `pattern` that matches a OPTIONS http method to
|
|
// execute the `handlerFn` http.HandlerFunc.
|
|
func (mx *Mux) Options(pattern string, handlerFn http.HandlerFunc) {
|
|
mx.handle(mOPTIONS, pattern, handlerFn)
|
|
}
|
|
|
|
// Patch adds the route `pattern` that matches a PATCH http method to
|
|
// execute the `handlerFn` http.HandlerFunc.
|
|
func (mx *Mux) Patch(pattern string, handlerFn http.HandlerFunc) {
|
|
mx.handle(mPATCH, pattern, handlerFn)
|
|
}
|
|
|
|
// Post adds the route `pattern` that matches a POST http method to
|
|
// execute the `handlerFn` http.HandlerFunc.
|
|
func (mx *Mux) Post(pattern string, handlerFn http.HandlerFunc) {
|
|
mx.handle(mPOST, pattern, handlerFn)
|
|
}
|
|
|
|
// Put adds the route `pattern` that matches a PUT http method to
|
|
// execute the `handlerFn` http.HandlerFunc.
|
|
func (mx *Mux) Put(pattern string, handlerFn http.HandlerFunc) {
|
|
mx.handle(mPUT, pattern, handlerFn)
|
|
}
|
|
|
|
// Trace adds the route `pattern` that matches a TRACE http method to
|
|
// execute the `handlerFn` http.HandlerFunc.
|
|
func (mx *Mux) Trace(pattern string, handlerFn http.HandlerFunc) {
|
|
mx.handle(mTRACE, pattern, handlerFn)
|
|
}
|
|
|
|
// NotFound sets a custom http.HandlerFunc for routing paths that could
|
|
// not be found. The default 404 handler is `http.NotFound`.
|
|
func (mx *Mux) NotFound(handlerFn http.HandlerFunc) {
|
|
// Build NotFound handler chain
|
|
m := mx
|
|
hFn := handlerFn
|
|
if mx.inline && mx.parent != nil {
|
|
m = mx.parent
|
|
hFn = Chain(mx.middlewares...).HandlerFunc(hFn).ServeHTTP
|
|
}
|
|
|
|
// Update the notFoundHandler from this point forward
|
|
m.notFoundHandler = hFn
|
|
m.updateSubRoutes(func(subMux *Mux) {
|
|
if subMux.notFoundHandler == nil {
|
|
subMux.NotFound(hFn)
|
|
}
|
|
})
|
|
}
|
|
|
|
// MethodNotAllowed sets a custom http.HandlerFunc for routing paths where the
|
|
// method is unresolved. The default handler returns a 405 with an empty body.
|
|
func (mx *Mux) MethodNotAllowed(handlerFn http.HandlerFunc) {
|
|
// Build MethodNotAllowed handler chain
|
|
m := mx
|
|
hFn := handlerFn
|
|
if mx.inline && mx.parent != nil {
|
|
m = mx.parent
|
|
hFn = Chain(mx.middlewares...).HandlerFunc(hFn).ServeHTTP
|
|
}
|
|
|
|
// Update the methodNotAllowedHandler from this point forward
|
|
m.methodNotAllowedHandler = hFn
|
|
m.updateSubRoutes(func(subMux *Mux) {
|
|
if subMux.methodNotAllowedHandler == nil {
|
|
subMux.MethodNotAllowed(hFn)
|
|
}
|
|
})
|
|
}
|
|
|
|
// With adds inline middlewares for an endpoint handler.
|
|
func (mx *Mux) With(middlewares ...func(http.Handler) http.Handler) Router {
|
|
// Similarly as in handle(), we must build the mux handler once additional
|
|
// middleware registration isn't allowed for this stack, like now.
|
|
if !mx.inline && mx.handler == nil {
|
|
mx.buildRouteHandler()
|
|
}
|
|
|
|
// Copy middlewares from parent inline muxs
|
|
var mws Middlewares
|
|
if mx.inline {
|
|
mws = make(Middlewares, len(mx.middlewares))
|
|
copy(mws, mx.middlewares)
|
|
}
|
|
mws = append(mws, middlewares...)
|
|
|
|
im := &Mux{
|
|
pool: mx.pool, inline: true, parent: mx, tree: mx.tree, middlewares: mws,
|
|
notFoundHandler: mx.notFoundHandler, methodNotAllowedHandler: mx.methodNotAllowedHandler,
|
|
}
|
|
|
|
return im
|
|
}
|
|
|
|
// Group creates a new inline-Mux with a fresh middleware stack. It's useful
|
|
// for a group of handlers along the same routing path that use an additional
|
|
// set of middlewares. See _examples/.
|
|
func (mx *Mux) Group(fn func(r Router)) Router {
|
|
im := mx.With().(*Mux)
|
|
if fn != nil {
|
|
fn(im)
|
|
}
|
|
return im
|
|
}
|
|
|
|
// Route creates a new Mux with a fresh middleware stack and mounts it
|
|
// along the `pattern` as a subrouter. Effectively, this is a short-hand
|
|
// call to Mount. See _examples/.
|
|
func (mx *Mux) Route(pattern string, fn func(r Router)) Router {
|
|
subRouter := NewRouter()
|
|
if fn != nil {
|
|
fn(subRouter)
|
|
}
|
|
mx.Mount(pattern, subRouter)
|
|
return subRouter
|
|
}
|
|
|
|
// Mount attaches another http.Handler or chi Router as a subrouter along a routing
|
|
// path. It's very useful to split up a large API as many independent routers and
|
|
// compose them as a single service using Mount. See _examples/.
|
|
//
|
|
// Note that Mount() simply sets a wildcard along the `pattern` that will continue
|
|
// routing at the `handler`, which in most cases is another chi.Router. As a result,
|
|
// if you define two Mount() routes on the exact same pattern the mount will panic.
|
|
func (mx *Mux) Mount(pattern string, handler http.Handler) {
|
|
// Provide runtime safety for ensuring a pattern isn't mounted on an existing
|
|
// routing pattern.
|
|
if mx.tree.findPattern(pattern+"*") || mx.tree.findPattern(pattern+"/*") {
|
|
panic(fmt.Sprintf("chi: attempting to Mount() a handler on an existing path, '%s'", pattern))
|
|
}
|
|
|
|
// Assign sub-Router's with the parent not found & method not allowed handler if not specified.
|
|
subr, ok := handler.(*Mux)
|
|
if ok && subr.notFoundHandler == nil && mx.notFoundHandler != nil {
|
|
subr.NotFound(mx.notFoundHandler)
|
|
}
|
|
if ok && subr.methodNotAllowedHandler == nil && mx.methodNotAllowedHandler != nil {
|
|
subr.MethodNotAllowed(mx.methodNotAllowedHandler)
|
|
}
|
|
|
|
mountHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
rctx := RouteContext(r.Context())
|
|
rctx.RoutePath = mx.nextRoutePath(rctx)
|
|
handler.ServeHTTP(w, r)
|
|
})
|
|
|
|
if pattern == "" || pattern[len(pattern)-1] != '/' {
|
|
mx.handle(mALL|mSTUB, pattern, mountHandler)
|
|
mx.handle(mALL|mSTUB, pattern+"/", mountHandler)
|
|
pattern += "/"
|
|
}
|
|
|
|
method := mALL
|
|
subroutes, _ := handler.(Routes)
|
|
if subroutes != nil {
|
|
method |= mSTUB
|
|
}
|
|
n := mx.handle(method, pattern+"*", mountHandler)
|
|
|
|
if subroutes != nil {
|
|
n.subroutes = subroutes
|
|
}
|
|
}
|
|
|
|
// Routes returns a slice of routing information from the tree,
|
|
// useful for traversing available routes of a router.
|
|
func (mx *Mux) Routes() []Route {
|
|
return mx.tree.routes()
|
|
}
|
|
|
|
// Middlewares returns a slice of middleware handler functions.
|
|
func (mx *Mux) Middlewares() Middlewares {
|
|
return mx.middlewares
|
|
}
|
|
|
|
// Match searches the routing tree for a handler that matches the method/path.
|
|
// It's similar to routing a http request, but without executing the handler
|
|
// thereafter.
|
|
//
|
|
// Note: the *Context state is updated during execution, so manage
|
|
// the state carefully or make a NewRouteContext().
|
|
func (mx *Mux) Match(rctx *Context, method, path string) bool {
|
|
m, ok := methodMap[method]
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
node, _, h := mx.tree.FindRoute(rctx, m, path)
|
|
|
|
if node != nil && node.subroutes != nil {
|
|
rctx.RoutePath = mx.nextRoutePath(rctx)
|
|
return node.subroutes.Match(rctx, method, rctx.RoutePath)
|
|
}
|
|
|
|
return h != nil
|
|
}
|
|
|
|
// NotFoundHandler returns the default Mux 404 responder whenever a route
|
|
// cannot be found.
|
|
func (mx *Mux) NotFoundHandler() http.HandlerFunc {
|
|
if mx.notFoundHandler != nil {
|
|
return mx.notFoundHandler
|
|
}
|
|
return http.NotFound
|
|
}
|
|
|
|
// MethodNotAllowedHandler returns the default Mux 405 responder whenever
|
|
// a method cannot be resolved for a route.
|
|
func (mx *Mux) MethodNotAllowedHandler() http.HandlerFunc {
|
|
if mx.methodNotAllowedHandler != nil {
|
|
return mx.methodNotAllowedHandler
|
|
}
|
|
return methodNotAllowedHandler
|
|
}
|
|
|
|
// buildRouteHandler builds the single mux handler that is a chain of the middleware
|
|
// stack, as defined by calls to Use(), and the tree router (Mux) itself. After this
|
|
// point, no other middlewares can be registered on this Mux's stack. But you can still
|
|
// compose additional middlewares via Group()'s or using a chained middleware handler.
|
|
func (mx *Mux) buildRouteHandler() {
|
|
mx.handler = chain(mx.middlewares, http.HandlerFunc(mx.routeHTTP))
|
|
}
|
|
|
|
// handle registers a http.Handler in the routing tree for a particular http method
|
|
// and routing pattern.
|
|
func (mx *Mux) handle(method methodTyp, pattern string, handler http.Handler) *node {
|
|
if len(pattern) == 0 || pattern[0] != '/' {
|
|
panic(fmt.Sprintf("chi: routing pattern must begin with '/' in '%s'", pattern))
|
|
}
|
|
|
|
// Build the computed routing handler for this routing pattern.
|
|
if !mx.inline && mx.handler == nil {
|
|
mx.buildRouteHandler()
|
|
}
|
|
|
|
// Build endpoint handler with inline middlewares for the route
|
|
var h http.Handler
|
|
if mx.inline {
|
|
mx.handler = http.HandlerFunc(mx.routeHTTP)
|
|
h = Chain(mx.middlewares...).Handler(handler)
|
|
} else {
|
|
h = handler
|
|
}
|
|
|
|
// Add the endpoint to the tree and return the node
|
|
return mx.tree.InsertRoute(method, pattern, h)
|
|
}
|
|
|
|
// routeHTTP routes a http.Request through the Mux routing tree to serve
|
|
// the matching handler for a particular http method.
|
|
func (mx *Mux) routeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
// Grab the route context object
|
|
rctx := r.Context().Value(RouteCtxKey).(*Context)
|
|
|
|
// The request routing path
|
|
routePath := rctx.RoutePath
|
|
if routePath == "" {
|
|
if r.URL.RawPath != "" {
|
|
routePath = r.URL.RawPath
|
|
} else {
|
|
routePath = r.URL.Path
|
|
}
|
|
}
|
|
|
|
// Check if method is supported by chi
|
|
if rctx.RouteMethod == "" {
|
|
rctx.RouteMethod = r.Method
|
|
}
|
|
method, ok := methodMap[rctx.RouteMethod]
|
|
if !ok {
|
|
mx.MethodNotAllowedHandler().ServeHTTP(w, r)
|
|
return
|
|
}
|
|
|
|
// Find the route
|
|
if _, _, h := mx.tree.FindRoute(rctx, method, routePath); h != nil {
|
|
h.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
if rctx.methodNotAllowed {
|
|
mx.MethodNotAllowedHandler().ServeHTTP(w, r)
|
|
} else {
|
|
mx.NotFoundHandler().ServeHTTP(w, r)
|
|
}
|
|
}
|
|
|
|
func (mx *Mux) nextRoutePath(rctx *Context) string {
|
|
routePath := "/"
|
|
nx := len(rctx.routeParams.Keys) - 1 // index of last param in list
|
|
if nx >= 0 && rctx.routeParams.Keys[nx] == "*" && len(rctx.routeParams.Values) > nx {
|
|
routePath = "/" + rctx.routeParams.Values[nx]
|
|
}
|
|
return routePath
|
|
}
|
|
|
|
// Recursively update data on child routers.
|
|
func (mx *Mux) updateSubRoutes(fn func(subMux *Mux)) {
|
|
for _, r := range mx.tree.routes() {
|
|
subMux, ok := r.SubRoutes.(*Mux)
|
|
if !ok {
|
|
continue
|
|
}
|
|
fn(subMux)
|
|
}
|
|
}
|
|
|
|
// methodNotAllowedHandler is a helper function to respond with a 405,
|
|
// method not allowed.
|
|
func methodNotAllowedHandler(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(405)
|
|
w.Write(nil)
|
|
}
|