Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions internal/common/serve/spa.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package serve

import (
"errors"
"io/fs"
"net/http"
"strings"
)

const indexHTMLPage = "index.html"

// SinglePageApplicationHandler handles requests for a single-page application front end. It prevents caching of
// index.html responses and defers handling of file paths which cannot be found to the front end by serving index.html
// in such cases.
func SinglePageApplicationHandler(fsys http.FileSystem) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if shouldServeIndexHTML(fsys, r.URL.Path) {
r.URL.Path = "/"
// Prevent caching when serving index.html. Its content determines the version of the JS/CSS
// bundle, and we want to prevent the user from accessing a stale bundle.
w.Header().Set("Cache-Control", "no-store, must-revalidate")
}

http.FileServer(fsys).ServeHTTP(w, r)
})
}

func shouldServeIndexHTML(fsys http.FileSystem, urlPath string) bool {
trimmedURLPath := strings.TrimPrefix(urlPath, "/")

// Serve index.html when the file cannot be found - client-side routing in the SPA handles such cases.
if _, err := fsys.Open(trimmedURLPath); errors.Is(err, fs.ErrNotExist) {
return true
}

if trimmedURLPath == "" || trimmedURLPath == indexHTMLPage {
return true
}

return false
}
22 changes: 0 additions & 22 deletions internal/common/serve/static.go
Original file line number Diff line number Diff line change
@@ -1,35 +1,13 @@
package serve

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

"github.com/pkg/errors"

"github.com/armadaproject/armada/internal/common/armadacontext"
)

// dirWithIndexFallback is a http.FileSystem that serves the index.html file at
// the root of dir if the requested file is not found. This behavior differs
// from http.Dir, which only forwards requests for /a/ to /a/index.html; we need
// to serve the index.html file at the root of dir if the requested file is not
// found, so that the frontend can handle routing in those cases.
type dirWithIndexFallback struct {
dir http.Dir
}

func CreateDirWithIndexFallback(path string) http.FileSystem {
return dirWithIndexFallback{http.Dir(path)}
}

func (d dirWithIndexFallback) Open(name string) (http.File, error) {
file, err := d.dir.Open(name)
if errors.Is(err, fs.ErrNotExist) {
return d.dir.Open("index.html")
}
return file, err
}

// ListenAndServe calls server.ListenAndServe().
// Additionally, it calls server.Shutdown() if ctx is cancelled.
func ListenAndServe(ctx *armadacontext.Context, server *http.Server) error {
Expand Down
15 changes: 1 addition & 14 deletions internal/lookout/gen/restapi/configure_lookout.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ func setupGlobalMiddleware(apiHandler http.Handler) http.Handler {
func uiHandler(apiHandler http.Handler) http.Handler {
mux := http.NewServeMux()

mux.Handle("/", setCacheControl(http.FileServer(serve.CreateDirWithIndexFallback("./internal/lookoutui/build"))))
mux.Handle("/", serve.SinglePageApplicationHandler(http.Dir("./internal/lookoutui/build")))

mux.HandleFunc("/config", func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
Expand All @@ -112,19 +112,6 @@ func uiHandler(apiHandler http.Handler) http.Handler {
return mux
}

func setCacheControl(fileHandler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" || r.URL.Path == "/index.html" {
// Because the version of index.html determines the version of the
// JavaScript bundle, caching index.html would prevent users from
// ever picking up new versions of the JavaScript bundle without
// manually invalidating the cache.
w.Header().Set("Cache-Control", "no-store")
}
fileHandler.ServeHTTP(w, r)
})
}

func allowCORS(handler http.Handler, corsAllowedOrigins []string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if origin := r.Header.Get("Origin"); origin != "" && slices.Contains(corsAllowedOrigins, origin) {
Expand Down
Loading