Home > Mobile >  Filtering embed.FS causes ERR_TOO_MANY_REDIRECTS on HTTP server
Filtering embed.FS causes ERR_TOO_MANY_REDIRECTS on HTTP server

Time:12-08

My application runs an HTTP server that serves some static files. Most files are reachable under /static/ but some, like index.html, must be reachable at the root.

This code tries to implement that by embedding the files in an embed.FS (for demonstration, I'm only embedding index.html here):

package main

import (
    "net/http"
    "embed"
    "io/fs"
    "log"
)

//go:embed index.html
var files embed.FS

type primaryFiles struct {}

func (pf *primaryFiles) Open(name string) (fs.File, error) {
    // name will be "." for paths / and /index.html, I guess that's a feature
    if name == "." {
        return files.Open("index.html")
    }
    return nil, fs.ErrNotExist
}

func main() {
    http.Handle("/", http.FileServer(http.FS(&primaryFiles{})))
    http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(files))))
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Now when running the code, I can query the index.html just fine at both http://localhost:8080/static/ and http://localhost:8080/static/index.html. However, at http://localhost:8080/ and http://localhost:8080/index.html, the browser will give me ERR_TOO_MANY_REDIRECTS. Why is that happening? How can I fix it?

I already tried to hand through the ".", which yields a file listing instead of the index.html content. I'm on go version go1.17.3 darwin/arm64. I also tried to figure out what's happening with curl:

$ curl -v http://localhost:8080/index.html
*   Trying ::1:8080...
* Connected to localhost (::1) port 8080 (#0)
> GET /index.html HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.77.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 301 Moved Permanently
< Location: ./
< Date: Mon, 06 Dec 2021 22:05:50 GMT
< Content-Length: 0
<
* Connection #0 to host localhost left intact

$ curl -v http://localhost:8080/
*   Trying ::1:8080...
* Connected to localhost (::1) port 8080 (#0)
> GET / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.77.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 301 Moved Permanently
< Location: ..//
< Date: Mon, 06 Dec 2021 22:05:12 GMT
< Content-Length: 0
<
* Connection #0 to host localhost left intact

This doesn't help me understand what's happening – okay. /index.html is redirected to ./, that seems to make sense. But / being redirected to ..// … I don't know what to make of that.

CodePudding user response:

Your primaryFiles.Open implementation, when given ".", returns a file rather than a dir. This is an error.

The documentation on fs.FS.Open should lead you to fs.ValidPath whose godoc states the following.

package fs // import "io/fs"

func ValidPath(name string) bool
ValidPath reports whether the given path name is valid for use in a call to Open.

Path names passed to open are UTF-8-encoded, unrooted, slash-separated sequences of path elements, like “x/y/z”. Path names must not contain an element that is “.” or “..” or the empty string, except for the special case that the root directory is named “.”. Paths must not start or end with a slash: “/x” and “x/” are invalid.

Note that paths are slash-separated on all systems, even Windows. Paths containing other characters such as backslash and colon are accepted as valid, but those characters must never be interpreted by an FS implementation as path element separators.

net/http.FileServer is banking on the fact that recursively redirecting on ../ should eventually get to some directory, but there is no directory to be found based on the way your primaryFiles.Open works. It could be argued that this is an opportunity to enhance net/http.FileServer, but it's not clear.

CodePudding user response:

What's happening is partly documented in the http package:

As a special case, the returned file server redirects any request ending in "/index.html" to the same path, without the final "index.html".

So any request to index.html is not seen in my Open fn, since it redirects to ..

What the documentation does not tell that a request to . seems to be handled as follows:

  • the directory for . is queried via Open. This should return a directory, which my original code did not and caused the error.
  • if a directory is returned, it is searched for a file index.html and if one exists, another request to Open is made. This is what I missed.

So to fix the code, I need to pipe through the request to both . and index.html to the actual files:

func (pf *primaryFiles) Open(name string) (fs.File, error) {
    if name == "." || name == "index.html" {
        return files.Open(name)
    }
    return nil, fs.ErrNotExist
}
  • Related