fix: apply code-review and insight findings across nuntius
This commit is contained in:
@@ -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