mirror of
https://github.com/kataras/iris.git
synced 2025-02-02 15:30:36 +01:00
new x/errors package to handle HTTP wire errors
This commit is contained in:
parent
8ded69fd7e
commit
37c766fef7
|
@ -28,6 +28,8 @@ The codebase for Dependency Injection, Internationalization and localization and
|
||||||
|
|
||||||
## Fixes and Improvements
|
## Fixes and Improvements
|
||||||
|
|
||||||
|
- New [x/errors](x/errors) sub-package, helps with HTTP Wire Errors. Example can be found [here](_examples/routing/http-wire-errors/main.go).
|
||||||
|
|
||||||
- New [x/timex](x/timex) sub-package, helps working with weekdays.
|
- New [x/timex](x/timex) sub-package, helps working with weekdays.
|
||||||
|
|
||||||
- Minor improvements to the [JSON Kitchen Time](x/jsonx/kitchen_time.go).
|
- Minor improvements to the [JSON Kitchen Time](x/jsonx/kitchen_time.go).
|
||||||
|
|
|
@ -44,6 +44,7 @@
|
||||||
* [Overview](routing/overview/main.go)
|
* [Overview](routing/overview/main.go)
|
||||||
* [Basic](routing/basic/main.go)
|
* [Basic](routing/basic/main.go)
|
||||||
* [Custom HTTP Errors](routing/http-errors/main.go)
|
* [Custom HTTP Errors](routing/http-errors/main.go)
|
||||||
|
* [HTTP Wire Errors](routing/http-wire-errors/main.go) **NEW**
|
||||||
* [Not Found - Intelligence](routing/intelligence/main.go)
|
* [Not Found - Intelligence](routing/intelligence/main.go)
|
||||||
* [Not Found - Suggest Closest Paths](routing/intelligence/manual/main.go)
|
* [Not Found - Suggest Closest Paths](routing/intelligence/manual/main.go)
|
||||||
* [Dynamic Path](routing/dynamic-path/main.go)
|
* [Dynamic Path](routing/dynamic-path/main.go)
|
||||||
|
|
|
@ -28,6 +28,7 @@ func main() {
|
||||||
{
|
{
|
||||||
Name: "MyApp",
|
Name: "MyApp",
|
||||||
Addr: ":8080",
|
Addr: ":8080",
|
||||||
|
Hostname: "your-custom-sub-domain.ngrok.io", // optionally
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"github.com/kataras/iris/v12"
|
"github.com/kataras/iris/v12"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// See _examples/routing/http-wire-errors as well.
|
||||||
func main() {
|
func main() {
|
||||||
app := iris.New()
|
app := iris.New()
|
||||||
|
|
||||||
|
|
110
_examples/routing/http-wire-errors/main.go
Normal file
110
_examples/routing/http-wire-errors/main.go
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/kataras/iris/v12"
|
||||||
|
// IMPORTANT, import this sub-package.
|
||||||
|
// Note tht it does NOT break compatibility with the
|
||||||
|
// standard "errors" package as the New,
|
||||||
|
// Is, As, Unwrap functions are aliases to the standard package.
|
||||||
|
"github.com/kataras/iris/v12/x/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Optionally, register custom error codes.
|
||||||
|
//
|
||||||
|
// The default list of error code names:
|
||||||
|
// errors.Cancelled
|
||||||
|
// errors.Unknown
|
||||||
|
// errors.InvalidArgument
|
||||||
|
// errors.DeadlineExceeded
|
||||||
|
// errors.NotFound
|
||||||
|
// errors.AlreadyExists
|
||||||
|
// errors.PermissionDenied
|
||||||
|
// errors.Unauthenticated
|
||||||
|
// errors.ResourceExhausted
|
||||||
|
// errors.FailedPrecondition
|
||||||
|
// errors.Aborted
|
||||||
|
// errors.OutOfRange
|
||||||
|
// errors.Unimplemented
|
||||||
|
// errors.Internal
|
||||||
|
// errors.Unavailable
|
||||||
|
// errors.DataLoss
|
||||||
|
var (
|
||||||
|
Custom = errors.E("CUSTOM_CANONICAL_ERROR_NAME", iris.StatusBadRequest)
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := iris.New()
|
||||||
|
|
||||||
|
// Custom error code name.
|
||||||
|
app.Get("/custom", fireCustomErrorCodeName)
|
||||||
|
|
||||||
|
// Send a simple 400 request with message and an error
|
||||||
|
// or with more details and data.
|
||||||
|
app.Post("/invalid_argument", fireInvalidArgument)
|
||||||
|
|
||||||
|
// Compatibility with the iris.Problem type (and any other custom type).
|
||||||
|
app.Get("/problem", fireErrorWithProblem)
|
||||||
|
|
||||||
|
app.Listen(":8080")
|
||||||
|
}
|
||||||
|
|
||||||
|
func fireCustomErrorCodeName(ctx iris.Context) {
|
||||||
|
Custom.Details(ctx, "message", "details with arguments: %s", "an argument")
|
||||||
|
}
|
||||||
|
|
||||||
|
func fireInvalidArgument(ctx iris.Context) {
|
||||||
|
var req = struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
}{}
|
||||||
|
if err := ctx.ReadJSON(&req); err != nil {
|
||||||
|
errors.InvalidArgument.Err(ctx, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.WriteString(req.Username)
|
||||||
|
|
||||||
|
// Other examples: errors.InvalidArgument/NotFound/Internal and e.t.c.
|
||||||
|
// .Message(ctx, "message %s", "optional argument")
|
||||||
|
// .Details(ctx, "message", "details %s", "optional details argument")
|
||||||
|
// .Data(ctx, "message", anyTypeOfValue)
|
||||||
|
// .DataWithDetails(ctx, "unable to read the body", "malformed json", iris.Map{"custom": "data of any type"})
|
||||||
|
// .Log(ctx, "message %s", "optional argument")
|
||||||
|
// .LogErr(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fireErrorWithProblem(ctx iris.Context) {
|
||||||
|
myCondition := true
|
||||||
|
if myCondition {
|
||||||
|
problem := iris.NewProblem().
|
||||||
|
// The type URI, if relative it automatically convert to absolute.
|
||||||
|
Type("/product-error").
|
||||||
|
// The title, if empty then it gets it from the status code.
|
||||||
|
Title("Product validation problem").
|
||||||
|
// Any optional details.
|
||||||
|
Detail("details about the product error").
|
||||||
|
// The status error code of the problem, can be optional here.
|
||||||
|
// Status(iris.StatusBadRequest).
|
||||||
|
// Any custom key-value pair.
|
||||||
|
Key("product_name", "the product name")
|
||||||
|
|
||||||
|
errors.InvalidArgument.Data(ctx, "unable to process the request", problem)
|
||||||
|
return
|
||||||
|
|
||||||
|
/* Prints to the client:
|
||||||
|
{
|
||||||
|
"http_error_code": {
|
||||||
|
"canonical_name": "INVALID_ARGUMENT",
|
||||||
|
"status": 400
|
||||||
|
},
|
||||||
|
"message": "unable to process the request",
|
||||||
|
"data": {
|
||||||
|
"detail": "details about the product error",
|
||||||
|
"product_name": "the product name",
|
||||||
|
"title": "Product validation problem",
|
||||||
|
"type": "/product-error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -33,6 +33,9 @@ type Client struct {
|
||||||
|
|
||||||
// Optional handlers that are being fired before and after each new request.
|
// Optional handlers that are being fired before and after each new request.
|
||||||
requestHandlers []RequestHandler
|
requestHandlers []RequestHandler
|
||||||
|
|
||||||
|
// store it here for future use.
|
||||||
|
keepAlive bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns a new Iris HTTP Client.
|
// New returns a new Iris HTTP Client.
|
||||||
|
@ -57,6 +60,10 @@ func New(opts ...Option) *Client {
|
||||||
opt(c)
|
opt(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if transport, ok := c.HTTPClient.Transport.(*http.Transport); ok {
|
||||||
|
c.keepAlive = !transport.DisableKeepAlives
|
||||||
|
}
|
||||||
|
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -271,6 +278,13 @@ func (c *Client) Do(ctx context.Context, method, urlpath string, payload interfa
|
||||||
return resp, respErr
|
return resp, respErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DrainResponseBody drains response body and close it, allowing the transport to reuse TCP connections.
|
||||||
|
// It's automatically called on Client.ReadXXX methods on the end.
|
||||||
|
func (c *Client) DrainResponseBody(resp *http.Response) {
|
||||||
|
_, _ = io.Copy(ioutil.Discard, resp.Body)
|
||||||
|
resp.Body.Close()
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
acceptKey = "Accept"
|
acceptKey = "Accept"
|
||||||
contentTypeKey = "Content-Type"
|
contentTypeKey = "Content-Type"
|
||||||
|
@ -377,7 +391,7 @@ func (c *Client) ReadJSON(ctx context.Context, dest interface{}, method, urlpath
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer c.DrainResponseBody(resp)
|
||||||
|
|
||||||
if resp.StatusCode >= http.StatusBadRequest {
|
if resp.StatusCode >= http.StatusBadRequest {
|
||||||
return ExtractError(resp)
|
return ExtractError(resp)
|
||||||
|
@ -398,7 +412,7 @@ func (c *Client) ReadPlain(ctx context.Context, dest interface{}, method, urlpat
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer c.DrainResponseBody(resp)
|
||||||
|
|
||||||
if resp.StatusCode >= http.StatusBadRequest {
|
if resp.StatusCode >= http.StatusBadRequest {
|
||||||
return ExtractError(resp)
|
return ExtractError(resp)
|
||||||
|
|
25
x/errors/aliases.go
Normal file
25
x/errors/aliases.go
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
package errors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Is is an alias of the standard errors.Is function.
|
||||||
|
Is = errors.Is
|
||||||
|
// As is an alias of the standard errors.As function.
|
||||||
|
As = errors.As
|
||||||
|
// New is an alias of the standard errors.New function.
|
||||||
|
New = errors.New
|
||||||
|
// Unwrap is an alias of the standard errors.Unwrap function.
|
||||||
|
Unwrap = errors.Unwrap
|
||||||
|
)
|
||||||
|
|
||||||
|
func sprintf(format string, args ...interface{}) string {
|
||||||
|
if len(args) > 0 {
|
||||||
|
return fmt.Sprintf(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return format
|
||||||
|
}
|
309
x/errors/errors.go
Normal file
309
x/errors/errors.go
Normal file
|
@ -0,0 +1,309 @@
|
||||||
|
package errors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/v12"
|
||||||
|
"github.com/kataras/iris/v12/x/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LogErrorFunc is an alias of a function type which accepts the Iris request context and an error
|
||||||
|
// and it's fired whenever an error should be logged.
|
||||||
|
//
|
||||||
|
// See "OnErrorLog" variable to change the way an error is logged,
|
||||||
|
// by default the error is logged using the Application's Logger's Error method.
|
||||||
|
type LogErrorFunc = func(ctx iris.Context, err error)
|
||||||
|
|
||||||
|
// LogError can be modified to customize the way an error is logged to the server (most common: internal server errors, database errors et.c.).
|
||||||
|
// Can be used to customize the error logging, e.g. using Sentry (cloud-based error console).
|
||||||
|
var LogError LogErrorFunc = func(ctx iris.Context, err error) {
|
||||||
|
ctx.Application().Logger().Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SkipCanceled is a package-level setting which by default
|
||||||
|
// skips the logging of a canceled response or operation.
|
||||||
|
// See the "Context.IsCanceled()" method and "iris.IsCanceled()" function
|
||||||
|
// that decide if the error is caused by a canceled operation.
|
||||||
|
//
|
||||||
|
// Change of this setting MUST be done on initialization of the program.
|
||||||
|
var SkipCanceled = true
|
||||||
|
|
||||||
|
type (
|
||||||
|
// ErrorCodeName is a custom string type represents canonical error names.
|
||||||
|
//
|
||||||
|
// It contains functionality for safe and easy error populating.
|
||||||
|
// See its "Message", "Details", "Data" and "Log" methods.
|
||||||
|
ErrorCodeName string
|
||||||
|
|
||||||
|
// ErrorCode represents the JSON form ErrorCode of the Error.
|
||||||
|
ErrorCode struct {
|
||||||
|
CanonicalName ErrorCodeName `json:"canonical_name" yaml:"CanonicalName"`
|
||||||
|
Status int `json:"status" yaml:"Status"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// A read-only map of valid http error codes.
|
||||||
|
var errorCodeMap = make(map[ErrorCodeName]ErrorCode)
|
||||||
|
|
||||||
|
// E registers a custom HTTP Error and returns its canonical name for future use.
|
||||||
|
// The method "New" is reserved and was kept as it is for compatibility
|
||||||
|
// with the standard errors package, therefore the "E" name was chosen instead.
|
||||||
|
// The key stroke "e" is near and accessible while typing the "errors" word
|
||||||
|
// so developers may find it easy to use.
|
||||||
|
//
|
||||||
|
// See "RegisterErrorCode" and "RegisterErrorCodeMap" for alternatives.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
// var (
|
||||||
|
// NotFound = errors.E("NOT_FOUND", iris.StatusNotFound)
|
||||||
|
// )
|
||||||
|
// ...
|
||||||
|
// NotFound.Details(ctx, "resource not found", "user with id: %q was not found", userID)
|
||||||
|
//
|
||||||
|
// This method MUST be called on initialization, before HTTP server starts as
|
||||||
|
// the internal map is not protected by mutex.
|
||||||
|
func E(httpErrorCanonicalName string, httpStatusCode int) ErrorCodeName {
|
||||||
|
canonicalName := ErrorCodeName(httpErrorCanonicalName)
|
||||||
|
RegisterErrorCode(canonicalName, httpStatusCode)
|
||||||
|
return canonicalName
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterErrorCode registers a custom HTTP Error.
|
||||||
|
//
|
||||||
|
// This method MUST be called on initialization, before HTTP server starts as
|
||||||
|
// the internal map is not protected by mutex.
|
||||||
|
func RegisterErrorCode(canonicalName ErrorCodeName, httpStatusCode int) {
|
||||||
|
errorCodeMap[canonicalName] = ErrorCode{
|
||||||
|
CanonicalName: canonicalName,
|
||||||
|
Status: httpStatusCode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterErrorCodeMap registers one or more custom HTTP Errors.
|
||||||
|
//
|
||||||
|
// This method MUST be called on initialization, before HTTP server starts as
|
||||||
|
// the internal map is not protected by mutex.
|
||||||
|
func RegisterErrorCodeMap(errorMap map[ErrorCodeName]int) {
|
||||||
|
if len(errorMap) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for canonicalName, httpStatusCode := range errorMap {
|
||||||
|
RegisterErrorCode(canonicalName, httpStatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// List of default error codes a server should follow and send back to the client.
|
||||||
|
var (
|
||||||
|
Cancelled ErrorCodeName = E("CANCELLED", iris.StatusTokenRequired)
|
||||||
|
Unknown ErrorCodeName = E("UNKNOWN", iris.StatusInternalServerError)
|
||||||
|
InvalidArgument ErrorCodeName = E("INVALID_ARGUMENT", iris.StatusBadRequest)
|
||||||
|
DeadlineExceeded ErrorCodeName = E("DEADLINE_EXCEEDED", iris.StatusGatewayTimeout)
|
||||||
|
NotFound ErrorCodeName = E("NOT_FOUND", iris.StatusNotFound)
|
||||||
|
AlreadyExists ErrorCodeName = E("ALREADY_EXISTS", iris.StatusConflict)
|
||||||
|
PermissionDenied ErrorCodeName = E("PERMISSION_DENIED", iris.StatusForbidden)
|
||||||
|
Unauthenticated ErrorCodeName = E("UNAUTHENTICATED", iris.StatusUnauthorized)
|
||||||
|
ResourceExhausted ErrorCodeName = E("RESOURCE_EXHAUSTED", iris.StatusTooManyRequests)
|
||||||
|
FailedPrecondition ErrorCodeName = E("FAILED_PRECONDITION", iris.StatusBadRequest)
|
||||||
|
Aborted ErrorCodeName = E("ABORTED", iris.StatusConflict)
|
||||||
|
OutOfRange ErrorCodeName = E("OUT_OF_RANGE", iris.StatusBadRequest)
|
||||||
|
Unimplemented ErrorCodeName = E("UNIMPLEMENTED", iris.StatusNotImplemented)
|
||||||
|
Internal ErrorCodeName = E("INTERNAL", iris.StatusInternalServerError)
|
||||||
|
Unavailable ErrorCodeName = E("UNAVAILABLE", iris.StatusServiceUnavailable)
|
||||||
|
DataLoss ErrorCodeName = E("DATA_LOSS", iris.StatusInternalServerError)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Message sends an error with a simple message to the client.
|
||||||
|
func (e ErrorCodeName) Message(ctx iris.Context, format string, args ...interface{}) {
|
||||||
|
fail(ctx, e, sprintf(format, args...), "", nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Details sends an error with a message and details to the client.
|
||||||
|
func (e ErrorCodeName) Details(ctx iris.Context, msg, details string, detailsArgs ...interface{}) {
|
||||||
|
fail(ctx, e, msg, sprintf(details, detailsArgs...), nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data sends an error with a message and json data to the client.
|
||||||
|
func (e ErrorCodeName) Data(ctx iris.Context, msg string, data interface{}) {
|
||||||
|
fail(ctx, e, msg, "", nil, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DataWithDetails sends an error with a message, details and json data to the client.
|
||||||
|
func (e ErrorCodeName) DataWithDetails(ctx iris.Context, msg, details string, data interface{}) {
|
||||||
|
fail(ctx, e, msg, details, nil, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation sends an error which contains the invalid fields to the client.
|
||||||
|
func (e ErrorCodeName) Validation(ctx iris.Context, errs ...ValidationError) {
|
||||||
|
fail(ctx, e, "validation failure", "fields were invalid", errs, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Err sends the error's text as a message to the client.
|
||||||
|
// In exception, if the given "err" is a type of validation error
|
||||||
|
// then the Validation method is called instead.
|
||||||
|
func (e ErrorCodeName) Err(ctx iris.Context, err error) {
|
||||||
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if validationErrors, ok := AsValidationErrors(err); ok {
|
||||||
|
e.Validation(ctx, validationErrors...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.Message(ctx, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log sends an error of "format" and optional "args" to the client and prints that
|
||||||
|
// error using the "LogError" package-level function, which can be customized.
|
||||||
|
//
|
||||||
|
// See "LogErr" too.
|
||||||
|
func (e ErrorCodeName) Log(ctx iris.Context, format string, args ...interface{}) {
|
||||||
|
if SkipCanceled {
|
||||||
|
if ctx.IsCanceled() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, arg := range args {
|
||||||
|
if err, ok := arg.(error); ok {
|
||||||
|
if iris.IsErrCanceled(err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := fmt.Errorf(format, args...)
|
||||||
|
e.LogErr(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogErr sends the given "err" as message to the client and prints that
|
||||||
|
// error to using the "LogError" package-level function, which can be customized.
|
||||||
|
func (e ErrorCodeName) LogErr(ctx iris.Context, err error) {
|
||||||
|
if SkipCanceled && (ctx.IsCanceled() || iris.IsErrCanceled(err)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
LogError(ctx, err)
|
||||||
|
|
||||||
|
e.Message(ctx, "server error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleAPIError handles remote server errors.
|
||||||
|
// Optionally, use it when you write your server's HTTP clients using the the /x/client package.
|
||||||
|
// When the HTTP Client sends data to a remote server but that remote server
|
||||||
|
// failed to accept the request as expected, then the error will be proxied
|
||||||
|
// to this server's end-client.
|
||||||
|
//
|
||||||
|
// When the given "err" is not a type of client.APIError then
|
||||||
|
// the error will be sent using the "Internal.LogErr" method which sends
|
||||||
|
// HTTP internal server error to the end-client and
|
||||||
|
// prints the "err" using the "LogError" package-level function.
|
||||||
|
func HandleAPIError(ctx iris.Context, err error) {
|
||||||
|
// Error expected and came from the external server,
|
||||||
|
// save its body so we can forward it to the end-client.
|
||||||
|
if apiErr, ok := client.GetError(err); ok {
|
||||||
|
statusCode := apiErr.Response.StatusCode
|
||||||
|
if statusCode >= 400 && statusCode < 500 {
|
||||||
|
InvalidArgument.DataWithDetails(ctx, "remote server error", "invalid client request", apiErr.Body)
|
||||||
|
} else {
|
||||||
|
Internal.Data(ctx, "remote server error", apiErr.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unavailable.DataWithDetails(ctx, "remote server error", "unavailable", apiErr.Body)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Internal.LogErr(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrUnexpected is the HTTP error which sent to the client
|
||||||
|
// when server fails to send an error, it's a fallback error.
|
||||||
|
// The server fails to send an error on two cases:
|
||||||
|
// 1. when the provided error code name is not registered (the error value is the ErrUnexpectedErrorCode)
|
||||||
|
// 2. when the error contains data but cannot be encoded to json (the value of the error is the result error of json.Marshal).
|
||||||
|
ErrUnexpected = E("UNEXPECTED_ERROR", iris.StatusInternalServerError)
|
||||||
|
// ErrUnexpectedErrorCode is the error which logged
|
||||||
|
// when the given error code name is not registered.
|
||||||
|
ErrUnexpectedErrorCode = New("unexpected error code name")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Error represents the JSON form of "http wire errors".
|
||||||
|
type Error struct {
|
||||||
|
ErrorCode ErrorCode `json:"http_error_code" yaml:"HTTPErrorCode"`
|
||||||
|
Message string `json:"message,omitempty" yaml:"Message"`
|
||||||
|
Details string `json:"details,omitempty" yaml:"Details"`
|
||||||
|
ValidationErrors ValidationErrors `json:"validation,omitempty" yaml:"Validation,omitempty"`
|
||||||
|
Data json.RawMessage `json:"data,omitempty" yaml:"Data,omitempty"` // any other custom json data.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error method completes the error interface. It just returns the canonical name, status code, message and details.
|
||||||
|
func (err Error) Error() string {
|
||||||
|
if err.Message == "" {
|
||||||
|
err.Message = "<empty>"
|
||||||
|
}
|
||||||
|
|
||||||
|
if err.Details == "" {
|
||||||
|
err.Details = "<empty>"
|
||||||
|
}
|
||||||
|
|
||||||
|
if err.ErrorCode.CanonicalName == "" {
|
||||||
|
err.ErrorCode.CanonicalName = ErrUnexpected
|
||||||
|
}
|
||||||
|
|
||||||
|
if err.ErrorCode.Status <= 0 {
|
||||||
|
err.ErrorCode.Status = iris.StatusInternalServerError
|
||||||
|
}
|
||||||
|
|
||||||
|
return sprintf("iris http wire error: canonical name: %s, http status code: %d, message: %s, details: %s", err.ErrorCode.CanonicalName, err.ErrorCode.Status, err.Message, err.Details)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fail(ctx iris.Context, codeName ErrorCodeName, msg, details string, validationErrors ValidationErrors, dataValue interface{}) {
|
||||||
|
errorCode, ok := errorCodeMap[codeName]
|
||||||
|
if !ok {
|
||||||
|
// This SHOULD NEVER happen, all ErrorCodeNames MUST be registered.
|
||||||
|
LogError(ctx, ErrUnexpectedErrorCode)
|
||||||
|
fail(ctx, ErrUnexpected, msg, details, validationErrors, dataValue)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var data json.RawMessage
|
||||||
|
if dataValue != nil {
|
||||||
|
switch v := dataValue.(type) {
|
||||||
|
case json.RawMessage:
|
||||||
|
data = v
|
||||||
|
case []byte:
|
||||||
|
data = v
|
||||||
|
case error:
|
||||||
|
if msg == "" {
|
||||||
|
msg = v.Error()
|
||||||
|
} else if details == "" {
|
||||||
|
details = v.Error()
|
||||||
|
} else {
|
||||||
|
data = json.RawMessage(v.Error())
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
b, err := json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
LogError(ctx, err)
|
||||||
|
fail(ctx, ErrUnexpected, err.Error(), "", nil, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data = b
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := Error{
|
||||||
|
ErrorCode: errorCode,
|
||||||
|
Message: msg,
|
||||||
|
Details: details,
|
||||||
|
Data: data,
|
||||||
|
ValidationErrors: validationErrors,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ctx.SetErr(err)
|
||||||
|
ctx.StopWithJSON(errorCode.Status, err)
|
||||||
|
}
|
181
x/errors/validation_error.go
Normal file
181
x/errors/validation_error.go
Normal file
|
@ -0,0 +1,181 @@
|
||||||
|
package errors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AsValidationErrors reports wheether the given "err" is a type of validation error(s).
|
||||||
|
func AsValidationErrors(err error) (ValidationErrors, bool) {
|
||||||
|
if err == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch e := err.(type) {
|
||||||
|
case ValidationError:
|
||||||
|
return ValidationErrors{e}, true
|
||||||
|
case ValidationErrors:
|
||||||
|
return e, true
|
||||||
|
case *ValidationErrors:
|
||||||
|
return *e, true
|
||||||
|
default:
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValueValidator is a generic interface which can be used to check if the value is valid for insert (or for comparison inside another validation step).
|
||||||
|
// Useful for enums.
|
||||||
|
// Should return a non-empty string on validation error, that string is the failure reason.
|
||||||
|
type ValueValidator interface {
|
||||||
|
Validate() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidationError describes a field validation error.
|
||||||
|
type ValidationError struct {
|
||||||
|
Field string `json:"field" yaml:"Field"`
|
||||||
|
Value interface{} `json:"value" yaml:"Value"`
|
||||||
|
Reason string `json:"reason" yaml:"Reason"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error completes the standard error interface.
|
||||||
|
func (e ValidationError) Error() string {
|
||||||
|
return sprintf("field %q got invalid value of %v: reason: %s", e.Field, e.Value, e.Reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidationErrors is just a custom type of ValidationError slice.
|
||||||
|
type ValidationErrors []ValidationError
|
||||||
|
|
||||||
|
// Error completes the error interface.
|
||||||
|
func (e ValidationErrors) Error() string {
|
||||||
|
var buf strings.Builder
|
||||||
|
for i, err := range e {
|
||||||
|
buf.WriteByte('[')
|
||||||
|
buf.WriteString(strconv.Itoa(i))
|
||||||
|
buf.WriteByte(']')
|
||||||
|
buf.WriteByte(' ')
|
||||||
|
|
||||||
|
buf.WriteString(err.Error())
|
||||||
|
|
||||||
|
if i < len(e)-1 {
|
||||||
|
buf.WriteByte(',')
|
||||||
|
buf.WriteByte(' ')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is reports whether the given "err" is a type of validation error or validation errors.
|
||||||
|
func (e ValidationErrors) Is(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch err.(type) {
|
||||||
|
case ValidationError:
|
||||||
|
return true
|
||||||
|
case *ValidationError:
|
||||||
|
return true
|
||||||
|
case ValidationErrors:
|
||||||
|
return true
|
||||||
|
case *ValidationErrors:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add is a helper for appending a validation error.
|
||||||
|
func (e *ValidationErrors) Add(err ValidationError) *ValidationErrors {
|
||||||
|
if err.Field == "" || err.Reason == "" {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
*e = append(*e, err)
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
// Join joins an existing Errors to this errors list.
|
||||||
|
func (e *ValidationErrors) Join(errs ValidationErrors) *ValidationErrors {
|
||||||
|
*e = append(*e, errs...)
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate returns the result of the value's Validate method, if exists otherwise
|
||||||
|
// it adds the field and value to the error list and reports false (invalidated).
|
||||||
|
// If reason is empty, means that the field is valid, this method will return true.
|
||||||
|
func (e *ValidationErrors) Validate(field string, value interface{}) bool {
|
||||||
|
var reason string
|
||||||
|
|
||||||
|
if v, ok := value.(ValueValidator); ok {
|
||||||
|
reason = v.Validate()
|
||||||
|
}
|
||||||
|
|
||||||
|
if reason != "" {
|
||||||
|
e.Add(ValidationError{
|
||||||
|
Field: field,
|
||||||
|
Value: value,
|
||||||
|
Reason: reason,
|
||||||
|
})
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustBeSatisfiedFunc compares the value with the given "isEqualFunc" function and reports
|
||||||
|
// if it's valid or not. If it's not valid, a new ValidationError is added to the "e" list.
|
||||||
|
func (e *ValidationErrors) MustBeSatisfiedFunc(field string, value string, isEqualFunc func(string) bool) bool {
|
||||||
|
if !isEqualFunc(value) {
|
||||||
|
e.Add(ValidationError{
|
||||||
|
Field: field,
|
||||||
|
Value: value,
|
||||||
|
Reason: "failed to satisfy constraint",
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustBeSatisfied compares the value with the given regex and reports
|
||||||
|
// if it's valid or not. If it's not valid, a new ValidationError is added to the "e" list.
|
||||||
|
func (e *ValidationErrors) MustBeSatisfied(field string, value string, regex *regexp.Regexp) bool {
|
||||||
|
return e.MustBeSatisfiedFunc(field, value, regex.MatchString)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustBeNotEmptyString reports and fails if the given "value" is empty.
|
||||||
|
func (e *ValidationErrors) MustBeNotEmptyString(field string, value string) bool {
|
||||||
|
if strings.TrimSpace(value) == "" {
|
||||||
|
e.Add(ValidationError{
|
||||||
|
Field: field,
|
||||||
|
Value: value,
|
||||||
|
Reason: "must be not an empty string",
|
||||||
|
})
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustBeInRangeString reports whether the "value" is in range of min and max.
|
||||||
|
func (e *ValidationErrors) MustBeInRangeString(field string, value string, minIncluding, maxIncluding int) bool {
|
||||||
|
if maxIncluding <= 0 {
|
||||||
|
maxIncluding = math.MaxInt32
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(value) < minIncluding || len(value) > maxIncluding {
|
||||||
|
e.Add(ValidationError{
|
||||||
|
Field: field,
|
||||||
|
Value: value,
|
||||||
|
Reason: sprintf("characters length must be between %d and %d", minIncluding, maxIncluding),
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user