场景解释
关联issue
我的一个go语言项目中使用了gin框架,并使用go的embed功能嵌入了前端页面。由于这个项目同时需要提供静态文件服务(前端编译产物)和API,经过查询后发现了gin有FileFromFS
和StaticFileFS
两个API,都用于从FileSystem
对象中返回特定路径文件的内容。
在具体使用时发现了一个特定情况下的问题,如上方issue提出的,当在根目录提供index.html
时就可能触发。
例如
1 2 3 r.NoRoute(func (c *gin.Context) { c.FileFromFS("/index.html" , frontend.FrontendRootFS()) })
或者
1 2 3 4 r.Any("/" , func (c *gin.Context) { c.FileFromFS("www/index.html" , http.FS(static)) })
使用r.StaticFileFS
可能也会出现?它的实现包装了FileFromFS
从而导致了该issue提到的反复重定向到根目录的问题。当然我已经在此issue中的回复作出了简要分析,在这里,因为个人认为比较有用,所以我打算把我当时的回复搬过来到博客中并进行详细一些的分析。
考虑issue中的gin代码
1 2 3 4 5 6 var static embed.FS r.Any("/" , func (c *gin.Context) { c.FileFromFS("www/index.html" , http.FS(static)) })
其大概作用来看,就是从embed的FS中获取www/index.html
的文件并将其作为响应体响应对/根目录的请求。但是在尝试时导致了不断地重定向到/
1 2 3 4 5 6 7 8 9 10 11 12 13 [GIN] 2021/03/14 - 20:17:18 | 301 | 0s | 127.0.0.1 | GET "/" [GIN] 2021/03/14 - 20:17:18 | 301 | 0s | 127.0.0.1 | GET "/" [GIN] 2021/03/14 - 20:17:18 | 301 | 0s | 127.0.0.1 | GET "/" [GIN] 2021/03/14 - 20:17:18 | 301 | 0s | 127.0.0.1 | GET "/" [GIN] 2021/03/14 - 20:17:18 | 301 | 0s | 127.0.0.1 | GET "/" [GIN] 2021/03/14 - 20:17:18 | 301 | 0s | 127.0.0.1 | GET "/" [GIN] 2021/03/14 - 20:17:18 | 301 | 0s | 127.0.0.1 | GET "/" [GIN] 2021/03/14 - 20:17:18 | 301 | 0s | 127.0.0.1 | GET "/" [GIN] 2021/03/14 - 20:17:18 | 301 | 0s | 127.0.0.1 | GET "/" [GIN] 2021/03/14 - 20:17:18 | 301 | 0s | 127.0.0.1 | GET "/" [GIN] 2021/03/14 - 20:17:18 | 301 | 0s | 127.0.0.1 | GET "/" [GIN] 2021/03/14 - 20:17:18 | 301 | 0s | 127.0.0.1 | GET "/" [GIN] 2021/03/14 - 20:17:18 | 301 | 0s | 127.0.0.1 | GET "/"
这是由于FileFromFS
的底层实现依赖导致,接下来我会通过简单地自顶向下地分析其处理流程,来查看为什么它会导致这个结果。
FileFromFS实现分析 FileFromFS
的实现只有这么短
1 2 3 4 5 6 7 8 9 func (c *Context) FileFromFS(filepath string , fs http.FileSystem) { defer func (old string ) { c.Request.URL.Path = old }(c.Request.URL.Path) c.Request.URL.Path = filepath http.FileServer(fs).ServeHTTP(c.Writer, c.Request) }
其中,主要用于修改Request.URL.Path
,并通过defer在函数调用结束后恢复Request.URL.Path
,FileServer
的定义是
1 2 3 func FileServer (root FileSystem) Handler { return &fileHandler{root} }
可以看到通过通过传入http.FileServer
来构造了一个http.Handler
的interface
。在这个FileServer
的函数上有注释
这其实足够说明为什么会产生这个问题了:所有以/index.html
结尾的请求都会被重定向到index.html
所在目录。
但是想要知道具体是怎么实现的,那就需要继续往下翻了。
http.FileServer.ServeHTTP
调用的其实是位于http
包的http.fileHandler.ServeHTTP
1 2 3 4 5 6 7 8 func (f *fileHandler) ServeHTTP(w ResponseWriter, r *Request) { upath := r.URL.Path if !strings.HasPrefix(upath, "/" ) { upath = "/" + upath r.URL.Path = upath } serveFile(w, r, f.root, path.Clean(upath), true ) }
可以看到当前的ServeHTTP内容是对Request.URL.Path
进行预处理:给没有以/
开头的路径加上/
接下来进入serveFile,serveFile比较长,所以只摘一部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 func serveFile (w ResponseWriter, r *Request, fs FileSystem, name string , redirect bool ) { const indexPage = "/index.html" if strings.HasSuffix(r.URL.Path, indexPage) { localRedirect(w, r, "./" ) return } if d.IsDir() { url := r.URL.Path if url == "" || url[len (url)-1 ] != '/' { localRedirect(w, r, path.Base(url)+"/" ) return } index := strings.TrimSuffix(name, "/" ) + indexPage ff, err := fs.Open(index) if err == nil { defer ff.Close() dd, err := ff.Stat() if err == nil { d = dd f = ff } } } }
这两块就是产生我们会遇到的问题的核心原因:如果r.URL.Path
以/index.html
结尾,则此函数会设置重定向的请求头为./
从而重定向到index.html
所在目录.../
。当客户端再次发起请求时函数通过拼接字符串来返回.../index.html
的内容serveFile
的预期行为是:
当请求某个目录下的index.html
文件时要求重定向到index.html
所在目录
在函数内部请求目录时手动拼接成.../index.html
形式
正常返回index.html
内容
可能是因为想要简洁或者别的什么原因,最终就这么实现了。
但是由于gin的实现产生了目录映射不一致问题,因为它的实现只是简单地赋值URL的Path,获取文件内容后再恢复URL的Path。
例如,考虑用gin.FileFromFS实现以下映射
.../
-> www/index.html
in FS
1 2 3 4 5 r.Any("...path1/" , func (c *gin.Context) { c.FileFromFS("...path2/index.html" , FS) })
此时由于serveFile
和gin
的共同作用,当请求...path1/
的时候,gin调用serveFile
函数,但是把请求的路径修改为/...path2/index.html
,当serveFile
处理时,发现此路径以index.html
结尾,所以写入响应头要求重定向到./
,serveFile
原本期望重定向到/...path2/
,再次发起请求到路径时再返回index.html
。但是实际上客户端请求的是...path1/
,重定向到./
还是...path1/
,而经过gin
时会重复此过程,表现出来的就是不断进行重定向。
解决方案 说实话,解决方案比较简单,但是个人感觉不够优雅。
对于任何想要利用FileFromFS
和类似的调用FS的gin
的函数,当需要返回index.html
的内容时,filepath
填为index.html
所在目录而不是直接填index.html
,即可解决问题。例如此issue提到的可以修改为
1 2 3 4 r.Any("/" , func (c *gin.Context) { c.FileFromFS("www/" , http.FS(static)) })
由于直接发起的请求是路径,所以不会产生重定向的行为,而是在ServeFile内拼接成www/index.html
再返回对应路劲的内容,问题就这么解决了。