// Package httpapi contains HTTP handlers, routing and middleware. package httpapi import ( "encoding/json" "errors" "log/slog" "net/http" "github.com/jackc/pgx/v5" ) // Problem is a small RFC 7807-ish error body. type Problem struct { Type string `json:"type,omitempty"` Title string `json:"title"` Status int `json:"status"` Detail string `json:"detail,omitempty"` } // renderJSON writes v as JSON with the given status code. func renderJSON(w http.ResponseWriter, status int, v any) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(status) if v == nil { return } if err := json.NewEncoder(w).Encode(v); err != nil { slog.Error("write json response failed", "error", err) } } // renderError writes a Problem response. func renderError(w http.ResponseWriter, status int, title, detail string) { renderJSON(w, status, Problem{ Title: title, Status: status, Detail: detail, }) } // apiError maps well-known errors to HTTP status codes. Returns true if handled. func apiError(w http.ResponseWriter, err error) bool { switch { case errors.Is(err, pgx.ErrNoRows): renderError(w, http.StatusNotFound, "Not found", "The requested resource does not exist.") case errors.Is(err, ErrUnauthorized): renderError(w, http.StatusUnauthorized, "Unauthorized", err.Error()) case errors.Is(err, ErrForbidden): renderError(w, http.StatusForbidden, "Forbidden", err.Error()) case errors.Is(err, ErrConflict): renderError(w, http.StatusConflict, "Conflict", err.Error()) case errors.Is(err, ErrBadRequest): renderError(w, http.StatusBadRequest, "Bad request", err.Error()) default: return false } return true } // Sentinel domain errors mapped by apiError. var ( ErrBadRequest = errors.New("bad request") ErrUnauthorized = errors.New("unauthorized") ErrForbidden = errors.New("forbidden") ErrConflict = errors.New("conflict") ) // decodeJSON decodes r.Body into v. Returns false and writes an error on failure. func decodeJSON(w http.ResponseWriter, r *http.Request, v any) bool { dec := json.NewDecoder(r.Body) dec.DisallowUnknownFields() if err := dec.Decode(v); err != nil { renderError(w, http.StatusBadRequest, "Invalid JSON", err.Error()) return false } return true }