mirror of
https://github.com/kataras/iris.git
synced 2025-01-23 02:31:04 +01:00
NEW (HOT) FEATURE: Add custom error handlers on path type parameters error
This commit is contained in:
parent
567c06702f
commit
5990e7f432
16
HISTORY.md
16
HISTORY.md
|
@ -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).
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
@ -254,8 +254,9 @@ type (
|
|||
master bool
|
||||
trailing bool
|
||||
|
||||
Evaluator ParamEvaluator
|
||||
funcs []ParamFunc
|
||||
Evaluator ParamEvaluator
|
||||
handleError interface{}
|
||||
funcs []ParamFunc
|
||||
}
|
||||
|
||||
// ParamFuncBuilder is a func
|
||||
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,22 +342,26 @@ var (
|
|||
// in this case.
|
||||
v, err := strconv.ParseBool(paramValue)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
return err, false
|
||||
}
|
||||
return v, true
|
||||
})
|
||||
|
||||
alphabeticalEval = MustRegexp("^[a-zA-Z ]+$")
|
||||
// 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
|
||||
})
|
||||
|
||||
fileEval = MustRegexp("^[a-zA-Z0-9_.-]*$")
|
||||
// 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)
|
||||
// numbers (0-9)
|
||||
|
@ -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
|
||||
|
|
|
@ -30,10 +30,13 @@ type TemplateParam struct {
|
|||
Src string `json:"src"` // the unparsed param'false source
|
||||
// Type is not useful anywhere here but maybe
|
||||
// it's useful on host to decide how to convert the path template to specific router's syntax
|
||||
Type ast.ParamType `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Index int `json:"index"`
|
||||
ErrCode int `json:"errCode"`
|
||||
Type ast.ParamType `json:"type"`
|
||||
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,
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user