mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-22 11:13:26 +00:00
fix: prevent 301 redirect to relative path (./) when accessing root without trailing slash (addresses #334)
- Replaced http.FileServer with custom file serving to avoid automatic directory redirects - Manually serve index.html for root path requests - Custom routing bypasses ServeMux for frontend files to prevent redirect behavior - This fixes reverse proxy and Cloudflare tunnel compatibility issues
This commit is contained in:
@@ -2,6 +2,7 @@ package api
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"strings"
|
||||
@@ -32,15 +33,39 @@ func serveFrontendHandler() http.HandlerFunc {
|
||||
log.Fatal().Err(err).Msg("Failed to get embedded frontend")
|
||||
}
|
||||
|
||||
fileServer := http.FileServer(fsys)
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Clean the path
|
||||
p := r.URL.Path
|
||||
|
||||
// Default to index.html for root
|
||||
if p == "/" {
|
||||
p = "/index.html"
|
||||
// Handle root path specially to avoid FileServer's directory redirect
|
||||
// Issue #334: Serve index.html directly without using FileServer for root
|
||||
if p == "/" || p == "" {
|
||||
// Directly serve index.html content
|
||||
file, err := fsys.Open("index.html")
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Check that it's not a directory
|
||||
_, err = file.Stat()
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Read the file content
|
||||
content, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Serve the content
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.Write(content)
|
||||
return
|
||||
}
|
||||
|
||||
// Remove leading slash for filesystem lookup
|
||||
@@ -49,10 +74,33 @@ func serveFrontendHandler() http.HandlerFunc {
|
||||
// Check if file exists in embedded FS
|
||||
file, err := fsys.Open(lookupPath)
|
||||
if err == nil {
|
||||
file.Close()
|
||||
// File exists, serve it
|
||||
fileServer.ServeHTTP(w, r)
|
||||
return
|
||||
defer file.Close()
|
||||
|
||||
// Get file info
|
||||
stat, err := file.Stat()
|
||||
if err == nil && !stat.IsDir() {
|
||||
// Read and serve the file
|
||||
content, err := io.ReadAll(file)
|
||||
if err == nil {
|
||||
// Detect content type
|
||||
contentType := "application/octet-stream"
|
||||
if strings.HasSuffix(lookupPath, ".html") {
|
||||
contentType = "text/html; charset=utf-8"
|
||||
} else if strings.HasSuffix(lookupPath, ".css") {
|
||||
contentType = "text/css; charset=utf-8"
|
||||
} else if strings.HasSuffix(lookupPath, ".js") {
|
||||
contentType = "application/javascript; charset=utf-8"
|
||||
} else if strings.HasSuffix(lookupPath, ".json") {
|
||||
contentType = "application/json"
|
||||
} else if strings.HasSuffix(lookupPath, ".svg") {
|
||||
contentType = "image/svg+xml"
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
w.Write(content)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For SPA routing, serve index.html for non-API routes
|
||||
@@ -60,9 +108,16 @@ func serveFrontendHandler() http.HandlerFunc {
|
||||
!strings.HasPrefix(p, "/ws") &&
|
||||
!strings.HasPrefix(p, "/socket.io/") {
|
||||
// Serve index.html for client-side routing
|
||||
r.URL.Path = "/index.html"
|
||||
fileServer.ServeHTTP(w, r)
|
||||
return
|
||||
indexFile, err := fsys.Open("index.html")
|
||||
if err == nil {
|
||||
defer indexFile.Close()
|
||||
content, err := io.ReadAll(indexFile)
|
||||
if err == nil {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.Write(content)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Not found
|
||||
|
||||
@@ -30,6 +30,12 @@ func (e *APIError) Error() string {
|
||||
// ErrorHandler is a middleware that handles panics and errors
|
||||
func ErrorHandler(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Fix for issue #334: Normalize empty path to "/" before ServeMux processes it
|
||||
// This prevents the automatic redirect from "" to "./"
|
||||
if r.URL.Path == "" {
|
||||
r.URL.Path = "/"
|
||||
}
|
||||
|
||||
// Skip error handling for WebSocket endpoints
|
||||
if r.Header.Get("Upgrade") == "websocket" {
|
||||
next.ServeHTTP(w, r)
|
||||
|
||||
+21
-4
@@ -636,9 +636,8 @@ func (r *Router) setupRoutes() {
|
||||
// Simple stats page
|
||||
r.mux.HandleFunc("/simple-stats", r.handleSimpleStats)
|
||||
|
||||
// Serve embedded frontend
|
||||
log.Info().Msg("Serving embedded frontend")
|
||||
r.mux.Handle("/", serveFrontendHandler())
|
||||
// Note: Frontend handler is handled manually in ServeHTTP to prevent redirect issues
|
||||
// See issue #334 - ServeMux redirects empty path to "./" which breaks reverse proxies
|
||||
|
||||
}
|
||||
|
||||
@@ -658,6 +657,7 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Check if we need authentication
|
||||
needsAuth := true
|
||||
@@ -798,7 +798,24 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
|
||||
// Log request
|
||||
start := time.Now()
|
||||
r.mux.ServeHTTP(w, req)
|
||||
|
||||
// Fix for issue #334: Custom routing to prevent ServeMux's "./" redirect
|
||||
// When accessing without trailing slash, ServeMux redirects to "./" which is wrong
|
||||
// We handle routing manually to avoid this issue
|
||||
|
||||
// Check if this is an API or WebSocket route
|
||||
if strings.HasPrefix(req.URL.Path, "/api/") ||
|
||||
strings.HasPrefix(req.URL.Path, "/ws") ||
|
||||
strings.HasPrefix(req.URL.Path, "/socket.io/") ||
|
||||
req.URL.Path == "/simple-stats" {
|
||||
// Use the mux for API and special routes
|
||||
r.mux.ServeHTTP(w, req)
|
||||
} else {
|
||||
// Serve frontend for all other paths (including root)
|
||||
handler := serveFrontendHandler()
|
||||
handler(w, req)
|
||||
}
|
||||
|
||||
log.Debug().
|
||||
Str("method", req.Method).
|
||||
Str("path", req.URL.Path).
|
||||
|
||||
Reference in New Issue
Block a user