package httpresponse import ( "encoding/json" "errors" "fmt" "io" "net/http" ) var ( // ErrEmptyBody is returned when the request body is empty. ErrEmptyBody = errors.New("request body is empty") // ErrInvalidJSON is returned when the request body contains invalid JSON. ErrInvalidJSON = errors.New("invalid JSON") // ErrUnknownFields is returned when strict decoding encounters unknown fields. ErrUnknownFields = errors.New("unknown fields in JSON") ) // ----------------------------------------------------------------------------- // Success Responses // ----------------------------------------------------------------------------- // JSON writes a JSON response with the given status code. // The data is wrapped in the standard response envelope. func JSON(w http.ResponseWriter, r *http.Request, status int, data any) { resp := Response{ Data: data, Meta: newMeta(r), } writeJSON(w, status, resp) } // OK writes a successful JSON response with status 200 OK. func OK(w http.ResponseWriter, r *http.Request, data any) { JSON(w, r, http.StatusOK, data) } // Created writes a successful JSON response with status 201 Created. func Created(w http.ResponseWriter, r *http.Request, data any) { JSON(w, r, http.StatusCreated, data) } // Accepted writes a successful JSON response with status 202 Accepted. func Accepted(w http.ResponseWriter, r *http.Request, data any) { JSON(w, r, http.StatusAccepted, data) } // NoContent writes a successful response with status 204 No Content. func NoContent(w http.ResponseWriter) { w.WriteHeader(http.StatusNoContent) } // ----------------------------------------------------------------------------- // Error Responses // ----------------------------------------------------------------------------- // WriteError writes an error response with the given status code. func WriteError(w http.ResponseWriter, r *http.Request, status int, code, message string, details ...any) { var detailsVal any if len(details) > 0 { detailsVal = details[0] } resp := Response{ Error: &Error{ Code: code, Message: message, Details: detailsVal, }, Meta: newMeta(r), } writeJSON(w, status, resp) } // BadRequest writes a 400 Bad Request error response. func BadRequest(w http.ResponseWriter, r *http.Request, message string) { WriteError(w, r, http.StatusBadRequest, CodeBadRequest, message) } // ValidationError writes a 400 Bad Request error response for validation failures. func ValidationError(w http.ResponseWriter, r *http.Request, message string, details any) { WriteError(w, r, http.StatusBadRequest, CodeValidation, message, details) } // Unauthorized writes a 401 Unauthorized error response. func Unauthorized(w http.ResponseWriter, r *http.Request, message string) { WriteError(w, r, http.StatusUnauthorized, CodeUnauthorized, message) } // Forbidden writes a 403 Forbidden error response. func Forbidden(w http.ResponseWriter, r *http.Request, message string) { WriteError(w, r, http.StatusForbidden, CodeForbidden, message) } // NotFound writes a 404 Not Found error response. func NotFound(w http.ResponseWriter, r *http.Request, message string) { WriteError(w, r, http.StatusNotFound, CodeNotFound, message) } // Conflict writes a 409 Conflict error response. func Conflict(w http.ResponseWriter, r *http.Request, message string) { WriteError(w, r, http.StatusConflict, CodeConflict, message) } // InternalError writes a 500 Internal Server Error response. // The message should be safe to expose to clients; internal details should be logged. func InternalError(w http.ResponseWriter, r *http.Request, message string) { WriteError(w, r, http.StatusInternalServerError, CodeInternal, message) } // ServiceUnavailable writes a 503 Service Unavailable error response. func ServiceUnavailable(w http.ResponseWriter, r *http.Request, message string) { WriteError(w, r, http.StatusServiceUnavailable, "SERVICE_UNAVAILABLE", message) } // ----------------------------------------------------------------------------- // Request Body Decoding // ----------------------------------------------------------------------------- // DecodeJSON decodes JSON from request body into v. // Returns descriptive errors for common failure cases. // Does not enforce strict field matching. func DecodeJSON(r *http.Request, v any) error { if r.Body == nil { return ErrEmptyBody } decoder := json.NewDecoder(r.Body) if err := decoder.Decode(v); err != nil { if errors.Is(err, io.EOF) { return ErrEmptyBody } return fmt.Errorf("%w: %w", ErrInvalidJSON, err) } return nil } // DecodeJSONStrict decodes JSON from request body into v. // Rejects JSON that contains fields not present in the target struct. // Useful for strict API validation to catch client errors early. func DecodeJSONStrict(r *http.Request, v any) error { if r.Body == nil { return ErrEmptyBody } decoder := json.NewDecoder(r.Body) decoder.DisallowUnknownFields() if err := decoder.Decode(v); err != nil { if errors.Is(err, io.EOF) { return ErrEmptyBody } // Check if it's an unknown field error var syntaxErr *json.SyntaxError var unmarshalErr *json.UnmarshalTypeError if errors.As(err, &syntaxErr) || errors.As(err, &unmarshalErr) { return fmt.Errorf("%w: %w", ErrInvalidJSON, err) } // Unknown field errors contain "unknown field" in the message return fmt.Errorf("%w: %w", ErrUnknownFields, err) } return nil } // IsEmptyBodyError checks if an error is ErrEmptyBody. func IsEmptyBodyError(err error) bool { return errors.Is(err, ErrEmptyBody) } // IsInvalidJSONError checks if an error is ErrInvalidJSON. func IsInvalidJSONError(err error) bool { return errors.Is(err, ErrInvalidJSON) } // IsUnknownFieldsError checks if an error is ErrUnknownFields. func IsUnknownFieldsError(err error) bool { return errors.Is(err, ErrUnknownFields) } // ----------------------------------------------------------------------------- // Internal helpers // ----------------------------------------------------------------------------- // writeJSON marshals and writes the response. func writeJSON(w http.ResponseWriter, status int, data any) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(data) }