66 "encoding/json"
77 "io"
88 "net/http"
9- "strings"
109
1110 cerrors "github.com/contiamo/go-base/v4/pkg/errors"
1211 "github.com/contiamo/go-base/v4/pkg/tracing"
@@ -20,6 +19,9 @@ const (
2019 Megabyte = 1024 * 1024
2120)
2221
22+ // ErrorParser is a function that parses an error into an HTTP status code and response body.
23+ type ErrorParser = func (ctx context.Context , err error , debug bool ) (int , interface {})
24+
2325// BaseHandler contains all the base functions every handler should have
2426type BaseHandler interface {
2527 tracing.Tracer
@@ -34,48 +36,105 @@ type BaseHandler interface {
3436// NewBaseHandler creates a new base HTTP handler that
3537// contains shared logic among all the handlers.
3638// The handler supports parsing and writing JSON objects
37- // `maxBodyBytes` is the maximal request body size, < 0 means the default Megabyte.
39+ // `maxBodyBytes` is the maximal request body size, < 0 means the default Megabyte. Using 0 will disable the limit.
3840// `componentName` is used for tracing to identify to which
3941// component this handler belongs to.
40- func NewBaseHandler (componentName string , maxBodyBytes int64 , debug bool ) BaseHandler {
42+ func NewBaseHandler (componentName string , maxBodyBytes int64 , debug bool ) * Handler {
4143 if maxBodyBytes < 0 {
4244 maxBodyBytes = Megabyte
4345 }
44- return & baseHandler {
46+ return & Handler {
4547 Tracer : tracing .NewTracer ("handlers" , componentName ),
46- maxBodyBytes : maxBodyBytes ,
47- debug : debug ,
48+ MaxBodyBytes : maxBodyBytes ,
49+ Debug : debug ,
4850 }
4951}
5052
5153// NewBasehandlerWithTracer create a new base HTTP handler, like NewBaseHandler, but allows
5254// the caller to configure the Tracer implementation independently.
53- func NewBaseHandlerWithTracer (tracer tracing.Tracer , maxBodyBytes int64 , debug bool ) BaseHandler {
55+ //
56+ // Deprecated: you can now configure/override the default Tracer using
57+ //
58+ // h := NewBaseHandler(componentName, maxBodyBytes, debug)
59+ // h.Tracer = tracing.NewTracer("handlers", componentName)
60+ func NewBaseHandlerWithTracer (tracer tracing.Tracer , maxBodyBytes int64 , debug bool ) * Handler {
5461 if maxBodyBytes < 0 {
5562 maxBodyBytes = Megabyte
5663 }
57- return & baseHandler {
64+ return & Handler {
5865 Tracer : tracer ,
59- maxBodyBytes : maxBodyBytes ,
60- debug : debug ,
66+ MaxBodyBytes : maxBodyBytes ,
67+ ErrorParser : DefaultErrorParser ,
68+ Debug : debug ,
6169 }
6270}
6371
64- type baseHandler struct {
72+ // Handler is the default implementation of BaseHandler and is suitable for use in
73+ // most REST API implementations.
74+ type Handler struct {
6575 tracing.Tracer
66- maxBodyBytes int64
67- debug bool
76+ // ErrorParser is used to parse error objects into HTTP status codes and response bodies.
77+ ErrorParser ErrorParser
78+ // MaxBodyBytes is the maximal request body size, < 0 means the default Megabyte.
79+ // Using 0 will disable the limit and allow parsing streams.
80+ MaxBodyBytes int64
81+ // Debug was used to enable/disable Debug mode, when enabled error messages will be included in responses.
82+ Debug bool
6883}
6984
70- func (h * baseHandler ) Error (ctx context.Context , w http.ResponseWriter , err error ) {
85+ func (h * Handler ) Error (ctx context.Context , w http.ResponseWriter , err error ) {
7186 span , _ := h .StartSpan (ctx , "Error" )
7287 defer h .FinishSpan (span , nil )
7388
74- if err == nil {
75- w .WriteHeader (http .StatusOK )
89+ parser := h .ErrorParser
90+ if parser == nil {
91+ parser = DefaultErrorParser
92+ }
93+
94+ status , resp := parser (ctx , err , h .Debug )
95+ h .Write (ctx , w , status , resp )
96+ }
97+
98+ func (h * Handler ) Write (ctx context.Context , w http.ResponseWriter , status int , obj interface {}) {
99+ span , _ := h .StartSpan (ctx , "Write" )
100+ var err error
101+ defer func () {
102+ h .FinishSpan (span , err )
103+ }()
104+
105+ if obj == nil {
106+ w .WriteHeader (status )
76107 return
77108 }
78109
110+ w .Header ().Set ("Content-Type" , "application/json" )
111+ w .WriteHeader (status )
112+ enc := json .NewEncoder (w )
113+ err = enc .Encode (obj )
114+ }
115+
116+ func (h * Handler ) Parse (r * http.Request , out interface {}) error {
117+ var body io.Reader = r .Body
118+ if h .MaxBodyBytes > 0 {
119+ body = io .LimitReader (r .Body , h .MaxBodyBytes )
120+ }
121+
122+ dec := json .NewDecoder (body )
123+ err := dec .Decode (out )
124+ if err != nil {
125+ return & parseError {cause : err }
126+ }
127+ return nil
128+ }
129+
130+ // DefaultErrorParser provides a reasonable default error parser that can handle
131+ // the various sentile errors in go-base as well as ozzo-validation errors.
132+ func DefaultErrorParser (ctx context.Context , err error , debug bool ) (int , interface {}) {
133+ // hey what are you doing here?
134+ if err == nil {
135+ return http .StatusOK , nil
136+ }
137+
79138 genErrResp := cerrors.ErrorResponse {
80139 Errors : []cerrors.APIErrorMessenger {
81140 & cerrors.GeneralError {
@@ -84,93 +143,68 @@ func (h *baseHandler) Error(ctx context.Context, w http.ResponseWriter, err erro
84143 }},
85144 }
86145
87- // Handler concrete errors:
88- // we can extend this error list in the future if needed
146+ // Handle sentinel errors
89147 switch err {
90- case cerrors .ErrNotImplemented :
91- h .Write (ctx , w , http .StatusNotImplemented , genErrResp )
92- return
93- case cerrors .ErrAuthorization :
94- h .Write (ctx , w , http .StatusUnauthorized , genErrResp )
95- return
96148 case cerrors .ErrPermission :
97- h .Write (ctx , w , http .StatusForbidden , genErrResp )
98- return
99- case cerrors .ErrForm :
100- h .Write (ctx , w , http .StatusUnprocessableEntity , genErrResp )
101- return
102- case sql .ErrNoRows , cerrors .ErrNotFound :
103- h .Write (ctx , w , http .StatusNotFound , genErrResp )
104- return
149+ return http .StatusForbidden , genErrResp
150+ case cerrors .ErrAuthorization :
151+ return http .StatusUnauthorized , genErrResp
152+ case cerrors .ErrInternal :
153+ return http .StatusInternalServerError , genErrResp
105154 case cerrors .ErrInvalidParameters :
106- h . Write ( ctx , w , http .StatusBadRequest , genErrResp )
107- return
108- case cerrors . ErrUnsupportedMediaType :
109- h . Write ( ctx , w , http . StatusUnsupportedMediaType , genErrResp )
110- return
155+ return http .StatusBadRequest , genErrResp
156+ case cerrors . ErrUnmarshalling , cerrors . ErrForm :
157+ return http . StatusUnprocessableEntity , genErrResp
158+ case sql . ErrNoRows , cerrors . ErrNotFound :
159+ return http . StatusNotFound , genErrResp
111160 case cerrors .ErrNotImplemented :
112- h .Write (ctx , w , http .StatusNotImplemented , genErrResp )
113- return
161+ return http .StatusNotImplemented , genErrResp
162+ case cerrors .ErrUnsupportedMediaType :
163+ return http .StatusUnsupportedMediaType , genErrResp
114164 }
115165
116- if strings .HasPrefix (err .Error (), cerrors .ErrUnmarshalling .Error ()) {
117- h .Write (ctx , w , http .StatusUnprocessableEntity , genErrResp )
118- return
166+ // Handle error types that wrap other errors
167+ var parseErr parseError
168+ if errors .As (err , & parseErr ) {
169+ return http .StatusUnprocessableEntity , genErrResp
170+ }
171+
172+ var userErr cerrors.UserError
173+ if errors .As (err , & userErr ) {
174+ return http .StatusBadRequest , genErrResp
119175 }
120176
121- // Handle error types that wrap other errors
122177 switch e := err .(type ) {
123- // covers ozzo v1 errors and go-base ValidationErrors
124178 case cerrors.ValidationErrors :
125- h .Write (
126- ctx ,
127- w ,
128- http .StatusUnprocessableEntity ,
129- cerrors .ValidationErrorsToFieldErrorResponse (e ),
130- )
131- return
179+ return http .StatusUnprocessableEntity , cerrors .ValidationErrorsToFieldErrorResponse (e )
132180 case validation.Errors :
133- h .Write (
134- ctx ,
135- w ,
136- http .StatusUnprocessableEntity ,
137- cerrors .ValidationErrorsToFieldErrorResponse (cerrors .ValidationErrors (e )),
138- )
139- return
140- case cerrors.UserError :
141- h .Write (ctx , w , http .StatusBadRequest , genErrResp )
142- return
181+ return http .StatusUnprocessableEntity , cerrors .ValidationErrorsToFieldErrorResponse (e )
143182 default :
144- if ! h . debug {
183+ if ! debug {
145184 for idx , e := range genErrResp .Errors {
146185 genErrResp .Errors [idx ] = e .Scrubbed (cerrors .ErrInternal .Error ())
147186 }
148187 }
149- h . Write ( ctx , w , http .StatusInternalServerError , genErrResp )
188+ return http .StatusInternalServerError , genErrResp
150189 }
151190}
152191
153- func (h * baseHandler ) Write (ctx context.Context , w http.ResponseWriter , status int , obj interface {}) {
154- span , _ := h .StartSpan (ctx , "Write" )
155- var err error
156- defer func () {
157- h .FinishSpan (span , err )
158- }()
192+ type parseError struct {
193+ cause error
194+ }
159195
160- w .Header ().Set ("Content-Type" , "application/json" )
161- w .WriteHeader (status )
162- if obj != nil {
163- enc := json .NewEncoder (w )
164- err = enc .Encode (obj )
165- }
196+ func (e parseError ) Error () string {
197+ return cerrors .ErrUnmarshalling .Error () + ": " + e .cause .Error ()
166198}
167199
168- func (h * baseHandler ) Parse (r * http.Request , out interface {}) error {
169- limitedBody := io .LimitReader (r .Body , h .maxBodyBytes )
170- dec := json .NewDecoder (limitedBody )
171- err := dec .Decode (out )
172- if err != nil {
173- return errors .Wrap (err , cerrors .ErrUnmarshalling .Error ())
174- }
175- return nil
200+ func (e parseError ) Unwrap () error {
201+ return e .cause
202+ }
203+
204+ func (e parseError ) As (target interface {}) bool {
205+ return errors .As (e .cause , & target )
206+ }
207+
208+ func (e parseError ) Is (target error ) bool {
209+ return errors .Is (e .cause , target )
176210}
0 commit comments