gin-gonic / gin

Gin is a HTTP web framework written in Go (Golang). It features a Martini-like API with much better performance -- up to 40 times faster. If you need smashing performance, get yourself some Gin.
https://gin-gonic.com/
MIT License
76.96k stars 7.92k forks source link

Cannot use (*gin.Context).FileFromFS("index.html", fs) to serve any file named index.html #2654

Open Tnze opened 3 years ago

Tnze commented 3 years ago

Description

It's not possible to serve any static file named index.html in a FileSystem.

How to reproduce

// The project fold:
// +-- www/
// |  +-- css/
// |  +-- src/
// |  |  +-- index.js
// |  +-- index.html
// +-- server.go <- This file

package main

import (
    "log"
    "github.com/gin-gonic/gin"
)

//go:embed www
var static embed.FS

func main() {
    r := gin.Default()
    r.StaticFS("/css", mustStaticFS("www/css"))
    r.StaticFS("/src", mustStaticFS("www/src"))
    r.Any("/", func(c *gin.Context) {
        c.FileFromFS("www/index.html", http.FS(static))
    })
    if err := r.Run(); err != nil {
        log.Panic(err)
    }
}

Expectations

$ curl http://localhost:8080/ # Expect with status 200
<html>the content of www/index.html</html>

Actual result

Infinite 301 redirection

[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      "/"

Environment

archekb commented 3 years ago

Hi! Fast solution: rename your index.html to index.htm, use c.FileFromFS("www/index.htm", http.FS(static)) and all will works.

Problem in net/http FileServer https://github.com/golang/go/blob/a7e16abb22f1b249d2691b32a5d20206282898f2/src/net/http/fs.go#L587 if in folder we found index.html, then redirect to /

honmaple commented 2 years ago

Using c.FileFromFS("www/", http.FS(static)) also work well.

SharkFourSix commented 1 year ago

Hi! Fast solution: rename your index.html to index.htm, use c.FileFromFS("www/index.htm", http.FS(static)) and all will works.

Problem in net/http FileServer https://github.com/golang/go/blob/a7e16abb22f1b249d2691b32a5d20206282898f2/src/net/http/fs.go#L587 if in folder we found index.html, then redirect to /

Hard-coding values like this is very bad. Two years later and hasn't been fixed.

zanjie1999 commented 1 year ago

use r.StaticFileFS("/", "./index.html", http.FS(staticFs)) like r.StaticFileFS("/", "./", http.FS(staticFs)) work fine

dangjinghao commented 1 month ago

The reason:

c.FileFromFS wraps the function http.FileServer(fs).ServeHTTP(...)

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

And http.FileServer function has a special case:

// ...
// As a special case, the returned file server redirects any request
// ending in "/index.html" to the same path, without the final
// "index.html".
// ...
func FileServer(root FileSystem) Handler {
    return &fileHandler{root}
}

As you can see, the implementation of c.FileFromFS is just assign filepath to http.Request,URL.Path and then calls serverHttp, ServeHTTP calls serveFile. If you serve a path ends with index.html, FileFromFS will automatically redirect to "./"

// name is '/'-separated, not filepath.Separator.
func serveFile(w ResponseWriter, r *Request, fs FileSystem, name string, redirect bool) {
    const indexPage = "/index.html"

    // redirect .../index.html to .../
    // can't use Redirect() because that would make the path absolute,
    // which would be a problem running under StripPrefix
    if strings.HasSuffix(r.URL.Path, indexPage) {
        localRedirect(w, r, "./")
        return
    }
//...

In your case,

By the way, serveFile supports reading directory by reading index.html in this directory:

// ...
    if d.IsDir() {
        url := r.URL.Path
        // redirect if the directory name doesn't end in a slash
        if url == "" || url[len(url)-1] != '/' {
            localRedirect(w, r, path.Base(url)+"/")
            return
        }

        // use contents of index.html for directory, if present
        index := strings.TrimSuffix(name, "/") + indexPage //indexPage is "/index.html"
        ff, err := fs.Open(index)
        if err == nil {
            defer ff.Close()
            dd, err := ff.Stat()
            if err == nil {
                d = dd
                f = ff
            }
        }
    }
// ...

so @honmaple 's comment is good and easy.