package api import ( "encoding/json" "net/http" ) // Error responses always use this envelope: // // {"error": {"code": "...", "message": "..."}} // // Known codes: invalid_json, validation_failed, not_found, invalid_id, // method_not_allowed, internal_error, timeout, unavailable. Add new codes // here when needed. type errorResponse struct { Error errorDetail `json:"error"` } type errorDetail struct { Code string `json:"code"` Message string `json:"message"` } // maxBodySize caps JSON request bodies at 1 MiB. const maxBodySize = 1 << 20 // writeJSON sets the content type and encodes v. func writeJSON(w http.ResponseWriter, status int, v any) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(status) // The status line is already sent, so an encode failure can only be // logged — and only by wrapping the ResponseWriter. Not worth it here. _ = json.NewEncoder(w).Encode(v) } // writeError emits the error envelope with the given status and code. func writeError(w http.ResponseWriter, status int, code, message string) { writeJSON(w, status, errorResponse{Error: errorDetail{Code: code, Message: message}}) } // decodeJSON strictly decodes the request body into dst: unknown fields and // bodies over maxBodySize are rejected. It writes the 400 response itself // and returns false, telling the handler to stop. func decodeJSON(w http.ResponseWriter, r *http.Request, dst any) bool { r.Body = http.MaxBytesReader(w, r.Body, maxBodySize) dec := json.NewDecoder(r.Body) dec.DisallowUnknownFields() if err := dec.Decode(dst); err != nil { writeError(w, http.StatusBadRequest, "invalid_json", "request body is not valid JSON: "+err.Error()) return false } return true }