From 7952bc7f6a9bf30863956f7fff2c9289fd538416 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Mon, 16 Feb 2026 16:49:54 -0500 Subject: [PATCH] feat(router): add smart 401 error handler for HTML vs API requests - Add strings import for Accept header parsing - Add wantsHTML() helper function to detect HTML vs API requests - Checks Accept header for text/html - Checks HX-Request header for HTMX requests - Checks X-Requested-With for AJAX (should return JSON) - Defaults to JSON for API routes - Update JWT middleware ErrorHandler to: - Redirect HTML requests to /login?session=expired - Return JSON error for API requests with session_expired message - Enables browser navigation to redirect gracefully while API calls return proper error responses This fixes the issue where protected routes returned JSON 401 for browser navigation instead of redirecting to login. --- internal/router/router.go | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/internal/router/router.go b/internal/router/router.go index e98a5b5..8bb5bd4 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -9,6 +9,7 @@ import ( "bookhoard/internal/sync" "log" "net/http" + "strings" "time" "github.com/go-playground/validator/v10" @@ -85,11 +86,44 @@ func createJWTMiddleware(cfg *Config) echo.MiddlewareFunc { }) }, ErrorHandler: func(c echo.Context, err error) error { - return c.JSON(http.StatusUnauthorized, map[string]string{"error": err.Error()}) + // Check if this is a page request (browser navigation) + if wantsHTML(c.Request().Header) { + // Page request → Redirect to login with message + loginURL := "/login?session=expired" + return c.Redirect(http.StatusFound, loginURL) + } + + // API request → Return JSON error + return c.JSON(http.StatusUnauthorized, map[string]string{ + "error": "session_expired", + "message": "Your session has expired. Please log in again.", + }) }, }) } +// wantsHTML determines if the request expects HTML response +func wantsHTML(header http.Header) bool { + // Check Accept header + accept := header.Get("Accept") + if accept != "" && (accept == "text/html" || strings.Contains(accept, "text/html")) { + return true + } + + // Check HTMX request + if header.Get("HX-Request") == "true" { + return true + } + + // Check for AJAX requests (should get JSON) + if header.Get("X-Requested-With") == "XMLHttpRequest" { + return false + } + + // Default to JSON for API routes + return false +} + // RegisterRoutes registers all application routes func RegisterRoutes(cfg *Config) *handlers.Handler { e := cfg.Echo