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.
This commit is contained in:
2026-02-16 16:49:54 -05:00
parent 2e1af8d20b
commit 7952bc7f6a
+35 -1
View File
@@ -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