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} } // 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(``)) return } w.Header().Set("Content-Type", "text/html") w.Write(buf.Bytes()) }