7

I have built a go application using gin and go1.17.

I am using go:embed to to serve static content for a SPA app built using react. (trying the approach as suggested in https://github.com/gin-contrib/static/issues/19). My frontend files are in a build folder

build/index.html
build/asset-manifest.json
build/static/css/**
build/static/js/**
build/manifest.json
//go:embed build/*
var reactStatic embed.FS

type embedFileSystem struct {
    http.FileSystem
    indexes bool
}

func (e embedFileSystem) Exists(prefix string, path string) bool {
    f, err := e.Open(path)
    if err != nil {
        return false
    }

    // check if indexing is allowed
    s, _ := f.Stat()
    if s.IsDir() && !e.indexes {
        return false
    }

    return true
}

func EmbedFolder(fsEmbed embed.FS, targetPath string, index bool) static.ServeFileSystem {
    subFS, err := fs.Sub(fsEmbed, targetPath)
    if err != nil {
        panic(err)
    }
    return embedFileSystem{
        FileSystem: http.FS(subFS),
        indexes:    index,
    }
}

func main() {
    router := gin.Default()

    fs := EmbedFolder(reactStatic, "build", true)

    //Serve frontend static files
    router.Use(static.Serve("/", fs))
    /* THESE ARE MY STATIC URLs FROM THE REACT APP in FRONTEND  */
    router.Use(static.Serve("/login", fs))
    router.Use(static.Serve("/calendar", fs))

    router.NoRoute(func(c *gin.Context) {
        c.JSON(404, gin.H{
            "code": "PAGE_NOT_FOUND", "message": "Page not found",
        })
    })

    setupBaseRoutes(router, database)

    httpServerExitDone := &sync.WaitGroup{}
    httpServerExitDone.Add(1)

    srv, ln := server.StartServer(router, httpServerExitDone)

    log.Printf("Starting Server at %s", ln.Addr().String())

    quit := make(chan os.Signal)
    signal.Notify(quit, os.Interrupt)
    <-quit
    log.Println("Shutdown Server ...")

    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    if err := srv.Shutdown(ctx); err != nil {
        log.Fatal("Server Shutdown:", err)
    }
    log.Println("Server exiting")
}

When the application loads and the page http://localhost:8000/ is opened it opens properly and I can navigate to http://localhost:8000/calendar using react-router-dom. But when I reload the page http://localhost:8000/calendar, I get 404 error.

5 Answers 5

4

I made the following comment on that github issue:

I was able to get this working for my SPA, serving from a wwwroot embedded directory with a minor hack in the NoRoute handler to always return index.html. I was originally simply trying to do:

//go:embed wwwroot
var app embed.FS
wwwroot := embedFolder(app, "wwwroot")

router.Use(static.Serve("/", wwwroot))
router.NoRoute(func(c *gin.Context) {
    c.FileFromFS("index.html", wwwroot)
})

but this doesn't play well with how the http.serveFile function always performs a local redirect to "/" when the path ends with /index.html. So instead of "index.html", I tried "", "/", "wwwroot", and "wwwroot/", but all of those failed because that wasn't actually a file in the embedded file system.

My solution was to re-write the request URL to the default empty path and re-use the static.Serve middleware since it can handle the "/" path by calling it manually:

wwwroot := embedFolder(app, "wwwroot")
staticServer := static.Serve("/", wwwroot)

router.Use(staticServer)
router.NoRoute(func(c *gin.Context) {
    if c.Request.Method == http.MethodGet &&
        !strings.ContainsRune(c.Request.URL.Path, '.') &&
        !strings.HasPrefix(c.Request.URL.Path, "/api/") {
        c.Request.URL.Path = "/"
        staticServer(c)
    }
})

Note that I'm only doing this for GET requests that don't contain a '.' or start with my API prefix so I should still get a 404 error for API routes and files that don't exist, like if I used a bad image path.

Sign up to request clarification or add additional context in comments.

Comments

2

I managed to find a workaround by renaming build/index.html to build/index.htm

Due to some reason, index.html is hard-coded in some golang library used by gin, which results in 404 on page reload.

I read about it in a Github issue but can't seem to find the link to that issue now.

1 Comment

Maybe you are refering to this function? cs.opensource.google/go/go/+/refs/tags/go1.21.5:src/net/http/… I can confirm that renaming the file works.
0

maybe you can try this bro, just improve from this repository mandrigin/gin-spa

package main

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

    "github.com/gin-gonic/contrib/static"
    "github.com/gin-gonic/gin"
)

var (
    //go:embed build
    reactDir embed.FS
)

func EmbedReact(urlPrefix, buildDirectory string, em embed.FS) gin.HandlerFunc {
    dir := static.LocalFile(buildDirectory, true)
    embedDir, _ := fs.Sub(em, buildDirectory)
    fileserver := http.FileServer(http.FS(embedDir))

    if urlPrefix != "" {
        fileserver = http.StripPrefix(urlPrefix, fileserver)
    }

    return func(c *gin.Context) {
        if !dir.Exists(urlPrefix, c.Request.URL.Path) {
            c.Request.URL.Path = "/"
        }
        fileserver.ServeHTTP(c.Writer, c.Request)
        c.Abort()
    }
}

func main() {
    r := gin.Default()
    r.Use(EmbedReact("/", "build", reactDir))
    r.Run()
}

Comments

0

I found another solution:

Go, Gin & React

engine := gin.Default()
engine.Use(static.Serve("/", static.LocalFile("./client/build", true)))
engine.NoRoute(func(c *gin.Context) {
    c.File("./client/build/index.html")
})
err := engine.Run(":8080")

Comments

0

I found a slightly simpler way that's working for me:

//go:embed build
var reactStatic embed.FS

static, err := fs.Sub(reactStatic, "build")
if err != nil {
  panic(err)
}

r := gin.Default()
r.StaticFS("/", http.FS(reactStatic))

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.