CODE-1845: Always serving index.html for static resources which are not found in dist folder (#2033)

pull/3519/head
Tan Nhu 2024-05-15 21:34:37 +00:00 committed by Harness
parent 49f3bf151e
commit 9480cf414c
1 changed files with 81 additions and 58 deletions

View File

@ -18,20 +18,23 @@ package web
import ( import (
"bytes" "bytes"
"embed" "embed"
"fmt"
"io" "io"
"io/fs" "io/fs"
"net/http" "net/http"
"os" "os"
"path" "path"
"path/filepath"
"strings"
"time" "time"
"github.com/rs/zerolog/log"
) )
//go:embed dist/* //go:embed dist/*
var UI embed.FS 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 // Handler returns an http.HandlerFunc that servers the
// static content from the embedded file system. // static content from the embedded file system.
@ -39,84 +42,104 @@ var UI embed.FS
//nolint:gocognit // refactor if required. //nolint:gocognit // refactor if required.
func Handler() http.HandlerFunc { func Handler() http.HandlerFunc {
// Load the files subdirectory // Load the files subdirectory
fs, err := fs.Sub(UI, "dist") fs, err := fs.Sub(UI, distPath)
if err != nil { if err != nil {
panic(err) panic(err)
} }
// Create an http.FileServer to serve the // Create an http.FileServer to serve the
// contents of the files subdiretory. // contents of the files subdiretory.
handler := http.FileServer(http.FS(fs)) 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) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// because this is a single page application, // Get the file base path
// we need to always load the index.html file basePath := path.Base(r.URL.Path)
// in the root of the project, unless the path
// points to a file with an extension (css, js, etc) // If the file exists in dist/ then serve it from "/".
// No ext: (1) a browser URL request, not a static asset request // Otherwise, rewrite the request to "/" so /index.html is served
if filepath.Ext(r.URL.Path) == "" || if fileNotFoundInDist(basePath) {
// "..." : (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.
r.URL.Path = "/" r.URL.Path = "/"
} else { } else {
// All static assets are served from the root path r.URL.Path = "/" + basePath
r.URL.Path = "/" + path.Base(r.URL.Path)
} }
// Disable caching and sniffing via HTTP headers for UI main entry resources // 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("Cache-Control", "no-cache, no-store, must-revalidate, max-age=0")
w.Header().Set("pragma", "no-cache") w.Header().Set("pragma", "no-cache")
w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("X-Content-Type-Options", "nosniff")
} }
if r.URL.Path == "/remoteEntry.js" { // Serve /remoteEntry.js from memory
if readerRemoteEntry, errFetch := fetchRemoteEntryJS(fs); errFetch == nil { if r.URL.Path == remoteEntryJSFullPath {
http.ServeContent(w, r, r.URL.Path, time.Now(), readerRemoteEntry) http.ServeContent(w, r, r.URL.Path, time.Now(), bytes.NewReader(remoteEntryContent))
} else { } else {
log.Error().Msgf("Failed to fetch remoteEntry.js %v", err)
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
}
} else {
// and finally serve the file.
handler.ServeHTTP(w, r) handler.ServeHTTP(w, r)
} }
}) })
} }
var remoteEntryContent *bytes.Reader func init() {
err := readRemoteEntryJSContent()
func fetchRemoteEntryJS(fs fs.FS) (*bytes.Reader, error) {
if remoteEntryContent == nil {
path := "remoteEntry.js"
file, err := fs.Open(path)
if err != nil { if err != nil {
log.Error().Msgf("Failed to open file %v", path) panic(err)
return nil, err
} }
buf, err := io.ReadAll(file) err = createFileMapForDistFolder()
if err != nil { if err != nil {
log.Error().Msgf("Failed to read file %v", path) panic(err)
return nil, 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") enableCDN := os.Getenv("ENABLE_CDN")
if len(enableCDN) == 0 { if len(enableCDN) == 0 {
enableCDN = "false" enableCDN = "false"
} }
modBuf := bytes.Replace(buf, []byte("__ENABLE_CDN__"), []byte(enableCDN), 1) remoteEntryContent = bytes.Replace(buf, []byte("__ENABLE_CDN__"), []byte(enableCDN), 1)
remoteEntryContent = bytes.NewReader(modBuf) return nil
} }
return remoteEntryContent, 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]
} }