Render admin page errors in the normal layout (closes #382)
check / check (push) Successful in 3m39s

Every 400, 403, 404 and 500 on an admin page now answers with an
error page in the normal layout: one fixed line for the status and a
link back to the webhook list, or to sign-in when nobody is signed
in. The router's handler for unknown paths, the CSRF middleware's
refusal and a panic in an admin page route group use the same page;
each such group has its own recoverer and error reporting for that.
The page always sends Cache-Control: no-store. Status codes are
unchanged. The receiver, the healthcheck and /metrics keep their
plain answers. If the error page fails to render, the answer is the
same status in plain text; if it panics, the answer is a 500.

Model: opus-5-5
This commit is contained in:
2026-10-02 04:44:14 +00:00
parent 803a94be37
commit ef59bf7554
24 changed files with 711 additions and 198 deletions
+16 -3
View File
@@ -88,14 +88,14 @@ func (h *Handlers) processWebhookRequest(
headersJSON, err := json.Marshal(r.Header)
if err != nil {
h.serverError(w, "failed to serialize headers", err)
h.receiverError(w, "failed to serialize headers", err)
return
}
targets, err := h.loadActiveTargets(entrypoint.WebhookID)
if err != nil {
h.serverError(w, "failed to query targets", err)
h.receiverError(w, "failed to query targets", err)
return
}
@@ -196,7 +196,7 @@ func (h *Handlers) createAndDeliverEvent(
targets,
)
if err != nil {
h.serverError(w, "failed to store webhook event", err)
h.receiverError(w, "failed to store webhook event", err)
return
}
@@ -204,6 +204,19 @@ func (h *Handlers) createAndDeliverEvent(
h.finishWebhookResponse(w, event, entrypoint, tasks)
}
// receiverError logs an error and answers the sender with a plain-text
// 500. The receiver's answers are for programs, so it never sends the
// error page the web UI uses.
func (h *Handlers) receiverError(
w http.ResponseWriter, msg string, err error,
) {
h.log.Error(msg, "error", err)
http.Error(
w, "Internal server error",
http.StatusInternalServerError,
)
}
// eventSource carries the fields a new event is built from. The
// receiver fills it from the live request; the resubmit handler fills
// it from a stored event. Both then go through createAndFanOut, so an