NEW (HOT) FEATURE: Add custom error handlers on path type parameters error

This commit is contained in:
Gerasimos (Makis) Maropoulos 2021-02-14 15:31:27 +02:00
parent 567c06702f
commit 5990e7f432
No known key found for this signature in database
GPG Key ID: A771A828097B36C7
7 changed files with 140 additions and 69 deletions

View File

@ -28,6 +28,22 @@ The codebase for Dependency Injection, Internationalization and localization and
## Fixes and Improvements
- **New feature:** add the ability to set custom error handlers on path type parameters errors (existing or custom ones). Example Code:
```go
app.Macros().Get("uuid").HandleError(func(ctx iris.Context, err error) {
ctx.StatusCode(iris.StatusBadRequest)
ctx.JSON(iris.Map{
"error": err.Error(),
"message": "invalid path parameter",
})
})
app.Get("/users/{id:uuid}", getUser)
```
- Improve the performance and fix `:int, :int8, :int16, :int32, :int64, :uint, :uint8, :uint16, :uint32, :uint64` path type parameters couldn't accept a positive number written with the plus symbol or with a leading zeroes, e.g. `+42` and `021`.
- The `iris.WithEmptyFormError` option is respected on `context.ReadQuery` method too, as requested at [#1727](https://github.com/kataras/iris/issues/1727). [Example comments](https://github.com/kataras/iris/blob/master/_examples/request-body/read-query/main.go) were updated.
- New `httptest.Strict` option setter to enable the `httpexpect.RequireReporter` instead of the default `httpexpect.AssetReporter. Use that to enable complete test failure on the first error. As requested at: [#1722](https://github.com/kataras/iris/issues/1722).

View File

@ -152,6 +152,15 @@ func main() {
// +------------------------+
// UUIDv4 (and v1) path parameter validation.
// Optionally, set custom handler on path parameter type error:
app.Macros().Get("uuid").HandleError(func(ctx iris.Context, err error) {
ctx.StatusCode(iris.StatusBadRequest)
ctx.JSON(iris.Map{
"error": err.Error(),
"message": "invalid path parameter",
})
})
// http://localhost:8080/user/bb4f33e4-dc08-40d8-9f2b-e8b2bb615c0e -> OK
// http://localhost:8080/user/dsadsa-invalid-uuid -> NOT FOUND
app.Get("/user/{id:uuid}", func(ctx iris.Context) {

View File

@ -3,11 +3,27 @@
package handler
import (
"fmt"
"github.com/kataras/iris/v12/context"
"github.com/kataras/iris/v12/core/memstore"
"github.com/kataras/iris/v12/macro"
)
// ParamErrorHandler is a special type of Iris handler which receives
// any error produced by a path type parameter evaluator and let developers
// customize the output instead of the
// provided error code 404 or anyother status code given on the `else` literal.
//
// Note that the builtin macros return error too, but they're handled
// by the `else` literal (error code). To change this behavior
// and send a custom error response you have to register it:
// app.Macros().Get("uuid").HandleError(func(iris.Context, err error)).
// You can also set custom macros by `app.Macros().Register`.
//
// See macro.HandleError to set it.
type ParamErrorHandler = func(*context.Context, error) // alias.
// CanMakeHandler reports whether a macro template needs a special macro's evaluator handler to be validated
// before procceed to the next handler(s).
// If the template does not contain any dynamic attributes and a special handler is NOT required
@ -24,6 +40,13 @@ func CanMakeHandler(tmpl macro.Template) (needsMacroHandler bool) {
if p.CanEval() {
// if at least one needs it, then create the handler.
needsMacroHandler = true
if p.HandleError != nil {
// Check for its type.
if _, ok := p.HandleError.(ParamErrorHandler); !ok {
panic(fmt.Sprintf("HandleError must be a type of func(iris.Context, error) but got: %T", p.HandleError))
}
}
break
}
}
@ -83,9 +106,15 @@ func MakeFilter(tmpl macro.Template) context.Filter {
return false
}
value := p.Eval(entry.String())
if value == nil {
ctx.StatusCode(p.ErrCode)
value, passed := p.Eval(entry.String())
if !passed {
ctx.StatusCode(p.ErrCode) // status code can change from an error handler, set it here.
if value != nil && p.HandleError != nil {
// The "value" is an error here, always (see template.Eval).
// This is always a type of ParamErrorHandler at this state (see CanMakeHandler).
p.HandleError.(ParamErrorHandler)(ctx, value.(error))
}
return false
}

View File

@ -125,6 +125,12 @@ func convertBuilderFunc(fn interface{}) ParamFuncBuilder {
numFields := typFn.NumIn()
panicIfErr := func(i int, err error) {
if err != nil {
panic(fmt.Sprintf("on field index: %d: %v", i, err))
}
}
return func(args []string) reflect.Value {
if len(args) != numFields {
// no variadics support, for now.
@ -138,12 +144,6 @@ func convertBuilderFunc(fn interface{}) ParamFuncBuilder {
// try to convert the string literal as we get it from the parser.
var (
val interface{}
panicIfErr = func(i int, err error) {
if err != nil {
panic(fmt.Sprintf("on field index: %d: %v", i, err))
}
}
)
// try to get the value based on the expected type.
@ -255,6 +255,7 @@ type (
trailing bool
Evaluator ParamEvaluator
handleError interface{}
funcs []ParamFunc
}
@ -312,6 +313,17 @@ func (m *Macro) Trailing() bool {
return m.trailing
}
// HandleError registers a handler which will be executed
// when a parameter evaluator returns false and a non nil value which is a type of `error`.
// The "fnHandler" value MUST BE a type of `func(iris.Context, err error)`,
// otherwise the program will receive a panic before server startup.
// The status code of the ErrCode (`else` literal) is set
// before the error handler but it can be modified inside the handler itself.
func (m *Macro) HandleError(fnHandler interface{}) *Macro { // See handler.MakeFilter.
m.handleError = fnHandler
return m
}
// func (m *Macro) SetParamResolver(fn func(memstore.Entry) interface{}) *Macro {
// m.ParamResolver = fn
// return m

View File

@ -66,6 +66,8 @@ func TestGoodParamFuncName(t *testing.T) {
}
func testEvaluatorRaw(t *testing.T, macroEvaluator *Macro, input string, expectedType reflect.Kind, pass bool, i int) {
t.Helper()
if macroEvaluator.Evaluator == nil && pass {
return // if not evaluator defined then it should allow everything.
}
@ -120,7 +122,7 @@ func TestIntEvaluatorRaw(t *testing.T) {
{false, "-18446744073709553213213213213213121615"}, // 5
{false, "42 18446744073709551615"}, // 6
{false, "--42"}, // 7
{false, "+42"}, // 8
{true, "+42"}, // 8
{false, "main.css"}, // 9
{false, "/assets/main.css"}, // 10
}
@ -130,6 +132,15 @@ func TestIntEvaluatorRaw(t *testing.T) {
}
}
func BenchmarkIntEvaluatorRaw(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
Int.Evaluator("1234568320")
Int.Evaluator("-12345678999321")
}
}
func TestInt8EvaluatorRaw(t *testing.T) {
tests := []struct {
pass bool
@ -145,7 +156,7 @@ func TestInt8EvaluatorRaw(t *testing.T) {
{false, "-18446744073709553213213213213213121615"}, // 7
{false, "42 18446744073709551615"}, // 8
{false, "--42"}, // 9
{false, "+42"}, // 10
{true, "+42"}, // 10
{false, "main.css"}, // 11
{false, "/assets/main.css"}, // 12
}
@ -170,7 +181,7 @@ func TestInt16EvaluatorRaw(t *testing.T) {
{false, "-18446744073709553213213213213213121615"}, // 7
{false, "42 18446744073709551615"}, // 8
{false, "--42"}, // 9
{false, "+42"}, // 10
{true, "+42"}, // 10
{false, "main.css"}, // 11
{false, "/assets/main.css"}, // 12
}
@ -197,7 +208,7 @@ func TestInt32EvaluatorRaw(t *testing.T) {
{false, "-18446744073709553213213213213213121615"}, // 9
{false, "42 18446744073709551615"}, // 10
{false, "--42"}, // 11
{false, "+42"}, // 12
{true, "+42"}, // 12
{false, "main.css"}, // 13
{false, "/assets/main.css"}, // 14
}
@ -278,7 +289,7 @@ func TestUint8EvaluatorRaw(t *testing.T) {
{false, "+1"}, // 9
{false, "18446744073709551615"}, // 10
{false, "9223372036854775807"}, // 11
{false, "021"}, // 12 - no leading zeroes are allowed.
{true, "021"}, // 12 - leading zeroes are allowed.
{false, "300"}, // 13
{true, "0"}, // 14
{true, "255"}, // 15

View File

@ -1,6 +1,8 @@
package macro
import (
"errors"
"fmt"
"strconv"
"strings"
@ -47,19 +49,14 @@ var (
}
})
simpleNumberEval = MustRegexp("^-?[0-9]+$")
// Int or number type
// both positive and negative numbers, actual value can be min-max int64 or min-max int32 depends on the arch.
// If x64: -9223372036854775808 to 9223372036854775807.
// If x32: -2147483648 to 2147483647 and etc..
Int = NewMacro("int", "number", false, false, func(paramValue string) (interface{}, bool) {
if !simpleNumberEval(paramValue) {
return nil, false
}
v, err := strconv.Atoi(paramValue)
if err != nil {
return nil, false
return err, false
}
return v, true
@ -89,13 +86,9 @@ var (
// Int8 type
// -128 to 127.
Int8 = NewMacro("int8", "", false, false, func(paramValue string) (interface{}, bool) {
if !simpleNumberEval(paramValue) {
return nil, false
}
v, err := strconv.ParseInt(paramValue, 10, 8)
if err != nil {
return nil, false
return err, false
}
return int8(v), true
}).
@ -118,13 +111,9 @@ var (
// Int16 type
// -32768 to 32767.
Int16 = NewMacro("int16", "", false, false, func(paramValue string) (interface{}, bool) {
if !simpleNumberEval(paramValue) {
return nil, false
}
v, err := strconv.ParseInt(paramValue, 10, 16)
if err != nil {
return nil, false
return err, false
}
return int16(v), true
}).
@ -147,13 +136,9 @@ var (
// Int32 type
// -2147483648 to 2147483647.
Int32 = NewMacro("int32", "", false, false, func(paramValue string) (interface{}, bool) {
if !simpleNumberEval(paramValue) {
return nil, false
}
v, err := strconv.ParseInt(paramValue, 10, 32)
if err != nil {
return nil, false
return err, false
}
return int32(v), true
}).
@ -176,13 +161,9 @@ var (
// Int64 as int64 type
// -9223372036854775808 to 9223372036854775807.
Int64 = NewMacro("int64", "long", false, false, func(paramValue string) (interface{}, bool) {
if !simpleNumberEval(paramValue) {
return nil, false
}
v, err := strconv.ParseInt(paramValue, 10, 64)
if err != nil { // if err == strconv.ErrRange...
return nil, false
return err, false
}
return v, true
}).
@ -215,7 +196,7 @@ var (
Uint = NewMacro("uint", "", false, false, func(paramValue string) (interface{}, bool) {
v, err := strconv.ParseUint(paramValue, 10, strconv.IntSize) // 32,64...
if err != nil {
return nil, false
return err, false
}
return uint(v), true
}).
@ -241,17 +222,12 @@ var (
}
})
uint8Eval = MustRegexp("^([0-9]|[1-8][0-9]|9[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$")
// Uint8 as uint8 type
// 0 to 255.
Uint8 = NewMacro("uint8", "", false, false, func(paramValue string) (interface{}, bool) {
if !uint8Eval(paramValue) {
return nil, false
}
v, err := strconv.ParseUint(paramValue, 10, 8)
if err != nil {
return nil, false
return err, false
}
return uint8(v), true
}).
@ -282,7 +258,7 @@ var (
Uint16 = NewMacro("uint16", "", false, false, func(paramValue string) (interface{}, bool) {
v, err := strconv.ParseUint(paramValue, 10, 16)
if err != nil {
return nil, false
return err, false
}
return uint16(v), true
}).
@ -307,7 +283,7 @@ var (
Uint32 = NewMacro("uint32", "", false, false, func(paramValue string) (interface{}, bool) {
v, err := strconv.ParseUint(paramValue, 10, 32)
if err != nil {
return nil, false
return err, false
}
return uint32(v), true
}).
@ -332,7 +308,7 @@ var (
Uint64 = NewMacro("uint64", "", false, false, func(paramValue string) (interface{}, bool) {
v, err := strconv.ParseUint(paramValue, 10, 64)
if err != nil {
return nil, false
return err, false
}
return v, true
}).
@ -366,21 +342,25 @@ var (
// in this case.
v, err := strconv.ParseBool(paramValue)
if err != nil {
return nil, false
return err, false
}
return v, true
})
// ErrParamNotAlphabetical is fired when the parameter value is not an alphabetical text.
ErrParamNotAlphabetical = errors.New("parameter is not alphabetical")
alphabeticalEval = MustRegexp("^[a-zA-Z ]+$")
// Alphabetical letter type
// letters only (upper or lowercase)
Alphabetical = NewMacro("alphabetical", "", false, false, func(paramValue string) (interface{}, bool) {
if !alphabeticalEval(paramValue) {
return nil, false
return fmt.Errorf("%s: %w", paramValue, ErrParamNotAlphabetical), false
}
return paramValue, true
})
// ErrParamNotFile is fired when the parameter value is not a form of a file.
ErrParamNotFile = errors.New("parameter is not a file")
fileEval = MustRegexp("^[a-zA-Z0-9_.-]*$")
// File type
// letters (upper or lowercase)
@ -391,7 +371,7 @@ var (
// no spaces! or other character
File = NewMacro("file", "", false, false, func(paramValue string) (interface{}, bool) {
if !fileEval(paramValue) {
return nil, false
return fmt.Errorf("%s: %w", paramValue, ErrParamNotFile), false
}
return paramValue, true
})
@ -409,7 +389,7 @@ var (
UUID = NewMacro("uuid", "uuidv4", false, false, func(paramValue string) (interface{}, bool) {
_, err := uuid.Parse(paramValue) // this is x10+ times faster than regexp.
if err != nil {
return nil, false
return err, false
}
return paramValue, true

View File

@ -34,6 +34,9 @@ type TemplateParam struct {
Name string `json:"name"`
Index int `json:"index"`
ErrCode int `json:"errCode"`
// Note that, the value MUST BE a type of `func(iris.Context, err error)`.
HandleError interface{} `json:"-"` /* It's not an typed value because of import-cycle,
// neither a special struct required, see `handler.MakeFilter`. */
TypeEvaluator ParamEvaluator `json:"-"`
Funcs []reflect.Value `json:"-"`
@ -53,7 +56,7 @@ func (p TemplateParam) preComputed() TemplateParam {
// i.e {myparam} or {myparam:string} or {myparam:path} ->
// their type evaluator is nil because they don't do any checks and they don't change
// the default parameter value's type (string) so no need for any work).
p.canEval = p.TypeEvaluator != nil || len(p.Funcs) > 0 || p.ErrCode != parser.DefaultParamErrorCode
p.canEval = p.TypeEvaluator != nil || len(p.Funcs) > 0 || p.ErrCode != parser.DefaultParamErrorCode || p.HandleError != nil
return p
}
@ -64,6 +67,10 @@ func (p *TemplateParam) CanEval() bool {
return p.canEval
}
type errorInterface interface {
Error() string
}
// Eval is the most critical part of the TemplateParam.
// It is responsible to return the type-based value if passed otherwise nil.
// If the "paramValue" is the correct type of the registered parameter type
@ -71,21 +78,27 @@ func (p *TemplateParam) CanEval() bool {
//
// It is called from the converted macro handler (middleware)
// from the higher-level component of "kataras/iris/macro/handler#MakeHandler".
func (p *TemplateParam) Eval(paramValue string) interface{} {
func (p *TemplateParam) Eval(paramValue string) (interface{}, bool) {
if p.TypeEvaluator == nil {
for _, fn := range p.stringInFuncs {
if !fn(paramValue) {
return nil
return nil, false
}
}
return paramValue
return paramValue, true
}
// fmt.Printf("macro/template.go#L88: Eval for param value: %s and p.Src: %s\n", paramValue, p.Src)
newValue, passed := p.TypeEvaluator(paramValue)
if !passed {
return nil
if newValue != nil && p.HandleError != nil { // return this error only when a HandleError was registered.
if _, ok := newValue.(errorInterface); ok {
return newValue, false // this is an error, see `HandleError` and `MakeFilter`.
}
}
return nil, false
}
if len(p.Funcs) > 0 {
@ -94,14 +107,14 @@ func (p *TemplateParam) Eval(paramValue string) interface{} {
// or make it as func(interface{}) bool and pass directly the "newValue"
// but that would not be as easy for end-developer, so keep that "slower":
if !evalFunc.Call(paramIn)[0].Interface().(bool) { // i.e func(paramValue int) bool
return nil
return nil, false
}
}
}
// fmt.Printf("macro/template.go: passed with value: %v and type: %T\n", newValue, newValue)
return newValue
return newValue, true
}
// Parse takes a full route path and a macro map (macro map contains the macro types with their registered param functions)
@ -130,6 +143,7 @@ func Parse(src string, macros Macros) (Template, error) {
Name: p.Name,
Index: idx,
ErrCode: p.ErrorCode,
HandleError: m.handleError,
TypeEvaluator: typEval,
}