fix: apply code-review and insight findings across nuntius
This commit is contained in:
@@ -198,14 +198,19 @@ func compose(from, to string, req contactform.Request, formName, formType string
|
||||
case "generic":
|
||||
tmpl = emailTemplateGeneric
|
||||
}
|
||||
_ = tmpl.Execute(&htmlBuf, map[string]string{
|
||||
if err := tmpl.Execute(&htmlBuf, map[string]string{
|
||||
"FormName": formName,
|
||||
"Name": req.Name,
|
||||
"Email": req.Email,
|
||||
"Service": req.Service,
|
||||
"Message": req.Message,
|
||||
"Received": received,
|
||||
})
|
||||
}); err != nil {
|
||||
// template.Must guarantees valid templates; this path is
|
||||
// unreachable in normal operation.
|
||||
htmlBuf.Reset()
|
||||
fmt.Fprintf(&htmlBuf, "<p>Email generation error: %v</p>", err)
|
||||
}
|
||||
|
||||
// --- Subject + plain-text body per form type ---
|
||||
var subject, text string
|
||||
|
||||
@@ -114,10 +114,12 @@ func (h *ContactHandler) makeHandler(path string) http.HandlerFunc {
|
||||
if errs := contactform.Validate(&req, form.Type); len(errs) > 0 {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_ = json.NewEncoder(w).Encode(contactform.ErrorResponse{
|
||||
if err := json.NewEncoder(w).Encode(contactform.ErrorResponse{
|
||||
Error: "validation",
|
||||
Details: errs,
|
||||
})
|
||||
}); err != nil {
|
||||
slog.Error("failed to encode json response", "err", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -155,10 +157,12 @@ func (h *ContactHandler) makeHandler(path string) http.HandlerFunc {
|
||||
// Health handles GET /health.
|
||||
func (h *ContactHandler) Health(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
if err := json.NewEncoder(w).Encode(map[string]any{
|
||||
"status": "ok",
|
||||
"forms": len(h.forms),
|
||||
})
|
||||
}); err != nil {
|
||||
slog.Error("failed to encode health response", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
// --- internals ---
|
||||
@@ -201,10 +205,36 @@ type bucket struct {
|
||||
}
|
||||
|
||||
func newRateLimiter(perHour int) *rateLimiter {
|
||||
return &rateLimiter{
|
||||
r := &rateLimiter{
|
||||
perHour: perHour,
|
||||
buckets: make(map[string]*bucket),
|
||||
}
|
||||
// Clean up entries older than twice the refill window every hour.
|
||||
r.startCleanup(1*time.Hour, 2*time.Hour)
|
||||
return r
|
||||
}
|
||||
|
||||
// startCleanup launches a background goroutine that periodically removes
|
||||
// expired bucket entries to prevent unbounded memory growth.
|
||||
func (r *rateLimiter) startCleanup(interval, maxAge time.Duration) {
|
||||
go func() {
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
r.cleanup(maxAge)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (r *rateLimiter) cleanup(maxAge time.Duration) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
cutoff := time.Now().Add(-maxAge)
|
||||
for ip, b := range r.buckets {
|
||||
if b.last.Before(cutoff) {
|
||||
delete(r.buckets, ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *rateLimiter) allow(ip string) bool {
|
||||
@@ -239,16 +269,20 @@ func minF(a, b float64) float64 {
|
||||
|
||||
func respondOK(w http.ResponseWriter) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
_ = json.NewEncoder(w).Encode(contactform.Response{OK: true})
|
||||
if err := json.NewEncoder(w).Encode(contactform.Response{OK: true}); err != nil {
|
||||
slog.Error("failed to encode ok response", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
func respondError(w http.ResponseWriter, code int, err, msg string) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(code)
|
||||
_ = json.NewEncoder(w).Encode(contactform.ErrorResponse{
|
||||
if encErr := json.NewEncoder(w).Encode(contactform.ErrorResponse{
|
||||
Error: err,
|
||||
Message: msg,
|
||||
})
|
||||
}); encErr != nil {
|
||||
slog.Error("failed to encode error response", "err", encErr)
|
||||
}
|
||||
}
|
||||
|
||||
func clientIP(r *http.Request) string {
|
||||
|
||||
Reference in New Issue
Block a user