github.com/gofiber/fiber/v2@v2.47.0/router.go (about) 1 // ⚡️ Fiber is an Express inspired web framework written in Go with ☕️ 2 // 🤖 Github Repository: https://github.com/gofiber/fiber 3 // 📌 API Documentation: https://docs.gofiber.io 4 5 package fiber 6 7 import ( 8 "fmt" 9 "sort" 10 "strconv" 11 "strings" 12 "sync/atomic" 13 "time" 14 15 "github.com/gofiber/fiber/v2/utils" 16 17 "github.com/valyala/fasthttp" 18 ) 19 20 // Router defines all router handle interface, including app and group router. 21 type Router interface { 22 Use(args ...interface{}) Router 23 24 Get(path string, handlers ...Handler) Router 25 Head(path string, handlers ...Handler) Router 26 Post(path string, handlers ...Handler) Router 27 Put(path string, handlers ...Handler) Router 28 Delete(path string, handlers ...Handler) Router 29 Connect(path string, handlers ...Handler) Router 30 Options(path string, handlers ...Handler) Router 31 Trace(path string, handlers ...Handler) Router 32 Patch(path string, handlers ...Handler) Router 33 34 Add(method, path string, handlers ...Handler) Router 35 Static(prefix, root string, config ...Static) Router 36 All(path string, handlers ...Handler) Router 37 38 Group(prefix string, handlers ...Handler) Router 39 40 Route(prefix string, fn func(router Router), name ...string) Router 41 42 Mount(prefix string, fiber *App) Router 43 44 Name(name string) Router 45 } 46 47 // Route is a struct that holds all metadata for each registered handler. 48 type Route struct { 49 // always keep in sync with the copy method "app.copyRoute" 50 // Data for routing 51 pos uint32 // Position in stack -> important for the sort of the matched routes 52 use bool // USE matches path prefixes 53 mount bool // Indicated a mounted app on a specific route 54 star bool // Path equals '*' 55 root bool // Path equals '/' 56 path string // Prettified path 57 routeParser routeParser // Parameter parser 58 group *Group // Group instance. used for routes in groups 59 60 // Public fields 61 Method string `json:"method"` // HTTP method 62 Name string `json:"name"` // Route's name 63 //nolint:revive // Having both a Path (uppercase) and a path (lowercase) is fine 64 Path string `json:"path"` // Original registered route path 65 Params []string `json:"params"` // Case sensitive param keys 66 Handlers []Handler `json:"-"` // Ctx handlers 67 } 68 69 func (r *Route) match(detectionPath, path string, params *[maxParams]string) bool { 70 // root detectionPath check 71 if r.root && detectionPath == "/" { 72 return true 73 // '*' wildcard matches any detectionPath 74 } else if r.star { 75 if len(path) > 1 { 76 params[0] = path[1:] 77 } else { 78 params[0] = "" 79 } 80 return true 81 } 82 // Does this route have parameters 83 if len(r.Params) > 0 { 84 // Match params 85 if match := r.routeParser.getMatch(detectionPath, path, params, r.use); match { 86 // Get params from the path detectionPath 87 return match 88 } 89 } 90 // Is this route a Middleware? 91 if r.use { 92 // Single slash will match or detectionPath prefix 93 if r.root || strings.HasPrefix(detectionPath, r.path) { 94 return true 95 } 96 // Check for a simple detectionPath match 97 } else if len(r.path) == len(detectionPath) && r.path == detectionPath { 98 return true 99 } 100 // No match 101 return false 102 } 103 104 func (app *App) next(c *Ctx) (bool, error) { 105 // Get stack length 106 tree, ok := app.treeStack[c.methodINT][c.treePath] 107 if !ok { 108 tree = app.treeStack[c.methodINT][""] 109 } 110 lenTree := len(tree) - 1 111 112 // Loop over the route stack starting from previous index 113 for c.indexRoute < lenTree { 114 // Increment route index 115 c.indexRoute++ 116 117 // Get *Route 118 route := tree[c.indexRoute] 119 120 var match bool 121 var err error 122 // skip for mounted apps 123 if route.mount { 124 continue 125 } 126 127 // Check if it matches the request path 128 match = route.match(c.detectionPath, c.path, &c.values) 129 if !match { 130 // No match, next route 131 continue 132 } 133 // Pass route reference and param values 134 c.route = route 135 136 // Non use handler matched 137 if !c.matched && !route.use { 138 c.matched = true 139 } 140 141 // Execute first handler of route 142 c.indexHandler = 0 143 if len(route.Handlers) > 0 { 144 err = route.Handlers[0](c) 145 } 146 return match, err // Stop scanning the stack 147 } 148 149 // If c.Next() does not match, return 404 150 err := NewError(StatusNotFound, "Cannot "+c.method+" "+c.pathOriginal) 151 if !c.matched && app.methodExist(c) { 152 // If no match, scan stack again if other methods match the request 153 // Moved from app.handler because middleware may break the route chain 154 err = ErrMethodNotAllowed 155 } 156 return false, err 157 } 158 159 func (app *App) handler(rctx *fasthttp.RequestCtx) { //revive:disable-line:confusing-naming // Having both a Handler() (uppercase) and a handler() (lowercase) is fine. TODO: Use nolint:revive directive instead. See https://github.com/golangci/golangci-lint/issues/3476 160 // Acquire Ctx with fasthttp request from pool 161 c := app.AcquireCtx(rctx) 162 defer app.ReleaseCtx(c) 163 164 // handle invalid http method directly 165 if c.methodINT == -1 { 166 _ = c.Status(StatusBadRequest).SendString("Invalid http method") //nolint:errcheck // It is fine to ignore the error here 167 return 168 } 169 170 // Find match in stack 171 match, err := app.next(c) 172 if err != nil { 173 if catch := c.app.ErrorHandler(c, err); catch != nil { 174 _ = c.SendStatus(StatusInternalServerError) //nolint:errcheck // It is fine to ignore the error here 175 } 176 // TODO: Do we need to return here? 177 } 178 // Generate ETag if enabled 179 if match && app.config.ETag { 180 setETag(c, false) 181 } 182 } 183 184 func (app *App) addPrefixToRoute(prefix string, route *Route) *Route { 185 prefixedPath := getGroupPath(prefix, route.Path) 186 prettyPath := prefixedPath 187 // Case sensitive routing, all to lowercase 188 if !app.config.CaseSensitive { 189 prettyPath = utils.ToLower(prettyPath) 190 } 191 // Strict routing, remove trailing slashes 192 if !app.config.StrictRouting && len(prettyPath) > 1 { 193 prettyPath = utils.TrimRight(prettyPath, '/') 194 } 195 196 route.Path = prefixedPath 197 route.path = RemoveEscapeChar(prettyPath) 198 route.routeParser = parseRoute(prettyPath) 199 route.root = false 200 route.star = false 201 202 return route 203 } 204 205 func (*App) copyRoute(route *Route) *Route { 206 return &Route{ 207 // Router booleans 208 use: route.use, 209 mount: route.mount, 210 star: route.star, 211 root: route.root, 212 213 // Path data 214 path: route.path, 215 routeParser: route.routeParser, 216 Params: route.Params, 217 218 // misc 219 pos: route.pos, 220 221 // Public data 222 Path: route.Path, 223 Method: route.Method, 224 Handlers: route.Handlers, 225 } 226 } 227 228 func (app *App) register(method, pathRaw string, group *Group, handlers ...Handler) { 229 // Uppercase HTTP methods 230 method = utils.ToUpper(method) 231 // Check if the HTTP method is valid unless it's USE 232 if method != methodUse && app.methodInt(method) == -1 { 233 panic(fmt.Sprintf("add: invalid http method %s\n", method)) 234 } 235 // is mounted app 236 isMount := group != nil && group.app != app 237 // A route requires atleast one ctx handler 238 if len(handlers) == 0 && !isMount { 239 panic(fmt.Sprintf("missing handler in route: %s\n", pathRaw)) 240 } 241 // Cannot have an empty path 242 if pathRaw == "" { 243 pathRaw = "/" 244 } 245 // Path always start with a '/' 246 if pathRaw[0] != '/' { 247 pathRaw = "/" + pathRaw 248 } 249 // Create a stripped path in-case sensitive / trailing slashes 250 pathPretty := pathRaw 251 // Case sensitive routing, all to lowercase 252 if !app.config.CaseSensitive { 253 pathPretty = utils.ToLower(pathPretty) 254 } 255 // Strict routing, remove trailing slashes 256 if !app.config.StrictRouting && len(pathPretty) > 1 { 257 pathPretty = utils.TrimRight(pathPretty, '/') 258 } 259 // Is layer a middleware? 260 isUse := method == methodUse 261 // Is path a direct wildcard? 262 isStar := pathPretty == "/*" 263 // Is path a root slash? 264 isRoot := pathPretty == "/" 265 // Parse path parameters 266 parsedRaw := parseRoute(pathRaw) 267 parsedPretty := parseRoute(pathPretty) 268 269 // Create route metadata without pointer 270 route := Route{ 271 // Router booleans 272 use: isUse, 273 mount: isMount, 274 star: isStar, 275 root: isRoot, 276 277 // Path data 278 path: RemoveEscapeChar(pathPretty), 279 routeParser: parsedPretty, 280 Params: parsedRaw.params, 281 282 // Group data 283 group: group, 284 285 // Public data 286 Path: pathRaw, 287 Method: method, 288 Handlers: handlers, 289 } 290 // Increment global handler count 291 atomic.AddUint32(&app.handlersCount, uint32(len(handlers))) 292 293 // Middleware route matches all HTTP methods 294 if isUse { 295 // Add route to all HTTP methods stack 296 for _, m := range app.config.RequestMethods { 297 // Create a route copy to avoid duplicates during compression 298 r := route 299 app.addRoute(m, &r, isMount) 300 } 301 } else { 302 // Add route to stack 303 app.addRoute(method, &route, isMount) 304 } 305 } 306 307 func (app *App) registerStatic(prefix, root string, config ...Static) { 308 // For security we want to restrict to the current work directory. 309 if root == "" { 310 root = "." 311 } 312 // Cannot have an empty prefix 313 if prefix == "" { 314 prefix = "/" 315 } 316 // Prefix always start with a '/' or '*' 317 if prefix[0] != '/' { 318 prefix = "/" + prefix 319 } 320 // in case sensitive routing, all to lowercase 321 if !app.config.CaseSensitive { 322 prefix = utils.ToLower(prefix) 323 } 324 // Strip trailing slashes from the root path 325 if len(root) > 0 && root[len(root)-1] == '/' { 326 root = root[:len(root)-1] 327 } 328 // Is prefix a direct wildcard? 329 isStar := prefix == "/*" 330 // Is prefix a root slash? 331 isRoot := prefix == "/" 332 // Is prefix a partial wildcard? 333 if strings.Contains(prefix, "*") { 334 // /john* -> /john 335 isStar = true 336 prefix = strings.Split(prefix, "*")[0] 337 // Fix this later 338 } 339 prefixLen := len(prefix) 340 if prefixLen > 1 && prefix[prefixLen-1:] == "/" { 341 // /john/ -> /john 342 prefixLen-- 343 prefix = prefix[:prefixLen] 344 } 345 const cacheDuration = 10 * time.Second 346 // Fileserver settings 347 fs := &fasthttp.FS{ 348 Root: root, 349 AllowEmptyRoot: true, 350 GenerateIndexPages: false, 351 AcceptByteRange: false, 352 Compress: false, 353 CompressedFileSuffix: app.config.CompressedFileSuffix, 354 CacheDuration: cacheDuration, 355 IndexNames: []string{"index.html"}, 356 PathRewrite: func(fctx *fasthttp.RequestCtx) []byte { 357 path := fctx.Path() 358 if len(path) >= prefixLen { 359 if isStar && app.getString(path[0:prefixLen]) == prefix { 360 path = append(path[0:0], '/') 361 } else { 362 path = path[prefixLen:] 363 if len(path) == 0 || path[len(path)-1] != '/' { 364 path = append(path, '/') 365 } 366 } 367 } 368 if len(path) > 0 && path[0] != '/' { 369 path = append([]byte("/"), path...) 370 } 371 return path 372 }, 373 PathNotFound: func(fctx *fasthttp.RequestCtx) { 374 fctx.Response.SetStatusCode(StatusNotFound) 375 }, 376 } 377 378 // Set config if provided 379 var cacheControlValue string 380 var modifyResponse Handler 381 if len(config) > 0 { 382 maxAge := config[0].MaxAge 383 if maxAge > 0 { 384 cacheControlValue = "public, max-age=" + strconv.Itoa(maxAge) 385 } 386 fs.CacheDuration = config[0].CacheDuration 387 fs.Compress = config[0].Compress 388 fs.AcceptByteRange = config[0].ByteRange 389 fs.GenerateIndexPages = config[0].Browse 390 if config[0].Index != "" { 391 fs.IndexNames = []string{config[0].Index} 392 } 393 modifyResponse = config[0].ModifyResponse 394 } 395 fileHandler := fs.NewRequestHandler() 396 handler := func(c *Ctx) error { 397 // Don't execute middleware if Next returns true 398 if len(config) != 0 && config[0].Next != nil && config[0].Next(c) { 399 return c.Next() 400 } 401 // Serve file 402 fileHandler(c.fasthttp) 403 // Sets the response Content-Disposition header to attachment if the Download option is true 404 if len(config) > 0 && config[0].Download { 405 c.Attachment() 406 } 407 // Return request if found and not forbidden 408 status := c.fasthttp.Response.StatusCode() 409 if status != StatusNotFound && status != StatusForbidden { 410 if len(cacheControlValue) > 0 { 411 c.fasthttp.Response.Header.Set(HeaderCacheControl, cacheControlValue) 412 } 413 if modifyResponse != nil { 414 return modifyResponse(c) 415 } 416 return nil 417 } 418 // Reset response to default 419 c.fasthttp.SetContentType("") // Issue #420 420 c.fasthttp.Response.SetStatusCode(StatusOK) 421 c.fasthttp.Response.SetBodyString("") 422 // Next middleware 423 return c.Next() 424 } 425 426 // Create route metadata without pointer 427 route := Route{ 428 // Router booleans 429 use: true, 430 root: isRoot, 431 path: prefix, 432 // Public data 433 Method: MethodGet, 434 Path: prefix, 435 Handlers: []Handler{handler}, 436 } 437 // Increment global handler count 438 atomic.AddUint32(&app.handlersCount, 1) 439 // Add route to stack 440 app.addRoute(MethodGet, &route) 441 // Add HEAD route 442 app.addRoute(MethodHead, &route) 443 } 444 445 func (app *App) addRoute(method string, route *Route, isMounted ...bool) { 446 // Check mounted routes 447 var mounted bool 448 if len(isMounted) > 0 { 449 mounted = isMounted[0] 450 } 451 452 // Get unique HTTP method identifier 453 m := app.methodInt(method) 454 455 // prevent identically route registration 456 l := len(app.stack[m]) 457 if l > 0 && app.stack[m][l-1].Path == route.Path && route.use == app.stack[m][l-1].use && !route.mount && !app.stack[m][l-1].mount { 458 preRoute := app.stack[m][l-1] 459 preRoute.Handlers = append(preRoute.Handlers, route.Handlers...) 460 } else { 461 // Increment global route position 462 route.pos = atomic.AddUint32(&app.routesCount, 1) 463 route.Method = method 464 // Add route to the stack 465 app.stack[m] = append(app.stack[m], route) 466 app.routesRefreshed = true 467 } 468 469 // Execute onRoute hooks & change latestRoute if not adding mounted route 470 if !mounted { 471 app.mutex.Lock() 472 app.latestRoute = route 473 if err := app.hooks.executeOnRouteHooks(*route); err != nil { 474 panic(err) 475 } 476 app.mutex.Unlock() 477 } 478 } 479 480 // buildTree build the prefix tree from the previously registered routes 481 func (app *App) buildTree() *App { 482 if !app.routesRefreshed { 483 return app 484 } 485 486 // loop all the methods and stacks and create the prefix tree 487 for m := range app.config.RequestMethods { 488 tsMap := make(map[string][]*Route) 489 for _, route := range app.stack[m] { 490 treePath := "" 491 if len(route.routeParser.segs) > 0 && len(route.routeParser.segs[0].Const) >= 3 { 492 treePath = route.routeParser.segs[0].Const[:3] 493 } 494 // create tree stack 495 tsMap[treePath] = append(tsMap[treePath], route) 496 } 497 app.treeStack[m] = tsMap 498 } 499 500 // loop the methods and tree stacks and add global stack and sort everything 501 for m := range app.config.RequestMethods { 502 tsMap := app.treeStack[m] 503 for treePart := range tsMap { 504 if treePart != "" { 505 // merge global tree routes in current tree stack 506 tsMap[treePart] = uniqueRouteStack(append(tsMap[treePart], tsMap[""]...)) 507 } 508 // sort tree slices with the positions 509 slc := tsMap[treePart] 510 sort.Slice(slc, func(i, j int) bool { return slc[i].pos < slc[j].pos }) 511 } 512 } 513 app.routesRefreshed = false 514 515 return app 516 }