Search code examples
gohttp-headerslast-modified

Overriding Last-Modified header in http.FileServer


I'm trying to override the Last-Modified-header set by http.FileServer, but it reverts to the Last-Modified-time of the file I'm trying to serve:

var myTime time.Time

func main() {
     myTime = time.Now()         

     fs := http.StripPrefix("/folder/", SetCacheHeader(http.FileServer(http.Dir("/folder/"))))
     http.Handle("/folder/", fs)
     http.ListenAndServe(":80", nil)
}

My SetCacheHeader-handler:

func SetCacheHeader(h http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Last-Modified", myTime.Format(http.TimeFormat))
        h.ServeHTTP(w, r)
    })
}

Solution

  • The handler returned by http.FileServer() unconditionally sets the "Last-Modified" header in the http.serveFile() and http.serveContent() unexported functions:

    func serveFile(w ResponseWriter, r *Request, fs FileSystem,
        name string, redirect bool) {
    
        // ...
        f, err := fs.Open(name)
        // ...
        d, err := f.Stat()
        // ...
    
        // serveContent will check modification time
        sizeFunc := func() (int64, error) { return d.Size(), nil }
        serveContent(w, r, d.Name(), d.ModTime(), sizeFunc, f)
    }
    
    func serveContent(w ResponseWriter, r *Request, name string, modtime time.Time,
        sizeFunc func() (int64, error), content io.ReadSeeker) {
        setLastModified(w, modtime)
        // ...
    }
    
    
    func setLastModified(w ResponseWriter, modtime time.Time) {
        if !isZeroTime(modtime) {
            w.Header().Set("Last-Modified", modtime.UTC().Format(TimeFormat))
        }
    }
    

    So what you set prior to calling the file server handler will get overwritten. You can't really do anything about this.

    If you need to serve the content of a file with custom last-modified time, you may use http.ServeContent():

    func ServeContent(w ResponseWriter, req *Request, name string,
        modtime time.Time, content io.ReadSeeker)
    

    Where you can pass the last-modified time to be used, but then of course you lose all the convenient features provided by http.FileServer().

    If you want to keep using http.FileServer(), another option would be to not use the http.Dir type, but create your own http.FileSystem implementation that you pass to http.FileServer(), in which you could report any last-modified timestamp you wish.

    This would require you to implement the following interface:

    type FileSystem interface {
            Open(name string) (File, error)
    }
    

    So you need a method which opens a file by its name, and returns a value which implements http.File, which is:

    type File interface {
            io.Closer
            io.Reader
            io.Seeker
            Readdir(count int) ([]os.FileInfo, error)
            Stat() (os.FileInfo, error)
    }
    

    And the value you return (that implements http.File) could have a Stat() (os.FileInfo, error) method implementation, whose os.FileInfo returned value contains the last-modified time of your choosing. Note that you should also implement the Readdir() method to return custom last-modified timestamps consistent with the timestamps returned by Stat()'s fileinfo. See related question how to do that: Simples way to make a []byte into a "virtual" File object in golang?