Search code examples
reactjsgoogle-app-enginegoreact-routerbrowser-history

How to Serve Dynamically Created URL Paths with Go?


I am using react-router and browserHistory's pushState in a reactjs project. This project lets a user create a note which creates a new path. To serve this type of site I need to serve the same HTML file to every path besides the static content. So my nodejs code looks like this.

// Serve the static content
app.use('/static/css/', express.static(path.join(__dirname, '../../react-ui/build/static/css')));
app.use('/static/js/', express.static(path.join(__dirname, '../../react-ui/build/static/js')));
app.use('/static/media/', express.static(path.join(__dirname, '../../react-ui/build/static/media')));
app.use('/static/img/', express.static(path.join(__dirname, '../../react-ui/build/static/img')));
app.use('/img/', express.static(path.join(__dirname, '../../react-ui/build/img')));

// Serve the same HTML file to everything else
app.use('*', express.static(path.join(__dirname, '../../react-ui/build'))); 

I don't see any wildcard support for the Go FileServer. Currently I have all the static pages served using Go code similar to this.

package main

import (
    "net/http"
)

func init(){
    fs := http.FileServer(http.Dir("web"))
    http.Handle("/", fs)
    http.Handle("/static-page-1/", http.StripPrefix("/static-page-1/", fs))
    http.Handle("/static-page-2/", http.StripPrefix("/static-page-2/", fs))
    http.Handle("/static-page-3/", http.StripPrefix("/static-page-3/", fs))
}

Is it possible to serve content to dynamically generated URL paths with a Go server?

If the Handle method supported variables then I'd write the code like this

fs := http.FileServer(http.Dir("web"))
http.Handle("/static/", fs)
http.Handle("/{unknownUserPath}", http.StripPrefix("/{unknownUserPath}", fs))

{unknownUserPath} would be any path that a user types in that is not under /static/ path.

Here's the go project structure

enter image description here

Here's the server based on @putu answer

package main

import (
    "net/http"
    "strings"
)

func adaptFileServer(fs http.Handler) http.Handler {
    fn := func(w http.ResponseWriter, req *http.Request) {
        staticIndex := strings.Index(req.URL.Path, "/static/");
        imgIndex := strings.Index(req.URL.Path, "/img/");

        if staticIndex == -1 && imgIndex == -1 {
            fsHandler := http.StripPrefix(req.URL.Path, fs)
            fsHandler.ServeHTTP(w, req)
        } else {
            fs.ServeHTTP(w, req)
        }
    }
    return http.HandlerFunc(fn)
}

func init() {
    fs := http.FileServer(http.Dir("web"))
    http.Handle("/", adaptFileServer(fs))
}

Solution

  • If you want to serve static contents with URL pattern /* to a specific directory, then use the answer provided by jeevatkm.

    If you need slightly customizable version, you need a kind of adapter that map the URL path to static file handler (http.FileServer). The example code looks like:

    package main
    
    import (
        "log"
        "net/http"
        "regexp"
    )
    
    func helloHandler(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello world!"))
    }
    
    func adaptFileServer(fs http.Handler, mux http.Handler) http.Handler {
        fn := func(w http.ResponseWriter, req *http.Request) {
            //Use your Path matcher here.
            //For demonstration, REGEX match is used
            //and it's probably not the most efficient.
            staticRegex := regexp.MustCompile("^/static-page-[0-9]+/")
            if matches := staticRegex.FindStringSubmatch(req.URL.Path); matches != nil {
                log.Printf("Match: %v, %v", req.URL.Path, matches[0])
                fsHandler := http.StripPrefix(matches[0], fs)
                fsHandler.ServeHTTP(w, req)
            } else if mux != nil {
                log.Printf("Doesn't match, pass to other MUX: %v", req.URL.Path)
                mux.ServeHTTP(w, req)
            } else {
                http.Error(w, "Page Not Found", http.StatusNotFound)
            }
        }
        return http.HandlerFunc(fn)
    }
    
    func init() {
        //Usual routing definition with MUX
        mux := http.NewServeMux()
        mux.HandleFunc("/hello", helloHandler)
    
        //"Dynamic" static file server.
        fs := http.FileServer(http.Dir("web"))
        http.Handle("/", adaptFileServer(fs, mux))
    }
    
    func main() {
        log.Fatal(http.ListenAndServe(":8080", nil))
    }
    

    In the above adapter example, if request path match to a specific pattern (/static-page-*/ in the above example), it will be passed to http.FileServer. If doesn't match, and if a multiplexer is specified, it will call mux.ServeHTTP. Otherwise it will return 404 error.

    If you want another matching rule, just change the regex pattern (or use your custom matcher).

    Note:
    Please don't use a same handler instance for FileServer and mux. For example, when you call http.Handle, it use http.DefaultServeMux to handle routing. If you pass http.DefaultServeMux as the second argument of adaptFileServer you may end up with endless recursion.