feat(rest,config): wire CORS, real readiness, TLS options, error hygiene

- CORS config is now actually applied to the router (the middleware
  existed but was never wired); preflight returns 204 for allowed
  origins and 403 with no CORS headers for disallowed ones.
- /readyz runs real dependency checks (queue, auth database) and
  returns 503 with per-component detail when not ready; exported
  handlers support dedicated health listeners.
- Optional server.tls (cert_file/key_file) for REST and gRPC, validated
  at config load.
- 5xx responses no longer echo internal error details; not-found and
  already-sent map to 404/409 on cancel/retry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-18 09:15:57 -07:00
parent 21990f2533
commit ee82522b7c
6 changed files with 154 additions and 31 deletions
+20 -4
View File
@@ -2,6 +2,7 @@ package rest
import (
"encoding/json"
"errors"
"net/http"
"strconv"
"time"
@@ -160,7 +161,7 @@ func (h *Handler) CancelNotification(w http.ResponseWriter, r *http.Request) {
id := vars["id"]
if err := h.service.CancelNotification(r.Context(), id); err != nil {
respondError(w, http.StatusInternalServerError, "failed to cancel notification", err)
respondError(w, statusForServiceError(err), "failed to cancel notification", err)
return
}
@@ -177,7 +178,7 @@ func (h *Handler) RetryNotification(w http.ResponseWriter, r *http.Request) {
result, err := h.service.RetryNotification(r.Context(), id)
if err != nil {
respondError(w, http.StatusInternalServerError, "failed to retry notification", err)
respondError(w, statusForServiceError(err), "failed to retry notification", err)
return
}
@@ -186,6 +187,19 @@ func (h *Handler) RetryNotification(w http.ResponseWriter, r *http.Request) {
})
}
// statusForServiceError maps service-layer sentinel errors to HTTP status
// codes; anything unrecognized is an internal error.
func statusForServiceError(err error) int {
switch {
case errors.Is(err, domain.ErrNotificationNotFound):
return http.StatusNotFound
case errors.Is(err, domain.ErrNotificationAlreadySent):
return http.StatusConflict
default:
return http.StatusInternalServerError
}
}
// GetStats handles GET /api/v1/stats
func (h *Handler) GetStats(w http.ResponseWriter, r *http.Request) {
stats, err := h.service.GetStats(r.Context())
@@ -270,10 +284,12 @@ func respondJSON(w http.ResponseWriter, status int, data interface{}) {
}
}
// respondError sends an error response
// respondError sends an error response. Client errors (4xx) include the
// underlying detail to help callers fix their request; server errors (5xx)
// deliberately do not echo internals — those belong in the server log.
func respondError(w http.ResponseWriter, status int, message string, err error) {
errMsg := message
if err != nil {
if err != nil && status < http.StatusInternalServerError {
errMsg = message + ": " + err.Error()
}