Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Gin router: path segment conflicts with existing wildcard

Tags:

go

go-gin

I want to make my app to serve below things.

  • a.com => serve /www to a browser so that the browser can seek /www/index.html)
  • a.com/js/mylib.js => serve /www/js/mylib.js to a browser
  • a.com/api/v1/disk => typical REST API which returns JSON
  • a.com/api/v1/memory => another API

I made a code like below:

package main

import "github.com/gin-gonic/gin"

func main() {
    r := gin.Default()

    r.Static("/", "/www")

    apiv1 := r.Group("api/v1")
    {
        apiv1.GET("/disk", diskSpaceHandler)
        apiv1.GET("/memory", memoryHandler)
        apiv1.GET("/cpu", cpuHandler)
    }

    r.Run(":80")
}

When I run the code, it panics:

panic: path segment '/api/v1/disk' conflicts with existing wildcard '/*filepath' in path '/api/v1/disk'

I understand why it panics, but I have no idea how to fix.

Just two things comes up in my mind:

  1. Use NoRoute() function which will handle other than /api/v1 group path(don't know how exactly I implement that)
  2. Use middleware. There is static middlewhere https://github.com/gin-gonic/contrib but the code is not working on Windows(https://github.com/gin-gonic/contrib/issues/91)

Thank you in advance.

like image 976
Xeph Avatar asked Apr 01 '16 13:04

Xeph


1 Answers

This is an intended feature of Gin's underlying router implementation. Routes are matched on prefixes, so any path param or wildcard in the same position as another existing path segment will result in a conflict.

In this particular Q&A, the method RouterGroup.Static adds a wildcard /*filepath to the route you serve from. If that route is root /, the wildcard will conflict with every other route you declare.

What to do then?

You must accept that there is no straightforward solution, because that's just how Gin's router implementation works. If you can't accept a workaround then you might have to change HTTP framework. The comments mention Echo as an alternative.

1. If you CAN change your route mappings

The best workaround is no workaround, and instead embracing Gin's design. Then you can simply add a unique prefix to the static files path: r.Static("/static", "/www"). This doesn't mean you change your local directory structure, only what path prefix gets mapped to it. Request URLs have to change.

2. Wildcard conflict with one or few other route

Let's say that your router has only these two routes:

/*any
/api/foo

In this case you might get away with a an intermediary handler and manually check the path param:

r.GET("/*any", func(c *gin.Context) {
    path := c.Param("any")
    if strings.HasPrefix(path, "/api") {
        apiHandler(c) // your regular api handler
    } else {
        // handle *any
    }
})

3. Wildcard conflict with many other routes

Which one is best depends on your specific situation and directory structure.

3.1 using r.NoRoute handler; this may work but is a bad hack.

r.NoRoute(gin.WrapH(http.FileServer(gin.Dir("static", false))))
r.GET("/api", apiHandler)

This will serve files from static (or whatever other dir) BUT it will also attempt to serve assets for all non-existing routes under the /api group. E.g. /api/xyz will be handled by NoRoute handler. This may be acceptable, until it isn't. E.g. if you just happen to have a folder named api among your static assets.

3.2 using a middleware;

For example, you can also find gin-contrib/static:

// declare before other routes
r.Use(static.Serve("/", static.LocalFile("www", false)))

This middleware is slightly more sophisticated, but it suffers from the same limitation. Namely:

  • if you have a dir named like your API route among the static assets, it will lead to infinite redirection (can mitigate with Engine#RedirectTrailingSlash = false)
  • even without infinite redirection, the middleware will check the local FS first, and only if it finds nothing will proceed to the next handler in chain. This means you are making a system call at each request to check if a file exists. (or at least this is what gin-contrib/static does, as shown below)
    r := gin.New()
    r.Use(func(c *gin.Context) {
        fname := "static" + c.Request.URL.Path
        if _, err := os.Stat(fname); err == nil {
            c.File(fname)
            c.Abort() // file found, stop the handler chain
        }
        // else move on to the next handler in chain
    })
    r.GET("/api", apiHandler)
    r.Run(":5555")

3.3 using a Gin sub-engine; this may be an OK choice if you have a lot of potential conflicts, e.g. a wildcard on / and complex API routes with groups and whatnot. Using a sub-engine will give you more control over this, but the implementation still feels hacky. An example based on Engine.HandleContext:

func main() {
    apiEngine := gin.New()
    apiG := apiEngine.Group("/api")
    {
        apiG.GET("/foo", func(c *gin.Context) { c.JSON(200, gin.H{"foo": true})})
        apiG.GET("/bar", func(c *gin.Context) { c.JSON(200, gin.H{"bar": true})})
    }

    r := gin.New()
    r.GET("/*any", func(c *gin.Context) {
        path := c.Param("any")
        if strings.HasPrefix(path, "/api") {
            apiEngine.HandleContext(c)
        } else {
            assetHandler(c)
        }
    })
    r.Run(":9955")
}

Wrap up

If you can, redesign your routes. If you can't, this answer presents three possible workarounds of increasing complexity. As always, the limitations may or may not apply to your specific use case. YMMV.

If none of this works for you, maybe leave a comment to point out what is your use case.

like image 59
blackgreen Avatar answered Oct 27 '22 18:10

blackgreen