feat: add mobile-friendly HTTP 404 and 500 error pages

Add ErrorNotFound and ErrorInternal handler methods that render styled
error pages using the error.html template, with proper status codes,
responsive layout, SVG icons, and HTMX fragment support. Replace the
plain-text http.NotFound call in Dashboard with the new styled handler.

Closes leeworks-agents/gitea-mobile#131

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
agent-company
2026-03-28 18:06:10 +00:00
parent 77c8e92e38
commit 25bc305fc9
4 changed files with 205 additions and 1 deletions
+48 -1
View File
@@ -181,11 +181,58 @@ func renderPage(w http.ResponseWriter, r *http.Request, title, activeTab string,
}
}
// errorData holds the template data for error pages.
type errorData struct {
Code int
Title string
Message string
}
// ErrorNotFound renders a mobile-friendly 404 error page.
func (h *Handler) ErrorNotFound(w http.ResponseWriter, r *http.Request) {
data := errorData{
Code: http.StatusNotFound,
Title: "Page Not Found",
Message: "The page you are looking for does not exist or has been moved.",
}
h.renderError(w, r, data)
}
// ErrorInternal renders a mobile-friendly 500 error page.
func (h *Handler) ErrorInternal(w http.ResponseWriter, r *http.Request) {
data := errorData{
Code: http.StatusInternalServerError,
Title: "Internal Server Error",
Message: "Something went wrong on our end. Please try again later.",
}
h.renderError(w, r, data)
}
// renderError renders the error template with the given data and status code.
func (h *Handler) renderError(w http.ResponseWriter, r *http.Request, data errorData) {
tmpl, err := template.ParseFiles("internal/templates/error.html")
if err != nil {
slog.Error("failed to parse error template", "error", err)
http.Error(w, fmt.Sprintf("%d %s", data.Code, data.Title), data.Code)
return
}
var buf strings.Builder
if err := tmpl.ExecuteTemplate(&buf, "content", data); err != nil {
slog.Error("failed to execute error template", "error", err)
http.Error(w, fmt.Sprintf("%d %s", data.Code, data.Title), data.Code)
return
}
w.WriteHeader(data.Code)
renderPage(w, r, data.Title, "", buf.String())
}
// Dashboard handles GET / — the triage queue.
func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) {
// Only handle exact root path.
if r.URL.Path != "/" {
http.NotFound(w, r)
h.ErrorNotFound(w, r)
return
}
+81
View File
@@ -183,6 +183,87 @@ func TestAddComment_EmptyBody(t *testing.T) {
}
}
func TestErrorNotFound(t *testing.T) {
h := newTestHandler()
req := httptest.NewRequest(http.MethodGet, "/nonexistent", nil)
w := httptest.NewRecorder()
h.ErrorNotFound(w, req)
if w.Code != http.StatusNotFound {
t.Errorf("status = %d, want %d", w.Code, http.StatusNotFound)
}
body := w.Body.String()
if body == "" {
t.Error("expected non-empty response body")
}
if !contains(body, "404") {
t.Error("expected body to contain '404'")
}
if !contains(body, "Page Not Found") {
t.Error("expected body to contain 'Page Not Found'")
}
}
func TestErrorInternal(t *testing.T) {
h := newTestHandler()
req := httptest.NewRequest(http.MethodGet, "/error", nil)
w := httptest.NewRecorder()
h.ErrorInternal(w, req)
if w.Code != http.StatusInternalServerError {
t.Errorf("status = %d, want %d", w.Code, http.StatusInternalServerError)
}
body := w.Body.String()
if body == "" {
t.Error("expected non-empty response body")
}
if !contains(body, "500") {
t.Error("expected body to contain '500'")
}
if !contains(body, "Internal Server Error") {
t.Error("expected body to contain 'Internal Server Error'")
}
}
func TestDashboard_NonRootPath_Returns404(t *testing.T) {
h := newTestHandler()
req := httptest.NewRequest(http.MethodGet, "/unknown/path", nil)
w := httptest.NewRecorder()
h.Dashboard(w, req)
if w.Code != http.StatusNotFound {
t.Errorf("status = %d, want %d", w.Code, http.StatusNotFound)
}
body := w.Body.String()
if !contains(body, "404") {
t.Error("expected body to contain '404' for non-root path")
}
}
func TestErrorNotFound_HTMX(t *testing.T) {
h := newTestHandler()
req := httptest.NewRequest(http.MethodGet, "/nonexistent", nil)
req.Header.Set("HX-Request", "true")
w := httptest.NewRecorder()
h.ErrorNotFound(w, req)
if w.Code != http.StatusNotFound {
t.Errorf("status = %d, want %d", w.Code, http.StatusNotFound)
}
body := w.Body.String()
// HTMX response should not contain DOCTYPE.
if contains(body, "<!DOCTYPE") {
t.Error("HTMX response should not contain DOCTYPE")
}
if !contains(body, "Page Not Found") {
t.Error("expected body to contain 'Page Not Found'")
}
}
func contains(s, substr string) bool {
return len(s) >= len(substr) && searchString(s, substr)
}