goyave.dev/goyave/v5@v5.0.0-rc9.0.20240517145003-d3f977d0b9f3/router.go (about) 1 package goyave 2 3 import ( 4 "errors" 5 "io/fs" 6 "net/http" 7 "regexp" 8 "strings" 9 10 "maps" 11 "slices" 12 13 "github.com/samber/lo" 14 "goyave.dev/goyave/v5/cors" 15 errorutil "goyave.dev/goyave/v5/util/errors" 16 ) 17 18 // Common route meta keys. 19 const ( 20 MetaCORS = "goyave.cors" 21 ) 22 23 // Special route names. 24 const ( 25 RouteMethodNotAllowed = "goyave.method-not-allowed" 26 RouteNotFound = "goyave.not-found" 27 ) 28 29 var ( 30 errMatchMethodNotAllowed = errors.New("Method not allowed for this route") 31 errMatchNotFound = errors.New("No match for this URI") 32 33 methodNotAllowedRoute = newRoute(func(response *Response, _ *Request) { 34 response.Status(http.StatusMethodNotAllowed) 35 }, RouteMethodNotAllowed) 36 notFoundRoute = newRoute(func(response *Response, _ *Request) { 37 response.Status(http.StatusNotFound) 38 }, RouteNotFound) 39 ) 40 41 // Handler responds to an HTTP request. 42 // 43 // The given `Response` and `Request` value should not 44 // be used outside of the context of an HTTP request. e.g.: passed to 45 // a goroutine or used after the finalization step in the request lifecycle. 46 type Handler func(response *Response, request *Request) 47 48 type routeMatcher interface { 49 match(method string, match *routeMatch) bool 50 } 51 52 type routeMatch struct { 53 route *Route 54 parameters map[string]string 55 err error 56 currentPath string 57 } 58 59 func (rm *routeMatch) mergeParams(params map[string]string) { 60 if rm.parameters == nil { 61 rm.parameters = params 62 return 63 } 64 for k, v := range params { 65 rm.parameters[k] = v 66 } 67 } 68 69 func (rm *routeMatch) trimCurrentPath(fullMatch string) { 70 length := len(fullMatch) 71 rm.currentPath = rm.currentPath[length:] 72 } 73 74 // Router registers routes to be matched and executes a handler. 75 type Router struct { 76 server *Server 77 parent *Router 78 statusHandlers map[int]StatusHandler 79 namedRoutes map[string]*Route 80 regexCache map[string]*regexp.Regexp 81 Meta map[string]any 82 83 parameterizable 84 middlewareHolder 85 globalMiddleware *middlewareHolder 86 87 prefix string 88 routes []*Route 89 subrouters []*Router 90 91 slashCount int 92 } 93 94 var _ http.Handler = (*Router)(nil) // implements http.Handler 95 var _ routeMatcher = (*Router)(nil) // implements routeMatcher 96 97 // NewRouter create a new root-level Router that is pre-configured with core 98 // middleware (recovery and language), as well as status handlers 99 // for all standard HTTP status codes. 100 // 101 // You don't need to manually build your router using this function. 102 // This method can however be useful for external tooling that build 103 // routers without starting the HTTP server. Don't forget to call 104 // `router.ClearRegexCache()` when you are done registering routes. 105 func NewRouter(server *Server) *Router { 106 router := &Router{ 107 server: server, 108 parent: nil, 109 prefix: "", 110 statusHandlers: make(map[int]StatusHandler, 41), 111 namedRoutes: make(map[string]*Route, 5), 112 middlewareHolder: middlewareHolder{ 113 middleware: nil, 114 }, 115 globalMiddleware: &middlewareHolder{ 116 middleware: make([]Middleware, 0, 2), 117 }, 118 regexCache: make(map[string]*regexp.Regexp, 5), 119 Meta: make(map[string]any), 120 } 121 router.StatusHandler(&PanicStatusHandler{}, http.StatusInternalServerError) 122 for i := 400; i <= 418; i++ { 123 router.StatusHandler(&ErrorStatusHandler{}, i) 124 } 125 router.StatusHandler(&ValidationStatusHandler{}, http.StatusUnprocessableEntity) 126 for i := 423; i <= 426; i++ { 127 router.StatusHandler(&ErrorStatusHandler{}, i) 128 } 129 router.StatusHandler(&ErrorStatusHandler{}, 421, 428, 429, 431, 444, 451) 130 router.StatusHandler(&ErrorStatusHandler{}, 501, 502, 503, 504, 505, 506, 507, 508, 510, 511) 131 router.GlobalMiddleware(&recoveryMiddleware{}, &languageMiddleware{}) 132 return router 133 } 134 135 // ClearRegexCache set internal router's regex cache used for route parameters optimisation to nil 136 // so it can be garbage collected. 137 // You don't need to call this function if you are using `goyave.Server`. 138 // However, this method SHOULD be called by external tooling that build routers without starting the HTTP 139 // server when they are done registering routes and subrouters. 140 func (r *Router) ClearRegexCache() { 141 r.regexCache = nil 142 for _, subrouter := range r.subrouters { 143 subrouter.ClearRegexCache() 144 } 145 } 146 147 // GetParent returns the parent Router of this router (can be `nil`). 148 func (r *Router) GetParent() *Router { 149 return r.parent 150 } 151 152 // GetRoutes returns the list of routes belonging to this router. 153 func (r *Router) GetRoutes() []*Route { 154 cpy := make([]*Route, len(r.routes)) 155 copy(cpy, r.routes) 156 return cpy 157 } 158 159 // GetSubrouters returns the list of subrouters belonging to this router. 160 func (r *Router) GetSubrouters() []*Router { 161 cpy := make([]*Router, len(r.subrouters)) 162 copy(cpy, r.subrouters) 163 return cpy 164 } 165 166 // GetRoute get a named route. 167 // Returns nil if the route doesn't exist. 168 func (r *Router) GetRoute(name string) *Route { 169 return r.namedRoutes[name] 170 } 171 172 // SetMeta attach a value to this router identified by the given key. 173 // 174 // This value is inherited by all subrouters and routes, unless they override 175 // it at their level. 176 func (r *Router) SetMeta(key string, value any) *Router { 177 r.Meta[key] = value 178 return r 179 } 180 181 // RemoveMeta detach the meta value identified by the given key from this router. 182 // This doesn't remove meta using the same key from the parent routers. 183 func (r *Router) RemoveMeta(key string) *Router { 184 delete(r.Meta, key) 185 return r 186 } 187 188 // LookupMeta value identified by the given key. If not found in this router, 189 // the value is recursively fetched in the parent routers. 190 // 191 // Returns the value and `true` if found in the current router or one of the 192 // parent routers, `nil` and `false` otherwise. 193 func (r *Router) LookupMeta(key string) (any, bool) { 194 val, ok := r.Meta[key] 195 if ok { 196 return val, ok 197 } 198 if r.parent != nil { 199 return r.parent.LookupMeta(key) 200 } 201 return nil, false 202 } 203 204 // GlobalMiddleware apply one or more global middleware. Global middleware are 205 // executed for every request, including when the request doesn't match any route 206 // or if it results in "Method Not Allowed". 207 // These middleware are global to the main Router: they will also be executed for subrouters. 208 // Global Middleware are always executed first. 209 // Use global middleware for logging and rate limiting for example. 210 func (r *Router) GlobalMiddleware(middleware ...Middleware) *Router { 211 for _, m := range middleware { 212 m.Init(r.server) 213 } 214 r.globalMiddleware.middleware = append(r.globalMiddleware.middleware, middleware...) 215 return r 216 } 217 218 // Middleware apply one or more middleware to the route group. 219 func (r *Router) Middleware(middleware ...Middleware) *Router { 220 if r.middleware == nil { 221 r.middleware = make([]Middleware, 0, 3) 222 } 223 for _, m := range middleware { 224 m.Init(r.server) 225 } 226 r.middleware = append(r.middleware, middleware...) 227 return r 228 } 229 230 // CORS set the CORS options for this route group. 231 // If the options are not `nil`, the CORS middleware is automatically added globally. 232 // To disable CORS for this router, subrouters and routes, give `nil` options. 233 // CORS can be re-enabled for subrouters and routes on a case-by-case basis 234 // using non-nil options. 235 func (r *Router) CORS(options *cors.Options) *Router { 236 r.Meta[MetaCORS] = options 237 if options == nil { 238 return r 239 } 240 if !hasMiddleware[*corsMiddleware](r.globalMiddleware.middleware) { 241 r.GlobalMiddleware(&corsMiddleware{}) 242 } 243 return r 244 } 245 246 // StatusHandler set a handler for responses with an empty body. 247 // The handler will be automatically executed if the request's life-cycle reaches its end 248 // and nothing has been written in the response body. 249 // 250 // Multiple status codes can be given. The handler will be executed if one of them matches. 251 // 252 // This method can be used to define custom error handlers for example. 253 // 254 // Status handlers are inherited as a copy in sub-routers. Modifying a child's status handler 255 // will not modify its parent's. 256 // 257 // Codes in the 400 and 500 ranges have a default status handler. 258 func (r *Router) StatusHandler(handler StatusHandler, status int, additionalStatuses ...int) { 259 handler.Init(r.server) 260 r.statusHandlers[status] = handler 261 for _, s := range additionalStatuses { 262 r.statusHandlers[s] = handler 263 } 264 } 265 266 // ServeHTTP dispatches the handler registered in the matched route. 267 func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { 268 if req.URL.Scheme != "" && req.URL.Scheme != "http" { 269 address := r.server.getProxyAddress(r.server.config) + req.URL.Path 270 query := req.URL.Query() 271 if len(query) != 0 { 272 address += "?" + query.Encode() 273 } 274 http.Redirect(w, req, address, http.StatusPermanentRedirect) 275 return 276 } 277 278 match := routeMatch{currentPath: req.URL.Path} 279 r.match(req.Method, &match) 280 r.requestHandler(&match, w, req) 281 } 282 283 // TODO export RouteMatch and add Match with string param function 284 285 func (r *Router) match(method string, match *routeMatch) bool { 286 // Check if router itself matches 287 var params []string 288 if r.parameterizable.regex != nil { 289 i := -1 290 if len(match.currentPath) > 0 { 291 // Ignore slashes in router prefix 292 i = nthIndex(match.currentPath[1:], "/", r.slashCount) + 1 293 } 294 if i <= 0 { 295 i = len(match.currentPath) 296 } 297 currentPath := match.currentPath[:i] 298 params = r.parameterizable.regex.FindStringSubmatch(currentPath) 299 } else { 300 params = []string{""} 301 } 302 303 if params != nil { 304 match.trimCurrentPath(params[0]) 305 if len(params) > 1 { 306 match.mergeParams(r.makeParameters(params)) 307 } 308 309 // Check in subrouters first 310 for _, router := range r.subrouters { 311 if router.match(method, match) { 312 if router.prefix == "" && match.route == methodNotAllowedRoute { 313 // This allows route groups with subrouters having empty prefix. 314 continue 315 } 316 return true 317 } 318 } 319 320 // Check if any route matches 321 for _, route := range r.routes { 322 if route.match(method, match) { 323 return true 324 } 325 } 326 } 327 328 if match.err == errMatchMethodNotAllowed { 329 match.route = methodNotAllowedRoute 330 return true 331 } 332 333 match.route = notFoundRoute 334 // Return true if the subrouter matched so we don't turn back and check other subrouters 335 return params != nil && len(params[0]) > 0 336 } 337 338 func nthIndex(str, substr string, n int) int { 339 index := -1 340 for nth := 0; nth < n; nth++ { 341 i := strings.Index(str, substr) 342 if i == -1 || i == len(str) { 343 return -1 344 } 345 index += i + 1 346 str = str[i+1:] 347 } 348 return index 349 } 350 351 func (r *Router) makeParameters(match []string) map[string]string { 352 return r.parameterizable.makeParameters(match, r.parameters) 353 } 354 355 // Subrouter create a new sub-router from this router. 356 // Use subrouters to create route groups and to apply middleware to multiple routes. 357 // CORS options are also inherited. 358 // 359 // Subrouters are matched before routes. For example, if you have a subrouter with a 360 // prefix "/{name}" and a route "/route", the "/route" will never match. 361 func (r *Router) Subrouter(prefix string) *Router { 362 if prefix == "/" { 363 prefix = "" 364 } 365 366 router := &Router{ 367 server: r.server, 368 parent: r, 369 prefix: prefix, 370 statusHandlers: maps.Clone(r.statusHandlers), 371 Meta: make(map[string]any), 372 namedRoutes: r.namedRoutes, 373 routes: make([]*Route, 0, 5), // Typical CRUD has 5 routes 374 middlewareHolder: middlewareHolder{ 375 middleware: nil, 376 }, 377 globalMiddleware: r.globalMiddleware, 378 regexCache: r.regexCache, 379 } 380 if prefix != "" { 381 router.compileParameters(router.prefix, false, r.regexCache) 382 router.slashCount = strings.Count(prefix, "/") 383 } 384 r.subrouters = append(r.subrouters, router) 385 return router 386 } 387 388 // Group create a new sub-router with an empty prefix. 389 func (r *Router) Group() *Router { 390 return r.Subrouter("") 391 } 392 393 // Route register a new route. 394 // 395 // Multiple methods can be passed. 396 // 397 // If the route matches the "GET" method, the "HEAD" method is automatically added 398 // to the matcher if it's missing. 399 // 400 // If the router has the CORS middleware, the "OPTIONS" method is automatically added 401 // to the matcher if it's missing, so it allows preflight requests. 402 // 403 // Returns the generated route. 404 func (r *Router) Route(methods []string, uri string, handler Handler) *Route { 405 return r.registerRoute(methods, uri, handler) 406 } 407 408 // Get registers a new route with the GET and HEAD methods. 409 func (r *Router) Get(uri string, handler Handler) *Route { 410 return r.registerRoute([]string{http.MethodGet}, uri, handler) 411 } 412 413 // Post registers a new route with the POST method. 414 func (r *Router) Post(uri string, handler Handler) *Route { 415 return r.registerRoute([]string{http.MethodPost}, uri, handler) 416 } 417 418 // Put registers a new route with the PUT method. 419 func (r *Router) Put(uri string, handler Handler) *Route { 420 return r.registerRoute([]string{http.MethodPut}, uri, handler) 421 } 422 423 // Patch registers a new route with the PATCH method. 424 func (r *Router) Patch(uri string, handler Handler) *Route { 425 return r.registerRoute([]string{http.MethodPatch}, uri, handler) 426 } 427 428 // Delete registers a new route with the DELETE method. 429 func (r *Router) Delete(uri string, handler Handler) *Route { 430 return r.registerRoute([]string{http.MethodDelete}, uri, handler) 431 } 432 433 // Options registers a new route wit the OPTIONS method. 434 func (r *Router) Options(uri string, handler Handler) *Route { 435 return r.registerRoute([]string{http.MethodOptions}, uri, handler) 436 } 437 438 // Static serve a directory and its subdirectories of static resources. 439 // Set the "download" parameter to true if you want the files to be sent as an attachment 440 // instead of an inline element. 441 // 442 // If no file is given in the url, or if the given file is a directory, the handler will 443 // send the "index.html" file if it exists. 444 func (r *Router) Static(fs fs.StatFS, uri string, download bool) *Route { 445 return r.registerRoute([]string{http.MethodGet}, uri+"{resource:.*}", staticHandler(fs, download)) 446 } 447 448 func (r *Router) registerRoute(methods []string, uri string, handler Handler) *Route { 449 methodsSlice := slices.Clone(methods) 450 451 corsOptions, hasCORSOptions := r.LookupMeta(MetaCORS) 452 if hasCORSOptions && corsOptions != (*cors.Options)(nil) && !lo.Contains(methodsSlice, http.MethodOptions) { 453 methodsSlice = append(methodsSlice, http.MethodOptions) 454 } 455 456 if lo.Contains(methodsSlice, http.MethodGet) && !lo.Contains(methodsSlice, http.MethodHead) { 457 methodsSlice = append(methodsSlice, http.MethodHead) 458 } 459 460 if uri == "/" && r.parent != nil && !(r.prefix == "" && r.parent.parent == nil) { 461 uri = "" 462 } 463 464 route := &Route{ 465 name: "", 466 uri: uri, 467 methods: methodsSlice, 468 parent: r, 469 handler: handler, 470 Meta: make(map[string]any), 471 } 472 route.compileParameters(route.uri, true, r.regexCache) 473 r.routes = append(r.routes, route) 474 return route 475 } 476 477 // Controller register all routes for a controller implementing the `Registrer` interface. 478 // Automatically calls `Init()` and `RegisterRoutes()` on the given controller. 479 func (r *Router) Controller(controller Registrer) *Router { 480 controller.Init(r.server) 481 controller.RegisterRoutes(r) 482 return r 483 } 484 485 func (r *Router) requestHandler(match *routeMatch, w http.ResponseWriter, rawRequest *http.Request) { 486 request := NewRequest(rawRequest) 487 request.Route = match.route 488 request.RouteParams = lo.Ternary(match.parameters == nil, map[string]string{}, match.parameters) 489 response := NewResponse(r.server, request, w) 490 handler := match.route.handler 491 492 // Route-specific middleware is executed after router middleware 493 handler = match.route.applyMiddleware(handler) 494 495 parent := match.route.parent 496 for parent != nil { 497 handler = parent.applyMiddleware(handler) 498 parent = parent.parent 499 } 500 501 handler = r.globalMiddleware.applyMiddleware(handler) 502 503 handler(response, request) 504 505 if err := r.finalize(response, request); err != nil { 506 r.server.Logger.Error(err) 507 } 508 } 509 510 // finalize the request's life-cycle. 511 func (r *Router) finalize(response *Response, request *Request) error { 512 if response.empty { 513 if response.status == 0 { 514 // If the response is empty, return status 204 to 515 // comply with RFC 7231, 6.3.5 516 response.Status(http.StatusNoContent) 517 } else if statusHandler, ok := r.statusHandlers[response.status]; ok { 518 // Status has been set but body is empty. 519 // Execute status handler if exists. 520 statusHandler.Handle(response, request) 521 } 522 } 523 524 if !response.wroteHeader && !response.hijacked { 525 response.WriteHeader(response.status) 526 } 527 528 return errorutil.New(response.close()) 529 }