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  }