The template's files at a77fd30, without its history or LICENSE, after script/rename simplexcalc. Model: opus-5-5
124 lines
3.7 KiB
Go
124 lines
3.7 KiB
Go
// Package handlers holds the HTTP handlers. They are methods on one
|
|
// struct whose dependencies come from fx, so a handler never reaches
|
|
// for a package-level singleton and a test can build the struct with
|
|
// exactly the collaborators it wants.
|
|
package handlers
|
|
|
|
import (
|
|
"log/slog"
|
|
"net/http"
|
|
|
|
"go.uber.org/fx"
|
|
"sneak.berlin/go/simplexcalc/internal/config"
|
|
"sneak.berlin/go/simplexcalc/internal/database"
|
|
"sneak.berlin/go/simplexcalc/internal/globals"
|
|
"sneak.berlin/go/simplexcalc/internal/logger"
|
|
"sneak.berlin/go/simplexcalc/internal/middleware"
|
|
"sneak.berlin/go/simplexcalc/internal/render"
|
|
"sneak.berlin/go/simplexcalc/internal/telemetry"
|
|
)
|
|
|
|
// Params defines dependencies for Handlers.
|
|
type Params struct {
|
|
fx.In
|
|
|
|
Config *config.Config
|
|
Globals *globals.Globals
|
|
Logger *logger.Logger
|
|
Database *database.Database
|
|
Renderer *render.Renderer
|
|
Sentry *telemetry.Sentry
|
|
}
|
|
|
|
// Handlers is the set of HTTP handlers.
|
|
type Handlers struct {
|
|
params Params
|
|
log *slog.Logger
|
|
}
|
|
|
|
// New creates the handler set.
|
|
//
|
|
//nolint:revive // lc parameter is required by fx even if unused.
|
|
func New(lc fx.Lifecycle, params Params) (*Handlers, error) {
|
|
return &Handlers{params: params, log: params.Logger.Get()}, nil
|
|
}
|
|
|
|
// NotFound answers unmatched routes with the error page rather than
|
|
// net/http's bare text, so a 404 still carries the site's own headers
|
|
// and chrome.
|
|
func (h *Handlers) NotFound() http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
data := render.ErrorPage{
|
|
Page: h.page(r),
|
|
Status: http.StatusNotFound,
|
|
Message: "No such page.",
|
|
}
|
|
|
|
err := h.params.Renderer.HTML(w, http.StatusNotFound, "error.html", data)
|
|
if err != nil {
|
|
h.log.Error("rendering 404 failed", "error", err)
|
|
http.Error(w, "not found", http.StatusNotFound)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MethodNotAllowed answers a known path with the wrong method.
|
|
func (h *Handlers) MethodNotAllowed() http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
data := render.ErrorPage{
|
|
Page: h.page(r),
|
|
Status: http.StatusMethodNotAllowed,
|
|
Message: "That method is not allowed here.",
|
|
}
|
|
|
|
err := h.params.Renderer.HTML(w, http.StatusMethodNotAllowed, "error.html", data)
|
|
if err != nil {
|
|
h.log.Error("rendering 405 failed", "error", err)
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
}
|
|
|
|
// page builds the common template data for r.
|
|
func (h *Handlers) page(r *http.Request) render.Page {
|
|
return h.params.Renderer.NewPage(middleware.CSRFField(r))
|
|
}
|
|
|
|
// fail reports a handler error and answers with the error page.
|
|
//
|
|
// The message shown to the client is chosen by the caller and is never
|
|
// the error's text: an error from the database layer carries a query,
|
|
// possibly a value out of a row, and always more about the internals
|
|
// than a stranger should be given. The error itself goes to the log and
|
|
// to Sentry, tied to the request id that is also in the response
|
|
// header, so the two can be joined afterwards.
|
|
func (h *Handlers) fail(
|
|
w http.ResponseWriter, r *http.Request, status int, message string, err error,
|
|
) {
|
|
h.log.Error("handler error",
|
|
"id", middleware.RequestIDFrom(r.Context()),
|
|
"path", r.URL.Path,
|
|
"status", status,
|
|
"error", err,
|
|
)
|
|
|
|
if status >= http.StatusInternalServerError {
|
|
h.params.Sentry.CaptureError(err)
|
|
}
|
|
|
|
data := render.ErrorPage{
|
|
Page: h.page(r),
|
|
Status: status,
|
|
Message: message,
|
|
}
|
|
|
|
renderErr := h.params.Renderer.HTML(w, status, "error.html", data)
|
|
if renderErr != nil {
|
|
// The error page itself failed. Anything further would be
|
|
// another chance to fail, so this is the floor: a plain
|
|
// status, and the reason in the log.
|
|
h.log.Error("rendering error page failed", "error", renderErr)
|
|
http.Error(w, http.StatusText(status), status)
|
|
}
|
|
}
|