Files
httpx/server/route.go
Aleksey Shakhmatov 8a63f142a7 Add WithNotFoundHandler option for custom 404 responses on Router
Allows configuring a custom handler for unmatched routes, enabling
consistent JSON error responses instead of ServeMux's default plain
text. NewRouter now accepts RouterOption functional options.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 21:48:13 +03:00

127 lines
3.7 KiB
Go

package server
import (
"net/http"
"strings"
)
// Router is a lightweight wrapper around http.ServeMux that adds middleware
// groups and sub-router mounting. It leverages Go 1.22+ enhanced patterns
// like "GET /users/{id}".
type Router struct {
mux *http.ServeMux
prefix string
middlewares []Middleware
notFoundHandler http.Handler
}
// RouterOption configures a Router.
type RouterOption func(*Router)
// WithNotFoundHandler sets a custom handler for requests that don't match
// any registered pattern. This is useful for returning JSON 404/405 responses
// instead of the default plain text.
func WithNotFoundHandler(h http.Handler) RouterOption {
return func(r *Router) { r.notFoundHandler = h }
}
// NewRouter creates a new Router backed by a fresh http.ServeMux.
func NewRouter(opts ...RouterOption) *Router {
r := &Router{
mux: http.NewServeMux(),
}
for _, opt := range opts {
opt(r)
}
return r
}
// Handle registers a handler for the given pattern. The pattern follows
// http.ServeMux conventions, including method-based patterns like "GET /users".
func (r *Router) Handle(pattern string, handler http.Handler) {
if len(r.middlewares) > 0 {
handler = Chain(r.middlewares...)(handler)
}
r.mux.Handle(r.prefixedPattern(pattern), handler)
}
// HandleFunc registers a handler function for the given pattern.
func (r *Router) HandleFunc(pattern string, fn http.HandlerFunc) {
r.Handle(pattern, fn)
}
// Group creates a sub-router with a shared prefix and optional middleware.
// Patterns registered on the group are prefixed automatically. The group
// shares the underlying ServeMux with the parent router.
//
// Example:
//
// api := router.Group("/api/v1", authMiddleware)
// api.HandleFunc("GET /users", listUsers) // registers "GET /api/v1/users"
func (r *Router) Group(prefix string, mws ...Middleware) *Router {
return &Router{
mux: r.mux,
prefix: r.prefix + prefix,
middlewares: append(r.middlewaresSnapshot(), mws...),
}
}
// Mount attaches an http.Handler under the given prefix. All requests
// starting with prefix are forwarded to the handler with the prefix stripped.
func (r *Router) Mount(prefix string, handler http.Handler) {
full := r.prefix + prefix
if !strings.HasSuffix(full, "/") {
full += "/"
}
r.mux.Handle(full, http.StripPrefix(strings.TrimSuffix(full, "/"), handler))
}
// ServeHTTP implements http.Handler, making Router usable as a handler.
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if r.notFoundHandler != nil {
// Use the mux to check for a match. If none, use the custom handler.
_, pattern := r.mux.Handler(req)
if pattern == "" {
r.notFoundHandler.ServeHTTP(w, req)
return
}
}
r.mux.ServeHTTP(w, req)
}
// prefixedPattern inserts the router prefix into a pattern. It is aware of
// method prefixes: "GET /users" with prefix "/api" becomes "GET /api/users".
func (r *Router) prefixedPattern(pattern string) string {
if r.prefix == "" {
return pattern
}
// Split method prefix if present: "GET /users" → method="GET ", path="/users"
method, path, hasMethod := splitMethodPattern(pattern)
path = r.prefix + path
if hasMethod {
return method + path
}
return path
}
// splitMethodPattern splits "GET /path" into ("GET ", "/path", true).
// If there is no method prefix, returns ("", pattern, false).
func splitMethodPattern(pattern string) (method, path string, hasMethod bool) {
if idx := strings.IndexByte(pattern, ' '); idx >= 0 {
return pattern[:idx+1], pattern[idx+1:], true
}
return "", pattern, false
}
func (r *Router) middlewaresSnapshot() []Middleware {
if len(r.middlewares) == 0 {
return nil
}
cp := make([]Middleware, len(r.middlewares))
copy(cp, r.middlewares)
return cp
}