Render admin page errors in the normal layout (closes #382)
check / check (push) Successful in 6m7s
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:
@@ -135,6 +135,7 @@ func New(
|
||||
"source_edit.html": parsePageTemplate("source_edit.html"),
|
||||
"source_logs.html": parsePageTemplate("source_logs.html"),
|
||||
"target_edit.html": parsePageTemplate("target_edit.html"),
|
||||
"error.html": parsePageTemplate("error.html"),
|
||||
}
|
||||
|
||||
lc.Append(fx.Hook{
|
||||
@@ -146,6 +147,15 @@ func New(
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// HandleErrorPage returns a handler that answers every request with
|
||||
// the error page for status. The router uses it for unknown paths and
|
||||
// the CSRF middleware for a refused form.
|
||||
func (s *Handlers) HandleErrorPage(status int) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
s.renderError(w, r, status)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Handlers) respondJSON(
|
||||
w http.ResponseWriter,
|
||||
_ *http.Request,
|
||||
@@ -163,15 +173,72 @@ func (s *Handlers) respondJSON(
|
||||
}
|
||||
}
|
||||
|
||||
// serverError logs an error and sends a 500 response.
|
||||
// serverError logs an error and answers with the 500 error page.
|
||||
func (s *Handlers) serverError(
|
||||
w http.ResponseWriter, msg string, err error,
|
||||
w http.ResponseWriter, r *http.Request, msg string, err error,
|
||||
) {
|
||||
s.log.Error(msg, "error", err)
|
||||
http.Error(
|
||||
w, "Internal server error",
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
s.renderError(w, r, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// renderError answers with status and the error page: the normal
|
||||
// layout, one fixed line explaining the status, and a link back to the
|
||||
// webhook list, or to sign-in when nobody is signed in.
|
||||
//
|
||||
// It renders the page itself rather than through renderTemplate,
|
||||
// whose own failure comes here. If the error page cannot render
|
||||
// either, the answer is the same status in plain text: never a second
|
||||
// attempt, and never a different status.
|
||||
func (s *Handlers) renderError(
|
||||
w http.ResponseWriter,
|
||||
r *http.Request,
|
||||
status int,
|
||||
) {
|
||||
data := s.pageData(r, map[string]any{
|
||||
"Status": status,
|
||||
"StatusText": http.StatusText(status),
|
||||
"Message": errorPageText(status),
|
||||
})
|
||||
|
||||
var buf bytes.Buffer
|
||||
|
||||
err := s.templates["error.html"].Execute(&buf, data)
|
||||
if err != nil {
|
||||
s.log.Error("failed to render error page", "error", err)
|
||||
http.Error(w, http.StatusText(status), status)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(status)
|
||||
|
||||
_, err = buf.WriteTo(w)
|
||||
if err != nil {
|
||||
s.log.Error("failed to write error page", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// errorPageText is the line the error page shows for status. It is
|
||||
// fixed per status, so the page tells the reader no more than the
|
||||
// plain-text answers it replaced did.
|
||||
func errorPageText(status int) string {
|
||||
switch status {
|
||||
case http.StatusBadRequest:
|
||||
return "The request could not be read."
|
||||
case http.StatusForbidden:
|
||||
return "The request was refused. If it came from a form " +
|
||||
"left open for a long time, reload the page and try " +
|
||||
"again."
|
||||
case http.StatusNotFound:
|
||||
return "There is nothing here. It may have been deleted, " +
|
||||
"or the address may be wrong."
|
||||
case http.StatusServiceUnavailable:
|
||||
return "The server is busy. Please try again in a moment."
|
||||
default: // http.StatusInternalServerError
|
||||
return "Something went wrong on the server. Please try " +
|
||||
"again."
|
||||
}
|
||||
}
|
||||
|
||||
// UserInfo represents user information for templates
|
||||
@@ -224,14 +291,17 @@ func (s *Handlers) renderTemplate(
|
||||
"template not found",
|
||||
"template", pageTemplate,
|
||||
)
|
||||
http.Error(
|
||||
w, "Internal server error",
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
s.renderError(w, r, http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
s.executeTemplate(w, r, tmpl, s.pageData(r, data))
|
||||
}
|
||||
|
||||
// pageData adds the fields the shared layout renders to a page's own
|
||||
// data.
|
||||
func (s *Handlers) pageData(r *http.Request, data any) any {
|
||||
userInfo := s.getUserInfo(r)
|
||||
csrfToken := middleware.CSRFToken(r)
|
||||
|
||||
@@ -245,19 +315,16 @@ func (s *Handlers) renderTemplate(
|
||||
m["User"] = userInfo
|
||||
m["CSRFToken"] = csrfToken
|
||||
m["Version"] = version
|
||||
s.executeTemplate(w, tmpl, m)
|
||||
|
||||
return
|
||||
return m
|
||||
}
|
||||
|
||||
wrapper := templateDataWrapper{
|
||||
return templateDataWrapper{
|
||||
User: userInfo,
|
||||
CSRFToken: csrfToken,
|
||||
Version: version,
|
||||
Data: data,
|
||||
}
|
||||
|
||||
s.executeTemplate(w, tmpl, wrapper)
|
||||
}
|
||||
|
||||
// executeTemplate renders the template into a buffer and writes to
|
||||
@@ -270,6 +337,7 @@ func (s *Handlers) renderTemplate(
|
||||
// this reason.
|
||||
func (s *Handlers) executeTemplate(
|
||||
w http.ResponseWriter,
|
||||
r *http.Request,
|
||||
tmpl *template.Template,
|
||||
data any,
|
||||
) {
|
||||
@@ -280,10 +348,7 @@ func (s *Handlers) executeTemplate(
|
||||
s.log.Error(
|
||||
"failed to execute template", "error", err,
|
||||
)
|
||||
http.Error(
|
||||
w, "Internal server error",
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
s.renderError(w, r, http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user