From 9480cf414c4b01cb1c9e6e09ce8e742bb5b0b65b Mon Sep 17 00:00:00 2001 From: Tan Nhu Date: Wed, 15 May 2024 21:34:37 +0000 Subject: [PATCH] CODE-1845: Always serving index.html for static resources which are not found in dist folder (#2033) --- web/dist.go | 139 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 81 insertions(+), 58 deletions(-) diff --git a/web/dist.go b/web/dist.go index 29ddfe28e..de5a91194 100644 --- a/web/dist.go +++ b/web/dist.go @@ -18,20 +18,23 @@ package web import ( "bytes" "embed" + "fmt" "io" "io/fs" "net/http" "os" "path" - "path/filepath" - "strings" "time" - - "github.com/rs/zerolog/log" ) //go:embed dist/* var UI embed.FS +var remoteEntryContent []byte +var fileMap map[string]bool + +const distPath = "dist" +const remoteEntryJS = "remoteEntry.js" +const remoteEntryJSFullPath = "/" + remoteEntryJS // Handler returns an http.HandlerFunc that servers the // static content from the embedded file system. @@ -39,84 +42,104 @@ var UI embed.FS //nolint:gocognit // refactor if required. func Handler() http.HandlerFunc { // Load the files subdirectory - fs, err := fs.Sub(UI, "dist") + fs, err := fs.Sub(UI, distPath) if err != nil { panic(err) } + // Create an http.FileServer to serve the // contents of the files subdiretory. handler := http.FileServer(http.FS(fs)) - // Create an http.HandlerFunc that wraps the - // http.FileServer to always load the index.html - // file if a directory path is being requested. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // because this is a single page application, - // we need to always load the index.html file - // in the root of the project, unless the path - // points to a file with an extension (css, js, etc) - // No ext: (1) a browser URL request, not a static asset request - if filepath.Ext(r.URL.Path) == "" || - // "..." : (2a) browser URL with ... in it - (strings.Contains(r.URL.Path, "...") && - // (2b) filter out static asset URLs that browsers make along with it - filepath.Ext(strings.ReplaceAll(r.URL.Path, "...", "")) == "") { - // HACK: alter the path to point to the - // root of the project. + // Get the file base path + basePath := path.Base(r.URL.Path) + + // If the file exists in dist/ then serve it from "/". + // Otherwise, rewrite the request to "/" so /index.html is served + if fileNotFoundInDist(basePath) { r.URL.Path = "/" } else { - // All static assets are served from the root path - r.URL.Path = "/" + path.Base(r.URL.Path) + r.URL.Path = "/" + basePath } // Disable caching and sniffing via HTTP headers for UI main entry resources - if r.URL.Path == "/" || r.URL.Path == "/remoteEntry.js" || r.URL.Path == "/index.html" { + if r.URL.Path == "/" || r.URL.Path == remoteEntryJSFullPath || r.URL.Path == "/index.html" { w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate, max-age=0") w.Header().Set("pragma", "no-cache") w.Header().Set("X-Content-Type-Options", "nosniff") } - if r.URL.Path == "/remoteEntry.js" { - if readerRemoteEntry, errFetch := fetchRemoteEntryJS(fs); errFetch == nil { - http.ServeContent(w, r, r.URL.Path, time.Now(), readerRemoteEntry) - } else { - log.Error().Msgf("Failed to fetch remoteEntry.js %v", err) - http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) - } + // Serve /remoteEntry.js from memory + if r.URL.Path == remoteEntryJSFullPath { + http.ServeContent(w, r, r.URL.Path, time.Now(), bytes.NewReader(remoteEntryContent)) } else { - // and finally serve the file. handler.ServeHTTP(w, r) } }) } -var remoteEntryContent *bytes.Reader - -func fetchRemoteEntryJS(fs fs.FS) (*bytes.Reader, error) { - if remoteEntryContent == nil { - path := "remoteEntry.js" - - file, err := fs.Open(path) - if err != nil { - log.Error().Msgf("Failed to open file %v", path) - return nil, err - } - - buf, err := io.ReadAll(file) - if err != nil { - log.Error().Msgf("Failed to read file %v", path) - return nil, err - } - - enableCDN := os.Getenv("ENABLE_CDN") - if len(enableCDN) == 0 { - enableCDN = "false" - } - - modBuf := bytes.Replace(buf, []byte("__ENABLE_CDN__"), []byte(enableCDN), 1) - - remoteEntryContent = bytes.NewReader(modBuf) +func init() { + err := readRemoteEntryJSContent() + if err != nil { + panic(err) } - return remoteEntryContent, nil + err = createFileMapForDistFolder() + if err != nil { + panic(err) + } +} + +func readRemoteEntryJSContent() error { + fs, err := fs.Sub(UI, distPath) + + if err != nil { + return fmt.Errorf("failed to open /dist: %w", err) + } + + file, err := fs.Open(remoteEntryJS) + + if err != nil { + return fmt.Errorf("failed to open remoteEntry.js: %w", err) + } + + defer file.Close() + buf, err := io.ReadAll(file) + + if err != nil { + return fmt.Errorf("failed to read remoteEntry.js: %w", err) + } + + enableCDN := os.Getenv("ENABLE_CDN") + + if len(enableCDN) == 0 { + enableCDN = "false" + } + + remoteEntryContent = bytes.Replace(buf, []byte("__ENABLE_CDN__"), []byte(enableCDN), 1) + + return nil +} + +func createFileMapForDistFolder() error { + fileMap = make(map[string]bool) + + err := fs.WalkDir(UI, distPath, func(path string, _ fs.DirEntry, err error) error { + if err != nil { + return fmt.Errorf("failed to build file map for path %q: %w", path, err) + } + + if path != distPath { // exclude "dist" from file map + fileMap[path] = true + } + + return nil + }) + + return err +} + +func fileNotFoundInDist(path string) bool { + return !fileMap[distPath+"/"+path] }