package server import ( "bytes" "html/template" "log/slog" "net/http" ) // SafeTemplates wraps *html/template.Template to ensure all rendering goes // through a buffer. This prevents partial HTML from being written to the // response if template execution fails mid-way — the stdlib streams directly // to the writer and cannot roll back a partial write. // // Handlers hold a *SafeTemplates instead of *template.Template. The underlying // template set is unexported, so callers cannot bypass the buffer by calling // ExecuteTemplate on the ResponseWriter directly — the compiler prevents it. type SafeTemplates struct { tmpl *template.Template logger *slog.Logger } // NewSafeTemplates wraps tmpl and logger for safe buffered rendering. func NewSafeTemplates(tmpl *template.Template, logger *slog.Logger) *SafeTemplates { return &SafeTemplates{tmpl: tmpl, logger: logger} } // RenderErrorPage writes a styled full-page error response for navigation // requests. HTMX partial requests (HX-Request header) get the plain-text // error the client-side toast contract expects, and a failure to render the // error template itself also falls back to plain text, so this never emits a // partial page. func (s *SafeTemplates) RenderErrorPage(w http.ResponseWriter, r *http.Request, status int, msg string) { if r.Header.Get("HX-Request") == "true" { http.Error(w, msg, status) return } data := struct { Status int StatusText string Message string }{Status: status, StatusText: http.StatusText(status), Message: msg} var buf bytes.Buffer if err := s.tmpl.ExecuteTemplate(&buf, "error.html", data); err != nil { s.logger.Error("error page render failed", slog.Int("status", status), slog.Any("error", err)) http.Error(w, msg, status) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(status) w.Write(buf.Bytes()) } // Render executes the named template into a buffer and writes it to w only on // success. On failure it logs the error and writes a 500 error fragment, // guaranteeing the response is never partial or truncated. func (s *SafeTemplates) Render(w http.ResponseWriter, name string, data any) { var buf bytes.Buffer if err := s.tmpl.ExecuteTemplate(&buf, name, data); err != nil { s.logger.Error("template execution failed", slog.String("template", name), slog.Any("error", err)) w.Header().Set("Content-Type", "text/html") w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(`