diff --git a/go.mod b/go.mod index 57d19e1..d778a51 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,9 @@ go 1.24.2 require ( github.com/go-chi/chi/v5 v5.2.1 + github.com/go-chi/render v1.0.3 github.com/golang-cz/devslog v0.0.13 github.com/google/uuid v1.6.0 ) + +require github.com/ajg/form v1.5.1 // indirect diff --git a/go.sum b/go.sum index 586159e..1d48e3e 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,9 @@ +github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= +github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= +github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= github.com/golang-cz/devslog v0.0.13 h1:JkJ6PPNSOCBpYyU03v3xw7WgpChQ3AYFqgRbYBhUk/Y= github.com/golang-cz/devslog v0.0.13/go.mod h1:bSe5bm0A7Nyfqtijf1OMNgVJHlWEuVSXnkuASiE1vV8= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= diff --git a/pkg/api/error.go b/pkg/api/error.go new file mode 100644 index 0000000..99b8f77 --- /dev/null +++ b/pkg/api/error.go @@ -0,0 +1,59 @@ +package api + +import ( + "log/slog" + "net/http" + + "github.com/go-chi/render" +) + +type ErrRepsonse struct { + Err error `json:"-"` // low-level runtime error + HTTPStatusCode int `json:"-"` // http response status code + + StatusText string `json:"status"` // user-level status message + ErrorText string `json:"error,omitempty"` // application-level error message, for debugging + Details interface{} `json:"details,omitempty"` +} + +func NewErrorResponse( + err error, + statusCode int, + statusText string, + errorText string, + details interface{}, +) *ErrRepsonse { + return &ErrRepsonse{ + Err: err, + HTTPStatusCode: statusCode, + StatusText: statusText, + ErrorText: errorText, + Details: details, + } +} + +func (e *ErrRepsonse) LogValue() slog.Value { + return slog.GroupValue( + slog.String("error", e.Err.Error()), + slog.Int("http_status_code", e.HTTPStatusCode), + slog.String("status_text", e.StatusText), + slog.String("error_text", e.ErrorText), + slog.Any("details", e.Details), + ) +} + +func (e *ErrRepsonse) Render(_ http.ResponseWriter, r *http.Request) error { + render.Status(r, e.HTTPStatusCode) + + return nil +} + +func RenderErrorResponse(err error) *ErrRepsonse { + return &ErrRepsonse{ + Err: err, + HTTPStatusCode: http.StatusUnprocessableEntity, + StatusText: "Error rendering response.", + ErrorText: err.Error(), + Details: nil, + } +} diff --git a/pkg/api/handler.go b/pkg/api/handler.go new file mode 100644 index 0000000..f0804c3 --- /dev/null +++ b/pkg/api/handler.go @@ -0,0 +1,35 @@ +package api + +import ( + "log/slog" + "net/http" + + "github.com/go-chi/render" +) + +type RendererFunc func(http.ResponseWriter, *http.Request) (render.Renderer, *ErrRepsonse) + +func HandleRendererFunc(fn RendererFunc, logger *slog.Logger) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var resp render.Renderer + + okResp, errResp := fn(w, r) + if errResp != nil { + logger.ErrorContext( + r.Context(), + "error in handler", + slog.Any("error", errResp), + ) + + resp = errResp + } else { + resp = okResp + } + + if err := render.Render(w, r, resp); err != nil { + logger.ErrorContext(r.Context(), "error rendering response", slog.Any("error", err)) + + render.Render(w, r, RenderErrorResponse(err)) //nolint: errcheck,gosec // ignore error + } + } +} diff --git a/pkg/chi/middlewares/middlewares.go b/pkg/api/middlewares.go similarity index 93% rename from pkg/chi/middlewares/middlewares.go rename to pkg/api/middlewares.go index d82703d..f3d096a 100644 --- a/pkg/chi/middlewares/middlewares.go +++ b/pkg/api/middlewares.go @@ -7,7 +7,7 @@ import ( "time" "github.com/go-chi/chi/v5" - chimiddleware "github.com/go-chi/chi/v5/middleware" + "github.com/go-chi/chi/v5/middleware" "github.com/google/uuid" ) @@ -16,7 +16,7 @@ type RequestIDContextKey struct{} func RequestIDMiddleware() func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ww := chimiddleware.NewWrapResponseWriter(w, r.ProtoMajor) + ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor) requestID := r.Header.Get("X-Request-ID") if requestID == "" { @@ -39,7 +39,7 @@ func RequestLoggerMiddleware(logger *slog.Logger) func(http.Handler) http.Handle start := time.Now() // Create a response writer wrapper to capture status and size - ww := chimiddleware.NewWrapResponseWriter(w, r.ProtoMajor) + ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor) requestID, ok := r.Context().Value(RequestIDContextKey{}).(string) if !ok {