V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
zzhirong
V2EX  ›  Go 编程语言

有关 gin.Context.FileFromFS 的小坑

  •  
  •   zzhirong · 3 天前 · 1134 次点击

    以下是有问题的实验代码。

    package main
    
    /*  当前目录
    .
    ├── go.mod
    ├── go.sum
    ├── dist
    │   └── index.html
    ├── main.go
    */
    
    import (
    	"github.com/gin-gonic/gin"
    	"embed"
    	"net/http"
    )
    
    //go:embed dist
    var fs embed.FS
    
    func main() {
    	router := gin.Default()
    	router.GET("/", func(c *gin.Context){
    		c.FileFromFS("dist/index.html", http.FS(fs))
    	})
    	router.Run(":8080")
    }
    

    执行

    go run . &
    wget http://localhost:8080
    

    大家猜一下结果是什么?

    第 1 条附言  ·  3 天前

    问题出在

    c.FileFromFS("dist/index.html", http.FS(fs))
    

    c.FileFromFS 的第一个参数是以 “index.html”结尾,c.FileFromFS 会调用net/http.FileServer,而后者,在碰到以"index.html"结尾的路径的时候,会返回 301,Location 设置成 ./,然后,wget在收到 301 后,又继续访问跳转地址 /,然后就发生循环了。

    解决方案就是:

    • index.html 改名(不推荐)
    • 直接把"index.html"去掉(从"dist/index.html"改成`"dist/")

    以下是net/http.FileServer的帮助文档:

    go doc net/http.FileServer
    package http // import "net/http"
    
    func FileServer(root FileSystem) Handler
        FileServer returns a handler that serves HTTP requests with the contents of
        the file system rooted at root.
    
        As a special case, the returned file server redirects any request ending in
        "/index.html" to the same path, without the final "index.html".
    
        To use the operating system's file system implementation, use http.Dir:
    
            http.Handle("/", http.FileServer(http.Dir("/tmp")))
    
        To use an fs.FS implementation, use http.FileServerFS instead.
    
    第 2 条附言  ·  2 天前

    这里有点违反直觉的是,c.HTML(200, "dist/index.html", nil)c.File("dist/<只要不是index>.html") 是没问题的,但 c.File("dist/index.html") 就有问题,其实我后来想了下,FileServer 认为 //index.html 是等同的,所以也就不能把 / 映射成 /index.html ,但是我感觉这是一条隐性规则,单看 r.GET 或 c.File 的方法文档是无法知道的。

    11 条回复    2025-03-30 12:01:47 +08:00
    zzhirong
        1
    zzhirong  
    OP
       3 天前
    补充一点:帖子被贴上 “404 not found” 的标签了,应该是没有创建 ./dist/index.html 文件的缘故,但这里说的问题并不是这个低级错误。

    - wget http://localhost:8080 返回的是 301 ,而且我这边返回的是 20 次 301 (超过阀值强行退出)。
    tbxark
        2
    tbxark  
       3 天前
    试一下 sf, err := fs.Sub(fs, "dist")
    zzhirong
        3
    zzhirong  
    OP
       3 天前
    @tbxark 原因我昨天通过调试就已经知道了,就是先把问题代码贴上来,看看大家能否一眼就能看出,你提出的修改方案也还是会返回 301

    // 还是会返回 301
    dist, _ := fs.Sub(dist, "dist")
    router.GET("/", func(c *gin.Context){
    c.FileFromFS("index.html", http.FS(dist))
    })
    lovelylain
        4
    lovelylain  
       3 天前 via Android
    HandleContext 内部转发
    zzhirong
        5
    zzhirong  
    OP
       3 天前
    @lovelylain 什么意思?你的意思是 /dist/index.html 会内部转发到 / 么?但是,wget 确实收到了 301 ,然后一直有尝试重定向,然后一直收到 301 。
    kingcanfish
        6
    kingcanfish  
       3 天前
    package main
    import (
    "github.com/gin-gonic/gin"
    "net/http"
    )
    func main() {
    r := gin.Default()
    // 设置静态文件服务
    r.Static("/dist", "./dist")
    // 根路径重定向到 /dist/index.html
    r.GET("/", func(c *gin.Context) {
    c.Redirect( http.StatusMovedPermanently, "/dist/index.html")
    })
    // 启动服务器
    r.Run(":8080") // 默认监听 0.0.0.0:8080
    }
    这样呗
    你这样的用法确实感觉有点奇怪 ,属于是标准库没有想到用人会这么用 gin 也没想到有人会这么用
    zzhirong
        7
    zzhirong  
    OP
       3 天前
    @kingcanfish 你的方案会有两个 301 重定向,当你碰到首页不在根目录下的情况,你的最佳实践是什么,我目前的做法是

    r.GET("/", func(c *gin.Context) {
    c.File("dist/") // 会直接返回 dist/index.html ,没有重定向
    })

    把 / 映射到 index.html 应该很常见吧,有更好的方式么?
    kingcanfish
        8
    kingcanfish  
       2 天前
    @zzhirong #7 最佳实践就是用 nginx 做🤣, gin 只用来跑 api
    gvison
        9
    gvison  
       2 天前
    我是这样使用 gin 来做文件服务器获取 index.html ,访问 url 要求包括 go:embed 的目录 dist ,例如 wget http://localhost:8080/dist/index.html

    ```go
    package main

    import (
    "embed"
    "github.com/gin-gonic/gin"
    "github.com/go-dev-frame/sponge/pkg/gin/frontend"
    )

    //go:embed dist
    var staticFS embed.FS

    func main() {
    r := gin.Default()
    f := frontend.New("dist",
    frontend.WithEmbedFS(staticFS),
    frontend.With404ToHome(),
    )
    err := f.SetRouter(r)
    if err != nil {
    panic(err)
    }
    err = r.Run(":8080")
    panic(err)
    }
    ```
    zzhirong
        10
    zzhirong  
    OP
       2 天前
    @gvison 你的方案确实可以,但我不想引入额外的包而且我就想把主页挂在 / 下,有无办法只用 gin 实现?
    zzhirong
        11
    zzhirong  
    OP
       2 天前
    @gvison 看了下引用的 frontend 包的源码,发现 github.com/go-dev-frame/sponge/pkg/gin/frontend.FrontEnd.setEmbedFSRouter 有个新的实现方式

    staticFS, _ := fs.Sub(staticFS, "dist")
    r.GET("/*file", func(c *gin.Context){
    staticServer := http.FileServer( http.FS(staticFS))
    staticServer.ServeHTTP(c.Writer, c.Request)
    })

    这种方案也是可以的,直接 wget http://localhost:8080/ 这会返回 301
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   990 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 22:20 · PVG 06:20 · LAX 15:20 · JFK 18:20
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.