Gin路由器:路径段与现有通配符冲突

huangapple go评论93阅读模式
英文:

Gin router: path segment conflicts with existing wildcard

问题

我想让我的应用程序提供以下功能:

  • a.com => 将/www提供给浏览器,以便浏览器可以访问/www/index.html
  • a.com/js/mylib.js => 将/www/js/mylib.js提供给浏览器
  • a.com/api/v1/disk => 典型的返回JSON的REST API
  • a.com/api/v1/memory => 另一个API

我编写了以下代码:

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")
}

当我运行这段代码时,它会出现错误:

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

我知道为什么会出错,但我不知道如何修复。

我脑海中浮现出两个解决方案:

  1. 使用NoRoute()函数来处理除/api/v1组路径之外的路径(不确定如何实现)。
  2. 使用中间件。有一个静态中间件https://github.com/gin-gonic/contrib,但该代码在Windows上无法工作(https://github.com/gin-gonic/contrib/issues/91)。

提前感谢你的帮助。

英文:

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.

答案1

得分: 13

这是Gin底层路由实现的一个预期特性。路由是基于前缀匹配的,因此在与另一个现有路径段处于相同位置的任何路径参数或通配符将导致冲突。

在这个特定的问答中,RouterGroup.Static 方法向你提供的路由添加了一个通配符 /*filepath。如果该路由是根路径 /,那么这个通配符将与你声明的每个其他路由发生冲突。

那么应该怎么办呢?

你必须接受没有直接的解决方案,因为这就是Gin的路由实现方式。如果你不能接受一个变通方法,那么你可能需要更换HTTP框架。评论中提到了Echo作为一个替代方案。

1. 如果你可以改变你的路由映射

最好的解决方法是没有解决方法,而是接受Gin的设计。然后你可以简单地给静态文件路径添加一个唯一的前缀:r.Static("/static", "/www")。这并不意味着你要改变本地目录结构,只是改变了映射到它的路径前缀。请求的URL将会改变。

2. 通配符与一个或少数其他路由冲突

假设你的路由只有这两个:

/*any
/api/foo

在这种情况下,你可以通过一个中间处理程序并手动检查路径参数来解决:

r.GET("/*any", func(c *gin.Context) {
    path := c.Param("any")
	if strings.HasPrefix(path, "/api") {
		apiHandler(c) // 你的常规API处理程序
	} else {
		// 处理 *any
	}
})

3. 通配符与许多其他路由冲突

哪种方法最好取决于你的具体情况和目录结构。

3.1 使用 r.NoRoute 处理程序;这可能有效,但是是一个不好的解决方法。

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

这将从 static(或其他目录)中提供文件,但是它还将尝试为 /api 组下的所有不存在的路由提供资源。例如,/api/xyz 将由 NoRoute 处理程序处理。这可能是可以接受的,直到它不可接受为止。例如,如果你的静态资源中恰好有一个名为 api 的文件夹。

3.2 使用中间件

例如,你还可以找到 gin-contrib/static

// 在其他路由之前声明
r.Use(static.Serve("/", static.LocalFile("www", false)))

这个中间件稍微复杂一些,但它也有相同的限制:

  • 如果你的静态资源中有一个与你的API路由同名的目录,它将导致无限重定向(可以通过 Engine#RedirectTrailingSlash = false 来缓解)
  • 即使没有无限重定向,中间件也会首先检查本地文件系统,只有在找不到文件时才会继续处理链中的下一个处理程序。这意味着你每次请求都会进行一次系统调用来检查文件是否存在。(至少这是 gin-contrib/static 的做法,如下所示)
	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() // 找到文件,停止处理程序链
		}
		// 否则继续处理链中的下一个处理程序
	})
	r.GET("/api", apiHandler)
	r.Run(":5555")

3.3 使用Gin子引擎;如果你有很多潜在的冲突,例如在 / 上使用通配符和具有组和其他内容的复杂API路由,这可能是一个可以接受的选择。使用子引擎将使你对此有更多的控制,但实现仍然感觉像是一个hack。下面是一个基于 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")
}

总结

如果可以的话,重新设计你的路由。如果不能,这个答案提供了三种可能的解决方法,复杂度逐渐增加。如常,这些限制可能适用于你的具体用例,也可能不适用。具体情况可能有所不同。

如果以上方法对你都不起作用,也许可以留下评论,指出你的用例是什么。

英文:

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.

答案2

得分: -3

你应该使用静态中间件,可以参考以下示例:

https://github.com/gin-contrib/static#canonical-example

r.Use(static.Serve("/", static.LocalFile("/tmp", false)))
英文:

you should use static middleware, see its example:

https://github.com/gin-contrib/static#canonical-example

r.Use(static.Serve("/", static.LocalFile("/tmp", false)))

huangapple
  • 本文由 发表于 2016年4月1日 21:37:10
  • 转载请务必保留本文链接:https://go.coder-hub.com/36357791.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定