Chi: How to fileserve for all requests?

Created on 1 Mar 2019  路  6Comments  路  Source: go-chi/chi

Hey! I am making an SPA and I need to fileserve on every page and also have an REST API.
I tried the fileserver example but I am getting the 404 page not found.

Using net/http works

package main

import (
    "net/http"
    "time"
)

func main() {
        server := &http.Server{
        Addr:         ":8080",
        ReadTimeout:  10 * time.Second,
        WriteTimeout: 10 * time.Second,
    }

    http.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) {
            w.Header().Set("Content-Type", "application/json")
        w.Write([]byte(`{ "message": "bar" }`))
    })

    http.Handle("/", http.FileServer(http.Dir("./web/dist")))

    panic(server.ListenAndServe())
}

Using go-chi/chi not found

package main

import (
    "net/http"
    "time"

        "github.com/go-chi/chi"
)

func main() {
        router := chi.NewRouter()
        server := &http.Server{
        Addr:         ":8080",
                Handler:      router,
        ReadTimeout:  10 * time.Second,
        WriteTimeout: 10 * time.Second,
    }

    router.Get("/foo", func(w http.ResponseWriter, r *http.Request) {
            w.Header().Set("Content-Type", "application/json")
        w.Write([]byte(`{ "message": "bar" }`))
    })

        // 404 Not Found
    router.Handle("/", http.FileServer(http.Dir("./web/dist")))

    panic(server.ListenAndServe())
}

Most helpful comment

I adapted your snippet to allow SPA to be served from sub path. In vue cli its the publicPath config

package main

import (
    "fmt"
    "net/http"
    "os"
    "path"
    "path/filepath"
    "strings"
    "time"

    "github.com/go-chi/chi"
)

const port = ":3333"

func main() {

    fmt.Printf("Starting Server on Port %v\n", port)
    router := chi.NewRouter()
    server := &http.Server{
        Addr:         port,
        Handler:      router,
        ReadTimeout:  10 * time.Second,
        WriteTimeout: 10 * time.Second,
    }

    router.Get("/foo", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        w.Write([]byte(`{ "message": "bar" }`))
    })

    FileServer(router, "/admin", "dist/")
    FileServer(router, "/", "../admin2/")

    panic(server.ListenAndServe())
}

// FileServer is serving static files
func FileServer(r chi.Router, public string, static string) {

    if strings.ContainsAny(public, "{}*") {
        panic("FileServer does not permit URL parameters.")
    }

    root, _ := filepath.Abs(static)
    if _, err := os.Stat(root); os.IsNotExist(err) {
        panic("Static Documents Directory Not Found")
    }

    fs := http.StripPrefix(public, http.FileServer(http.Dir(root)))

    if public != "/" && public[len(public)-1] != '/' {
        r.Get(public, http.RedirectHandler(public+"/", 301).ServeHTTP)
        public += "/"
    }

    r.Get(public+"*", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        file := strings.Replace(r.RequestURI, public, "/", 1)
        if _, err := os.Stat(root + file); os.IsNotExist(err) {
            http.ServeFile(w, r, path.Join(root, "index.html"))
            return
        }
        fs.ServeHTTP(w, r)
    }))
}

All 6 comments

Found the answer myself.

package cmd

import (
    "net/http"
    "os"
    "time"

    "github.com/go-chi/chi"
)

func main() {
    router := chi.NewRouter()
    server := &http.Server{
        Addr:         ":8080",
        Handler:      router,
        ReadTimeout:  10 * time.Second,
        WriteTimeout: 10 * time.Second,
    }

        router.Get("/foo", func(w http.ResponseWriter, r *http.Request) {
            w.Header().Set("Content-Type", "application/json")
        w.Write([]byte(`{ "message": "bar" }`))
    })

    FileServer(router)

    panic(server.ListenAndServe())
}

// FileServer is serving static files.
func FileServer(router *chi.Mux) {
    root := "./website/dist"
    fs := http.FileServer(http.Dir(root))

    router.Get("/*", func(w http.ResponseWriter, r *http.Request) {
        if _, err := os.Stat(root + r.RequestURI); os.IsNotExist(err) {
            http.StripPrefix(r.RequestURI, fs).ServeHTTP(w, r)
        } else {
            fs.ServeHTTP(w, r)
        }
    })
}

I've been struggling with this for days, thank you!

I adapted your snippet to allow SPA to be served from sub path. In vue cli its the publicPath config

package main

import (
    "fmt"
    "net/http"
    "os"
    "path"
    "path/filepath"
    "strings"
    "time"

    "github.com/go-chi/chi"
)

const port = ":3333"

func main() {

    fmt.Printf("Starting Server on Port %v\n", port)
    router := chi.NewRouter()
    server := &http.Server{
        Addr:         port,
        Handler:      router,
        ReadTimeout:  10 * time.Second,
        WriteTimeout: 10 * time.Second,
    }

    router.Get("/foo", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        w.Write([]byte(`{ "message": "bar" }`))
    })

    FileServer(router, "/admin", "dist/")
    FileServer(router, "/", "../admin2/")

    panic(server.ListenAndServe())
}

// FileServer is serving static files
func FileServer(r chi.Router, public string, static string) {

    if strings.ContainsAny(public, "{}*") {
        panic("FileServer does not permit URL parameters.")
    }

    root, _ := filepath.Abs(static)
    if _, err := os.Stat(root); os.IsNotExist(err) {
        panic("Static Documents Directory Not Found")
    }

    fs := http.StripPrefix(public, http.FileServer(http.Dir(root)))

    if public != "/" && public[len(public)-1] != '/' {
        r.Get(public, http.RedirectHandler(public+"/", 301).ServeHTTP)
        public += "/"
    }

    r.Get(public+"*", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        file := strings.Replace(r.RequestURI, public, "/", 1)
        if _, err := os.Stat(root + file); os.IsNotExist(err) {
            http.ServeFile(w, r, path.Join(root, "index.html"))
            return
        }
        fs.ServeHTTP(w, r)
    }))
}

Hi @js02sixty

I have a question about these lines:

    if strings.ContainsAny(public, "{}*") {
        panic("FileServer does not permit URL parameters.")
    }

Why does it need to check the "{}*"? Is it for security? I want to know the detail. Thanks.

Hi @js02sixty

I have a question about these lines:

  if strings.ContainsAny(public, "{}*") {
      panic("FileServer does not permit URL parameters.")
  }

Why does it need to check the "{}*"? Is it for security? I want to know the detail. Thanks.

Your probably right, i basically copied pkieltyka's example:
https://github.com/go-chi/chi/blob/master/_examples/fileserver/main.go

@TonyPythoneer @js02sixty hey guys -- the reason we cheque for any parameter characters such as {}* is because everything after the routing path that is passed to chi is handled by the handler in http.FileSystem. Therefore chi can't support variables for the folder path, and so we check and error out.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Bartuz picture Bartuz  路  3Comments

innovate-invent picture innovate-invent  路  11Comments

rocanion picture rocanion  路  4Comments

nhooyr picture nhooyr  路  12Comments

makhov picture makhov  路  4Comments