blob: 1b30d915b29bc89ed18a8c4525dee913ec29c5b7 [file]
package httpx
import (
"encoding/json"
"errors"
"log/slog"
"net/http"
)
// errorBody is the JSON shape of every failure response.
type errorBody struct {
Status int `json:"status"`
Error string `json:"error"`
Message string `json:"message"`
Fields []FieldError `json:"fieldErrors,omitempty"`
}
// WriteJSON writes v as JSON with the given status. A nil v (or status 204) writes no
// body, which is what the delete endpoints need.
func WriteJSON(w http.ResponseWriter, status int, v any) {
if v == nil || status == http.StatusNoContent {
w.WriteHeader(status)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
if err := json.NewEncoder(w).Encode(v); err != nil {
// The status line is already sent, so this can only be logged.
slog.Error("write json response", "error", err)
}
}
// WriteError renders err as a JSON failure. Anything that is not an *Error becomes a
// bare 500: the message of an unexpected error may name internal state, so it is
// logged rather than returned to the caller.
func WriteError(w http.ResponseWriter, r *http.Request, err error) {
var e *Error
if !errors.As(err, &e) {
slog.Error("unhandled error",
"method", r.Method, "path", r.URL.Path, "error", err)
e = &Error{Status: http.StatusInternalServerError, Message: "Internal server error"}
}
if e.Status >= http.StatusInternalServerError {
slog.Error("server error",
"method", r.Method, "path", r.URL.Path, "error", err)
}
WriteJSON(w, e.Status, errorBody{
Status: e.Status,
Error: http.StatusText(e.Status),
Message: e.Message,
Fields: e.Fields,
})
}
// DecodeJSON reads the request body into dst.
//
// Unknown fields are ignored, matching Spring Boot's Jackson defaults
// (FAIL_ON_UNKNOWN_PROPERTIES is off), so a client sending an extra key gets the same
// acceptance it got from the Java service.
func DecodeJSON(r *http.Request, dst any) error {
if r.Body == nil {
return BadRequest("Request body is required")
}
if err := json.NewDecoder(r.Body).Decode(dst); err != nil {
return BadRequest("Malformed JSON request body")
}
return nil
}
// Validator is implemented by request payloads that carry constraints.
type Validator interface {
Validate() []FieldError
}
// Bind decodes the request body into dst and validates it when dst supports it.
func Bind(r *http.Request, dst any) error {
if err := DecodeJSON(r, dst); err != nil {
return err
}
if v, ok := dst.(Validator); ok {
if fields := v.Validate(); len(fields) > 0 {
return Invalid(fields)
}
}
return nil
}