Render admin page errors in the normal layout (closes #382)
check / check (push) Waiting to run

Every 400, 403, 404 and 500 on an admin page now answers with an error page in the normal layout: the status, one fixed line explaining it, and a link back to the webhook list, or to sign-in when nobody is signed in. Unknown paths reach it through the router's not-found handler, a refused form token through the CSRF middleware, and a panic through a recoverer each admin page route group installs first. Status codes are unchanged, and the page always sends Cache-Control: no-store.

If the error page fails to render, the answer is the same status in plain text; if it panics, the answer is a 500. The receiver, the healthcheck and /metrics keep their plain answers.

Model: opus-5-5
This commit was merged in pull request #408.
This commit is contained in:
2026-10-02 08:15:12 +02:00
parent 38157d8936
commit 2416528b77
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