package errors import ( stdContext "context" "encoding/json" "errors" "fmt" "io" "math" "net/http" "reflect" "github.com/kataras/iris/v12/context" recovery "github.com/kataras/iris/v12/middleware/recover" "github.com/kataras/iris/v12/x/pagination" "golang.org/x/exp/constraints" ) func init() { context.SetHandlerName("iris/x/errors.RecoveryHandler.*", "iris.errors.recover") } // RecoveryHandler is a middleware which recovers from panics and sends an appropriate error response // to the logger and the client. func RecoveryHandler(ctx *context.Context) { defer func() { if err := recovery.PanicRecoveryError(ctx, recover()); err != nil { Internal.LogErr(ctx, err) ctx.StopExecution() } }() ctx.Next() } // Handle handles a generic response and error from a service call and sends a JSON response to the client. // It returns a boolean value indicating whether the handle was successful or not. // If the error is not nil, it calls HandleError to send an appropriate error response to the client. func Handle(ctx *context.Context, resp any, err error) bool { if HandleError(ctx, err) { return false } ctx.StatusCode(http.StatusOK) if resp != nil { if jsonErr := ctx.JSON(resp); jsonErr != nil { // this returns the original error as it's, even if it's handled by the HandleJSONError function. if unsupportedValueErr, ok := jsonErr.(*json.UnsupportedValueError); ok { if unsupportedValueErr.Str == "NaN" { if nanErr := checkNaN(resp); nanErr != nil { newErr := fmt.Errorf("unable to parse response body: json: unspported value: %w", nanErr) LogError(ctx, newErr) return false } } } return false } } return true } // checkNaN checks if any exported field in the struct is NaN and returns an error if it is. func checkNaN(v interface{}) error { val := reflect.ValueOf(v) return checkNaNRecursive(val, val.Type().Name()) } func checkNaNRecursive(val reflect.Value, path string) error { if val.Kind() == reflect.Ptr { if val.IsNil() { return nil } val = val.Elem() } switch val.Kind() { case reflect.Struct: for i := 0; i < val.NumField(); i++ { field := val.Field(i) fieldType := val.Type().Field(i) fieldPath := fmt.Sprintf("%s.%s", path, fieldType.Name) if err := checkNaNRecursive(field, fieldPath); err != nil { return err } } case reflect.Slice: for i := 0; i < val.Len(); i++ { elem := val.Index(i) elemPath := fmt.Sprintf("%s[%d]", path, i) if err := checkNaNRecursive(elem, elemPath); err != nil { return err } } case reflect.Float64: if math.IsNaN(val.Float()) { return fmt.Errorf("NaN value found in field: %s", path) } } return nil } // IDPayload is a simple struct which describes a json id value. type IDPayload[T string | int] struct { ID T `json:"id"` } // HandleCreate handles a create operation and sends a JSON response with the created resource to the client. // It returns a boolean value indicating whether the handle was successful or not. // // If the "respOrID" response is not nil, it sets the status code to 201 (Created) and sends the response as a JSON payload, // however if the given "respOrID" is a string or an int, it sends the response as a JSON payload of {"id": resp}. // If the "err" error is not nil, it calls HandleError to send an appropriate error response to the client. // It sets the status code to 201 (Created) and sends any response as a JSON payload, func HandleCreate(ctx *context.Context, respOrID any, err error) bool { if HandleError(ctx, err) { return false } ctx.StatusCode(http.StatusCreated) if respOrID != nil { switch responseValue := respOrID.(type) { case string: if ctx.JSON(IDPayload[string]{ID: responseValue}) != nil { return false } case int: if ctx.JSON(IDPayload[int]{ID: responseValue}) != nil { return false } default: if ctx.JSON(responseValue) != nil { return false } } } return true } // HandleUpdate handles an update operation and sends a status code to the client. // It returns a boolean value indicating whether the handle was successful or not. // If the error is not nil, it calls HandleError to send an appropriate error response to the client. // If the updated value is true, it sets the status code to 204 (No Content). // If the updated value is false, it sets the status code to 304 (Not Modified). func HandleUpdate(ctx *context.Context, updated bool, err error) bool { if HandleError(ctx, err) { return false } if updated { ctx.StatusCode(http.StatusNoContent) } else { ctx.StatusCode(http.StatusNotModified) } return true } // HandleDelete handles a delete operation and sends a status code to the client. // If the error is not nil, it calls HandleError to send an appropriate error response to the client. // If the deleted value is true, it sets the status code to 204 (No Content). // If the deleted value is false, it sets the status code to 304 (Not Modified). func HandleDelete(ctx *context.Context, deleted bool, err error) bool { return HandleUpdate(ctx, deleted, err) } // HandleDelete handles a delete operation and sends a status code to the client. // If the error is not nil, it calls HandleError to send an appropriate error response to the client. // It sets the status code to 204 (No Content). func HandleDeleteNoContent(ctx *context.Context, err error) bool { return HandleUpdate(ctx, true, err) } // ResponseFunc is a function which takes a context and a generic type T and returns a generic type R and an error. // It is used to bind a request payload to a generic type T and call a service function with it. type ResponseFunc[T, R any] interface { func(stdContext.Context, T) (R, error) } // ResponseOnlyErrorFunc is a function which takes a context and a generic type T and returns an error. // It is used to bind a request payload to a generic type T and call a service function with it. // It is used for functions which do not return a response. type ResponseOnlyErrorFunc[T any] interface { func(stdContext.Context, T) error } // ContextRequestFunc is a function which takes a context and a generic type T and returns an error. // It is used to validate the context before calling a service function. // // See Validation package-level function. type ContextRequestFunc[T any] func(*context.Context, T) error const contextRequestHandlerFuncKey = "iris.errors.ContextRequestHandler" // Validation adds a context validator function to the context. // It returns a middleware which can be used to validate the context before calling a service function. // It panics if the given validators are empty or nil. // // Example: // // r.Post("/", Validation(validateCreateRequest), createHandler(service)) // // func validateCreateRequest(ctx iris.Context, r *CreateRequest) error { // return validation.Join( // validation.String("fullname", r.Fullname).NotEmpty().Fullname().Length(3, 50), // validation.Number("age", r.Age).InRange(18, 130), // validation.Slice("hobbies", r.Hobbies).Length(1, 10), // ) // } func Validation[T any](validators ...ContextRequestFunc[T]) context.Handler { if len(validators) == 0 { return nil } validator := joinContextRequestFuncs[T](validators) return func(ctx *context.Context) { ctx.Values().Set(contextRequestHandlerFuncKey, validator) ctx.Next() } } func joinContextRequestFuncs[T any](requestHandlerFuncs []ContextRequestFunc[T]) ContextRequestFunc[T] { if len(requestHandlerFuncs) == 0 || requestHandlerFuncs[0] == nil { panic("at least one context request handler function is required") } if len(requestHandlerFuncs) == 1 { return requestHandlerFuncs[0] } return func(ctx *context.Context, req T) error { for _, handler := range requestHandlerFuncs { if handler == nil { continue } if err := handler(ctx, req); err != nil { return err } } return nil } } // RequestHandler is an interface which can be implemented by a request payload struct // in order to validate the context before calling a service function. type RequestHandler interface { HandleRequest(*context.Context) error } func validateRequest[T any](ctx *context.Context, req T) bool { var err error // Always run the request's validator first, // so dynamic validators can be customized per path and method. if contextRequestHandler, ok := any(&req).(RequestHandler); ok { err = contextRequestHandler.HandleRequest(ctx) } if err == nil { if v := ctx.Values().Get(contextRequestHandlerFuncKey); v != nil { if contextRequestHandlerFunc, ok := v.(ContextRequestFunc[T]); ok && contextRequestHandlerFunc != nil { err = contextRequestHandlerFunc(ctx, req) } else if contextRequestHandlerFunc, ok := v.(ContextRequestFunc[*T]); ok && contextRequestHandlerFunc != nil { // or a pointer of T. err = contextRequestHandlerFunc(ctx, &req) } } } return err == nil || !HandleError(ctx, err) } // ResponseHandler is an interface which can be implemented by a request payload struct // in order to handle a response before sending it to the client. type ResponseHandler[R any] interface { HandleResponse(ctx *context.Context, response *R) error } // ContextResponseFunc is a function which takes a context, a generic type T and a generic type R and returns an error. type ContextResponseFunc[T, R any] func(*context.Context, T, *R) error const contextResponseHandlerFuncKey = "iris.errors.ContextResponseHandler" func validateResponse[T, R any](ctx *context.Context, req T, resp *R) bool { var err error if contextResponseHandler, ok := any(&req).(ResponseHandler[R]); ok { err = contextResponseHandler.HandleResponse(ctx, resp) } if err == nil { if v := ctx.Values().Get(contextResponseHandlerFuncKey); v != nil { if contextResponseHandlerFunc, ok := v.(ContextResponseFunc[T, R]); ok && contextResponseHandlerFunc != nil { err = contextResponseHandlerFunc(ctx, req, resp) } else if contextResponseHandlerFunc, ok := v.(ContextResponseFunc[*T, R]); ok && contextResponseHandlerFunc != nil { err = contextResponseHandlerFunc(ctx, &req, resp) } } } return err == nil || !HandleError(ctx, err) } // Intercept adds a context response handler function to the context. // It returns a middleware which can be used to intercept the response before sending it to the client. // // Example Code: // // app.Post("/", errors.Intercept(func(ctx iris.Context, req *CreateRequest, resp *CreateResponse) error{ ... }), errors.CreateHandler(service.Create)) func Intercept[T, R any](responseHandlers ...ContextResponseFunc[T, R]) context.Handler { if len(responseHandlers) == 0 { return nil } responseHandler := joinContextResponseFuncs[T, R](responseHandlers) return func(ctx *context.Context) { ctx.Values().Set(contextResponseHandlerFuncKey, responseHandler) ctx.Next() } } func joinContextResponseFuncs[T, R any](responseHandlerFuncs []ContextResponseFunc[T, R]) ContextResponseFunc[T, R] { if len(responseHandlerFuncs) == 0 || responseHandlerFuncs[0] == nil { panic("at least one context response handler function is required") } if len(responseHandlerFuncs) == 1 { return responseHandlerFuncs[0] } return func(ctx *context.Context, req T, resp *R) error { for _, handler := range responseHandlerFuncs { if handler == nil { continue } if err := handler(ctx, req, resp); err != nil { return err } } return nil } } func bindResponse[T, R any, F ResponseFunc[T, R]](ctx *context.Context, fn F, fnInput ...T) (R, bool) { var req T switch len(fnInput) { case 0: var ok bool req, ok = ReadPayload[T](ctx) if !ok { var resp R return resp, false } case 1: req = fnInput[0] default: panic("invalid number of arguments") } if !validateRequest(ctx, req) { var resp R return resp, false } resp, err := fn(ctx, req) if err == nil { if !validateResponse(ctx, req, &resp) { return resp, false } } return resp, !HandleError(ctx, err) } // OK handles a generic response and error from a service call and sends a JSON response to the client. // It returns a boolean value indicating whether the handle was successful or not. // If the error is not nil, it calls HandleError to send an appropriate error response to the client. // It sets the status code to 200 (OK) and sends any response as a JSON payload. // // Useful for Get/List/Fetch operations. func OK[T, R any, F ResponseFunc[T, R]](ctx *context.Context, fn F, fnInput ...T) bool { // or Fetch. resp, ok := bindResponse(ctx, fn, fnInput...) if !ok { return false } return Handle(ctx, resp, nil) } // HandlerInputFunc is a function which takes a context and returns a generic type T. // It is used to call a service function with a generic type T. // It is used for functions which do not bind a request payload. // It is used for XHandler functions. // Developers can design their own HandlerInputFunc functions and use them with the XHandler functions. // To make a value required, stop the context execution through the context.StopExecution function and fire an error // or just use one of the [InvalidArgument].X methods. // // See PathParam, Query and Value package-level helpers too. type HandlerInputFunc[T any] interface { func(ctx *context.Context) T } // GetRequestInputs returns a slice of generic type T from a slice of HandlerInputFunc[T]. // It is exported so end-developers can use it to get the inputs from custom HandlerInputFunc[T] functions. func GetRequestInputs[T any, I HandlerInputFunc[T]](ctx *context.Context, fnInputFunc []I) ([]T, bool) { inputs := make([]T, 0, len(fnInputFunc)) for _, callIn := range fnInputFunc { if callIn == nil { continue } input := callIn(ctx) if ctx.IsStopped() { // if the input is required and it's not provided, then the context is stopped. return nil, false } inputs = append(inputs, input) } return inputs, true } // PathParam returns a HandlerInputFunc which reads a path parameter from the context and returns it as a generic type T. // It is used for XHandler functions. func PathParam[T any, I HandlerInputFunc[T]](paramName string) I { return func(ctx *context.Context) T { paramValue := ctx.Params().Store.Get(paramName) if paramValue == nil { var t T return t } return paramValue.(T) } } // Value returns a HandlerInputFunc which returns a generic type T. // It is used for XHandler functions. func Value[T any, I HandlerInputFunc[T]](value T) I { return func(ctx *context.Context) T { return value } } // Query returns a HandlerInputFunc which reads a URL query from the context and returns it as a generic type T. // It is used for XHandler functions. func Query[T any, I HandlerInputFunc[T]]() I { return func(ctx *context.Context) T { value, ok := ReadQuery[T](ctx) if !ok { var t T return t } return value } } // Handler handles a generic response and error from a service call and sends a JSON response to the client with status code of 200. // // See OK package-level function for more. func Handler[T, R any, F ResponseFunc[T, R], I HandlerInputFunc[T]](fn F, fnInput ...I) context.Handler { return func(ctx *context.Context) { inputs, ok := GetRequestInputs(ctx, fnInput) if !ok { return } OK(ctx, fn, inputs...) } } // ListResponseFunc is a function which takes a context, // a pagination.ListOptions and a generic type T and returns a slice []R, total count of the items and an error. // // It's used on the List function. type ListResponseFunc[T, R any, C constraints.Integer | constraints.Float] interface { func(stdContext.Context, pagination.ListOptions, T /* filter options */) ([]R, C, error) } // List handles a generic response and error from a service paginated call and sends a JSON response to the client. // It returns a boolean value indicating whether the handle was successful or not. // If the error is not nil, it calls HandleError to send an appropriate error response to the client. // It reads the pagination.ListOptions from the URL Query and any filter options of generic T from the request body. // It sets the status code to 200 (OK) and sends a *pagination.List[R] response as a JSON payload. func List[T, R any, C constraints.Integer | constraints.Float, F ListResponseFunc[T, R, C]](ctx *context.Context, fn F, fnInput ...T) bool { listOpts, filter, ok := ReadPaginationOptions[T](ctx) if !ok { return false } if !validateRequest(ctx, filter) { return false } items, totalCount, err := fn(ctx, listOpts, filter) if err != nil { HandleError(ctx, err) return false } resp := pagination.NewList(items, int64(totalCount), filter, listOpts) if !validateResponse(ctx, filter, resp) { return false } return Handle(ctx, resp, err) } // ListHandler handles a generic response and error from a service paginated call and sends a JSON response to the client. // // See List package-level function for more. func ListHandler[T, R any, C constraints.Integer | constraints.Float, F ListResponseFunc[T, R, C], I HandlerInputFunc[T]](fn F, fnInput ...I) context.Handler { return func(ctx *context.Context) { inputs, ok := GetRequestInputs(ctx, fnInput) if !ok { return } List(ctx, fn, inputs...) } } // Create handles a create operation and sends a JSON response with the created resource to the client. // It returns a boolean value indicating whether the handle was successful or not. // If the error is not nil, it calls HandleError to send an appropriate error response to the client. // It sets the status code to 201 (Created) and sends any response as a JSON payload // note that if the response is a string, then it sends an {"id": resp} JSON payload). // // Useful for Insert operations. func Create[T, R any, F ResponseFunc[T, R]](ctx *context.Context, fn F, fnInput ...T) bool { resp, ok := bindResponse(ctx, fn, fnInput...) if !ok { return false } return HandleCreate(ctx, resp, nil) } // CreateHandler handles a create operation and sends a JSON response with the created resource to the client with status code of 201. // // See Create package-level function for more. func CreateHandler[T, R any, F ResponseFunc[T, R], I HandlerInputFunc[T]](fn F, fnInput ...I) context.Handler { return func(ctx *context.Context) { inputs, ok := GetRequestInputs(ctx, fnInput) if !ok { return } Create(ctx, fn, inputs...) } } // NoContent handles a generic response and error from a service call and sends a JSON response to the client. // It returns a boolean value indicating whether the handle was successful or not. // If the error is not nil, it calls HandleError to send an appropriate error response to the client. // It sets the status code to 204 (No Content). // // Useful for Update and Deletion operations. func NoContent[T any, F ResponseOnlyErrorFunc[T]](ctx *context.Context, fn F, fnInput ...T) bool { toFn := func(c stdContext.Context, req T) (bool, error) { return true, fn(ctx, req) } return NoContentOrNotModified(ctx, toFn, fnInput...) } // NoContentHandler handles a generic response and error from a service call and sends a JSON response to the client with status code of 204. // // See NoContent package-level function for more. func NoContentHandler[T any, F ResponseOnlyErrorFunc[T], I HandlerInputFunc[T]](fn F, fnInput ...I) context.Handler { return func(ctx *context.Context) { inputs, ok := GetRequestInputs(ctx, fnInput) if !ok { return } NoContent(ctx, fn, inputs...) } } // NoContent handles a generic response and error from a service call and sends a JSON response to the client. // It returns a boolean value indicating whether the handle was successful or not. // If the error is not nil, it calls HandleError to send an appropriate error response to the client. // If the response is true, it sets the status code to 204 (No Content). // If the response is false, it sets the status code to 304 (Not Modified). // // Useful for Update and Deletion operations. func NoContentOrNotModified[T any, F ResponseFunc[T, bool]](ctx *context.Context, fn F, fnInput ...T) bool { resp, ok := bindResponse(ctx, fn, fnInput...) if !ok { return false } return HandleUpdate(ctx, bool(resp), nil) } // NoContentOrNotModifiedHandler handles a generic response and error from a service call and sends a JSON response to the client with status code of 204 or 304. // // See NoContentOrNotModified package-level function for more. func NoContentOrNotModifiedHandler[T any, F ResponseFunc[T, bool], I HandlerInputFunc[T]](fn F, fnInput ...I) context.Handler { return func(ctx *context.Context) { inputs, ok := GetRequestInputs(ctx, fnInput) if !ok { return } NoContentOrNotModified(ctx, fn, inputs...) } } // ReadPayload reads a JSON payload from the context and returns it as a generic type T. // It also returns a boolean value indicating whether the read was successful or not. // If the read fails, it sends an appropriate error response to the client. func ReadPayload[T any](ctx *context.Context) (T, bool) { var payload T err := ctx.ReadJSON(&payload) if err != nil { if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) { InvalidArgument.Details(ctx, "unable to parse body", "empty body") return payload, false } HandleError(ctx, err) return payload, false } return payload, true } // ReadQuery reads URL query values from the context and returns it as a generic type T. // It also returns a boolean value indicating whether the read was successful or not. // If the read fails, it sends an appropriate error response to the client. func ReadQuery[T any](ctx *context.Context) (T, bool) { var payload T err := ctx.ReadQuery(&payload) if err != nil { HandleError(ctx, err) return payload, false } return payload, true } // ReadPaginationOptions reads the ListOptions from the URL Query and // any filter options of generic T from the request body. func ReadPaginationOptions[T /* T is FilterOptions */ any](ctx *context.Context) (pagination.ListOptions, T, bool) { list, ok := ReadQuery[pagination.ListOptions](ctx) if !ok { var t T return list, t, false } filter, ok := ReadPayload[T](ctx) if !ok { var t T return list, t, false } return list, filter, true }