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

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 and the CSRF middleware's
refusal use the same page. Status codes are unchanged. The receiver,
the healthcheck and /metrics keep their plain answers. If the error
page itself cannot render, the answer is the same status in plain
text.

Model: opus-5-5
This commit is contained in:
2026-10-01 20:52:32 +00:00
committed by sneak
parent 9d29baaa2d
commit 718865e075
20 changed files with 467 additions and 182 deletions
+37 -59
View File
@@ -149,13 +149,7 @@ func (h *Handlers) HandleSourceList() http.HandlerFunc {
"user_id = ?", userID,
).Order("created_at DESC").Find(&webhooks).Error
if err != nil {
h.log.Error(
"failed to list webhooks", "error", err,
)
http.Error(
w, "Internal server error",
http.StatusInternalServerError,
)
h.serverError(w, r, "failed to list webhooks", err)
return
}
@@ -249,9 +243,7 @@ func (h *Handlers) HandleSourceCreateSubmit() http.HandlerFunc {
// middleware, which runs before CSRF parses the form.
err := r.ParseForm()
if err != nil {
http.Error(
w, "Bad request", http.StatusBadRequest,
)
h.renderError(w, r, http.StatusBadRequest)
return
}
@@ -311,7 +303,7 @@ func (h *Handlers) createWebhookWithEntrypoint(
err := h.commitWebhook(webhook)
if err != nil {
h.serverError(w, "failed to create webhook", err)
h.serverError(w, r, "failed to create webhook", err)
return
}
@@ -388,7 +380,7 @@ func (h *Handlers) HandleSourceDetail() http.HandlerFunc {
"id = ? AND user_id = ?", sourceID, userID,
).First(&webhook).Error
if err != nil {
http.NotFound(w, r)
h.renderError(w, r, http.StatusNotFound)
return
}
@@ -475,7 +467,7 @@ func (h *Handlers) HandleSourceEdit() http.HandlerFunc {
"id = ? AND user_id = ?", sourceID, userID,
).First(&webhook).Error
if err != nil {
http.NotFound(w, r)
h.renderError(w, r, http.StatusNotFound)
return
}
@@ -510,7 +502,7 @@ func (h *Handlers) HandleSourceEditSubmit() http.HandlerFunc {
"id = ? AND user_id = ?", sourceID, userID,
).First(&webhook).Error
if err != nil {
http.NotFound(w, r)
h.renderError(w, r, http.StatusNotFound)
return
}
@@ -519,9 +511,7 @@ func (h *Handlers) HandleSourceEditSubmit() http.HandlerFunc {
// middleware, which runs before CSRF parses the form.
err = r.ParseForm()
if err != nil {
http.Error(
w, "Bad request", http.StatusBadRequest,
)
h.renderError(w, r, http.StatusBadRequest)
return
}
@@ -575,7 +565,7 @@ func (h *Handlers) applyWebhookEdit(
err := h.db.DB().Save(webhook).Error
if err != nil {
h.serverError(w, "failed to update webhook", err)
h.serverError(w, r, "failed to update webhook", err)
return
}
@@ -605,7 +595,7 @@ func (h *Handlers) HandleSourceDelete() http.HandlerFunc {
"id = ? AND user_id = ?", sourceID, userID,
).First(&webhook).Error
if err != nil {
http.NotFound(w, r)
h.renderError(w, r, http.StatusNotFound)
return
}
@@ -632,7 +622,7 @@ func (h *Handlers) deleteWebhookResources(
// be removed by hand; deleted history cannot be recovered.
err := h.commitWebhookDeletion(&webhook)
if err != nil {
h.serverError(w, "failed to delete webhook", err)
h.serverError(w, r, "failed to delete webhook", err)
return
}
@@ -658,7 +648,7 @@ func (h *Handlers) deleteWebhookResources(
// redirecting as though everything succeeded: the file
// needs removing by hand, and the logged error names it.
h.serverError(
w, "failed to delete webhook event database", err,
w, r, "failed to delete webhook event database", err,
)
return
@@ -802,7 +792,7 @@ func (h *Handlers) ownedWebhook(
"id = ? AND user_id = ?", sourceID, userID,
).First(&webhook).Error
if err != nil {
http.NotFound(w, r)
h.renderError(w, r, http.StatusNotFound)
return database.Webhook{}, false
}
@@ -824,7 +814,7 @@ func (h *Handlers) HandleSourceLogs() http.HandlerFunc {
// Without the map every delivery renders through a
// zero redactor, so failing the page is the only
// safe answer.
h.serverError(w, "failed to load targets", err)
h.serverError(w, r, "failed to load targets", err)
return
}
@@ -832,7 +822,7 @@ func (h *Handlers) HandleSourceLogs() http.HandlerFunc {
page := h.parsePage(r)
evts, total, ok := h.loadEventsWithDeliveries(
w, webhook, targets, page,
w, r, webhook, targets, page,
)
if !ok {
return
@@ -942,6 +932,7 @@ func (h *Handlers) parsePage(r *http.Request) int {
// caller must then render nothing further.
func (h *Handlers) loadEventsWithDeliveries(
w http.ResponseWriter,
r *http.Request,
webhook database.Webhook,
targetMap map[string]eventLogTarget,
page int,
@@ -955,7 +946,7 @@ func (h *Handlers) loadEventsWithDeliveries(
webhookDB, err := h.dbMgr.GetDB(webhook.ID)
if err != nil {
h.serverError(
w, "failed to get webhook database", err,
w, r, "failed to get webhook database", err,
)
return nil, 0, false
@@ -992,7 +983,7 @@ func (h *Handlers) loadEventsWithDeliveries(
)
if err != nil {
h.serverError(
w, "failed to load delivery attempts", err,
w, r, "failed to load delivery attempts", err,
)
return nil, 0, false
@@ -1001,7 +992,7 @@ func (h *Handlers) loadEventsWithDeliveries(
resubmits, err := resubmitCounts(webhookDB, eventIDs)
if err != nil {
h.serverError(
w, "failed to count event resubmissions", err,
w, r, "failed to count event resubmissions", err,
)
return nil, 0, false
@@ -1224,7 +1215,7 @@ func (h *Handlers) HandleEntrypointCreate() http.HandlerFunc {
"id = ? AND user_id = ?", sourceID, userID,
).First(&webhook).Error
if err != nil {
http.NotFound(w, r)
h.renderError(w, r, http.StatusNotFound)
return
}
@@ -1233,9 +1224,7 @@ func (h *Handlers) HandleEntrypointCreate() http.HandlerFunc {
// middleware, which runs before CSRF parses the form.
err = r.ParseForm()
if err != nil {
http.Error(
w, "Bad request", http.StatusBadRequest,
)
h.renderError(w, r, http.StatusBadRequest)
return
}
@@ -1251,7 +1240,7 @@ func (h *Handlers) HandleEntrypointCreate() http.HandlerFunc {
err = h.db.DB().Create(entrypoint).Error
if err != nil {
h.serverError(w, "failed to create entrypoint", err)
h.serverError(w, r, "failed to create entrypoint", err)
return
}
@@ -1282,7 +1271,7 @@ func (h *Handlers) HandleTargetCreate() http.HandlerFunc {
"id = ? AND user_id = ?", sourceID, userID,
).First(&webhook).Error
if err != nil {
http.NotFound(w, r)
h.renderError(w, r, http.StatusNotFound)
return
}
@@ -1291,9 +1280,7 @@ func (h *Handlers) HandleTargetCreate() http.HandlerFunc {
// middleware, which runs before CSRF parses the form.
err = r.ParseForm()
if err != nil {
http.Error(
w, "Bad request", http.StatusBadRequest,
)
h.renderError(w, r, http.StatusBadRequest)
return
}
@@ -1364,7 +1351,7 @@ func (h *Handlers) processTargetCreate(
err = h.db.DB().Create(target).Error
if err != nil {
h.serverError(w, "failed to create target", err)
h.serverError(w, r, "failed to create target", err)
return
}
@@ -1458,7 +1445,7 @@ func (h *Handlers) buildTargetConfig(
case database.TargetTypeSlack:
return h.buildSlackTargetConfig(w, r, in.URL)
case database.TargetTypeDatabase:
return h.buildDatabaseTargetConfig(w, in.Expiry)
return h.buildDatabaseTargetConfig(w, r, in.Expiry)
case database.TargetTypeLog:
return "", nil
default:
@@ -1508,7 +1495,7 @@ func (h *Handlers) buildHTTPTargetConfig(
return "", err
}
return marshalTargetConfig(w, delivery.HTTPTargetConfig{
return h.marshalTargetConfig(w, r, delivery.HTTPTargetConfig{
URL: in.URL,
Headers: headers,
Timeout: timeout,
@@ -1530,7 +1517,7 @@ func (h *Handlers) buildSlackTargetConfig(
return "", err
}
return marshalTargetConfig(w, delivery.SlackTargetConfig{
return h.marshalTargetConfig(w, r, delivery.SlackTargetConfig{
WebhookURL: targetURL,
})
}
@@ -1584,16 +1571,14 @@ func (h *Handlers) validateTargetURL(
// marshalTargetConfig serialises a target configuration for storage,
// writing a 500 itself if it cannot.
func marshalTargetConfig(
func (h *Handlers) marshalTargetConfig(
w http.ResponseWriter,
r *http.Request,
cfg any,
) (string, error) {
configBytes, err := json.Marshal(cfg)
if err != nil {
http.Error(
w, "Internal server error",
http.StatusInternalServerError,
)
h.serverError(w, r, "failed to encode target config", err)
return "", err
}
@@ -1609,6 +1594,7 @@ func marshalTargetConfig(
// expiry yields an empty config (the keep-forever default).
func (h *Handlers) buildDatabaseTargetConfig(
w http.ResponseWriter,
r *http.Request,
expiry string,
) (string, error) {
expiry = strings.TrimSpace(expiry)
@@ -1627,8 +1613,8 @@ func (h *Handlers) buildDatabaseTargetConfig(
return "", err
}
return marshalTargetConfig(
w, map[string]any{"expiry": expiry},
return h.marshalTargetConfig(
w, r, map[string]any{"expiry": expiry},
)
}
@@ -1682,7 +1668,7 @@ func (h *Handlers) deleteChildResource(
"id = ? AND user_id = ?", sourceID, userID,
).First(&webhook).Error
if err != nil {
http.NotFound(w, r)
h.renderError(w, r, http.StatusNotFound)
return
}
@@ -1692,11 +1678,7 @@ func (h *Handlers) deleteChildResource(
childID, webhook.ID,
).Delete(model)
if result.Error != nil {
h.log.Error(errMsg, "error", result.Error)
http.Error(
w, "Internal server error",
http.StatusInternalServerError,
)
h.serverError(w, r, errMsg, result.Error)
return
}
@@ -1786,18 +1768,14 @@ func (h *Handlers) toggleChildResource(
"id = ? AND user_id = ?", sourceID, userID,
).First(&webhook).Error
if err != nil {
http.NotFound(w, r)
h.renderError(w, r, http.StatusNotFound)
return
}
err = toggleFn(webhook.ID, childID)
if err != nil {
h.log.Error(errMsg, "error", err)
http.Error(
w, "Internal server error",
http.StatusInternalServerError,
)
h.serverError(w, r, errMsg, err)
return
}