From ae828d8db9112598f9568108124c0a86f2f91a66 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Wed, 13 Apr 2022 01:00:53 +0300 Subject: [PATCH] new Application.SetContextErrorHandler method --- .fossa.yml | 7 +- HISTORY.md | 5 +- configuration.go | 117 ++++++++---- context/application.go | 4 + context/configuration.go | 5 + context/context.go | 395 +++++++++++++++++++-------------------- iris.go | 24 +++ 7 files changed, 314 insertions(+), 243 deletions(-) diff --git a/.fossa.yml b/.fossa.yml index cb3f48ee..f87fafda 100644 --- a/.fossa.yml +++ b/.fossa.yml @@ -1,8 +1,9 @@ -version: 2 +version: 3 cli: server: https://app.fossa.com - fetcher: custom - project: https://github.com/kataras/iris.git + fetcher: git + package: github.com/kataras/iris + project: github.com/kataras/iris analyze: modules: - name: iris diff --git a/HISTORY.md b/HISTORY.md index 64c6df8f..d3afa52c 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -28,11 +28,12 @@ The codebase for Dependency Injection, Internationalization and localization and ## Fixes and Improvements +- Add new `Application.SetContextErrorHandler` to globally customize the default behavior (status code 500 without body) on `JSON`, `JSONP`, `Protobuf`, `MsgPack`, `XML`, `YAML` and `Markdown` method call write errors instead of catching the error on each handler. - Add new [x/pagination](x/pagination/pagination.go) sub-package which supports generics code (go 1.18+). - Add new [middleware/modrevision](middleware/modrevision) middleware (example at [_examples/project/api/router.go]_examples/project/api/router.go). - Add `iris.BuildRevision` and `iris.BuildTime` to embrace the new go's 1.18 debug build information. -- Add `Context.SetJSONOptions` to customize on a higher level the JSON options on `Context.JSON` calls. +- ~Add `Context.SetJSONOptions` to customize on a higher level the JSON options on `Context.JSON` calls.~ update: remains as it's, per JSON call. - Add new [auth](auth) sub-package which helps on any user type auth using JWT (access & refresh tokens) and a cookie (optional). - Add `Party.EnsureStaticBindings` which, if called, the MVC binder panics if a struct's input binding depends on the HTTP request data instead of a static dependency. This is useful to make sure your API crafted through `Party.PartyConfigure` depends only on struct values you already defined at `Party.RegisterDependency` == will never use reflection at serve-time (maximum performance). @@ -71,7 +72,7 @@ The codebase for Dependency Injection, Internationalization and localization and - New `apps.OnApplicationRegistered` method which listens on new Iris applications hosted under the same binary. Use it on your `init` functions to configure Iris applications by any spot in your project's files. -- `Context.JSON` respects any object implements the `easyjson.Marshaler` interface and renders the result using the [easyjon](https://github.com/mailru/easyjson)'s writer. +- `Context.JSON` respects any object implements the `easyjson.Marshaler` interface and renders the result using the [easyjon](https://github.com/mailru/easyjson)'s writer. **Set** the `Configuration.EnableProtoJSON` and `Configuration.EnableEasyJSON` to true in order to enable this feature. - minor: `Context` structure implements the standard go Context interface now (includes: Deadline, Done, Err and Value methods). Handlers can now just pass the `ctx iris.Context` as a shortcut of `ctx.Request().Context()` when needed. diff --git a/configuration.go b/configuration.go index 6d5db5d2..73905652 100644 --- a/configuration.go +++ b/configuration.go @@ -10,11 +10,11 @@ import ( "strings" "time" - "github.com/kataras/golog" "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/core/netutil" "github.com/BurntSushi/toml" + "github.com/kataras/golog" "github.com/kataras/sitemap" "github.com/kataras/tunnel" "gopkg.in/yaml.v3" @@ -317,6 +317,20 @@ var WithOptimizations = func(app *Application) { app.config.EnableOptimizations = true } +// WithProtoJSON enables the proto marshaler on Context.JSON method. +// +// See `Configuration` for more. +var WithProtoJSON = func(app *Application) { + app.config.EnableProtoJSON = true +} + +// WithEasyJSON enables the fast easy json marshaler on Context.JSON method. +// +// See `Configuration` for more. +var WithEasyJSON = func(app *Application) { + app.config.EnableEasyJSON = true +} + // WithFireMethodNotAllowed enables the FireMethodNotAllowed setting. // // See `Configuration`. @@ -740,6 +754,17 @@ type Configuration struct { // // Defaults to false. EnableOptimizations bool `ini:"enable_optimizations" json:"enableOptimizations,omitempty" yaml:"EnableOptimizations" toml:"EnableOptimizations"` + // EnableProtoJSON when this field is true + // enables the proto marshaler on given proto messages when calling the Context.JSON method. + // + // Defaults to false. + EnableProtoJSON bool `ini:"enable_proto_json" json:"enableProtoJSON,omitempty" yaml:"EnableProtoJSON" toml:"EnableProtoJSON"` + // EnableEasyJSON when this field is true + // enables the fast easy json marshaler on compatible struct values when calling the Context.JSON method. + // + // Defaults to false. + EnableEasyJSON bool `ini:"enable_easy_json" json:"enableEasyJSON,omitempty" yaml:"EnableEasyJSON" toml:"EnableEasyJSON"` + // DisableBodyConsumptionOnUnmarshal manages the reading behavior of the context's body readers/binders. // If set to true then it // disables the body consumption by the `context.UnmarshalBody/ReadJSON/ReadXML`. @@ -915,177 +940,187 @@ type Configuration struct { var _ context.ConfigurationReadOnly = (*Configuration)(nil) // GetVHost returns the non-exported vhost config field. -func (c Configuration) GetVHost() string { +func (c *Configuration) GetVHost() string { return c.vhost } // GetLogLevel returns the LogLevel field. -func (c Configuration) GetLogLevel() string { +func (c *Configuration) GetLogLevel() string { return c.vhost } // GetSocketSharding returns the SocketSharding field. -func (c Configuration) GetSocketSharding() bool { +func (c *Configuration) GetSocketSharding() bool { return c.SocketSharding } // GetKeepAlive returns the KeepAlive field. -func (c Configuration) GetKeepAlive() time.Duration { +func (c *Configuration) GetKeepAlive() time.Duration { return c.KeepAlive } // GetKeepAlive returns the Timeout field. -func (c Configuration) GetTimeout() time.Duration { +func (c *Configuration) GetTimeout() time.Duration { return c.Timeout } // GetKeepAlive returns the TimeoutMessage field. -func (c Configuration) GetTimeoutMessage() string { +func (c *Configuration) GetTimeoutMessage() string { return c.TimeoutMessage } // GetDisablePathCorrection returns the DisablePathCorrection field. -func (c Configuration) GetDisablePathCorrection() bool { +func (c *Configuration) GetDisablePathCorrection() bool { return c.DisablePathCorrection } // GetDisablePathCorrectionRedirection returns the DisablePathCorrectionRedirection field. -func (c Configuration) GetDisablePathCorrectionRedirection() bool { +func (c *Configuration) GetDisablePathCorrectionRedirection() bool { return c.DisablePathCorrectionRedirection } // GetEnablePathIntelligence returns the EnablePathIntelligence field. -func (c Configuration) GetEnablePathIntelligence() bool { +func (c *Configuration) GetEnablePathIntelligence() bool { return c.EnablePathIntelligence } // GetEnablePathEscape returns the EnablePathEscape field. -func (c Configuration) GetEnablePathEscape() bool { +func (c *Configuration) GetEnablePathEscape() bool { return c.EnablePathEscape } // GetForceLowercaseRouting returns the ForceLowercaseRouting field. -func (c Configuration) GetForceLowercaseRouting() bool { +func (c *Configuration) GetForceLowercaseRouting() bool { return c.ForceLowercaseRouting } // GetFireMethodNotAllowed returns the FireMethodNotAllowed field. -func (c Configuration) GetFireMethodNotAllowed() bool { +func (c *Configuration) GetFireMethodNotAllowed() bool { return c.FireMethodNotAllowed } // GetEnableOptimizations returns the EnableOptimizations. -func (c Configuration) GetEnableOptimizations() bool { +func (c *Configuration) GetEnableOptimizations() bool { return c.EnableOptimizations } +// GetEnableProtoJSON returns the EnableProtoJSON field. +func (c *Configuration) GetEnableProtoJSON() bool { + return c.EnableProtoJSON +} + +// GetEnableEasyJSON returns the EnableEasyJSON field. +func (c *Configuration) GetEnableEasyJSON() bool { + return c.EnableEasyJSON +} + // GetDisableBodyConsumptionOnUnmarshal returns the DisableBodyConsumptionOnUnmarshal field. -func (c Configuration) GetDisableBodyConsumptionOnUnmarshal() bool { +func (c *Configuration) GetDisableBodyConsumptionOnUnmarshal() bool { return c.DisableBodyConsumptionOnUnmarshal } // GetFireEmptyFormError returns the DisableBodyConsumptionOnUnmarshal field. -func (c Configuration) GetFireEmptyFormError() bool { +func (c *Configuration) GetFireEmptyFormError() bool { return c.FireEmptyFormError } // GetDisableAutoFireStatusCode returns the DisableAutoFireStatusCode field. -func (c Configuration) GetDisableAutoFireStatusCode() bool { +func (c *Configuration) GetDisableAutoFireStatusCode() bool { return c.DisableAutoFireStatusCode } // GetResetOnFireErrorCode returns ResetOnFireErrorCode field. -func (c Configuration) GetResetOnFireErrorCode() bool { +func (c *Configuration) GetResetOnFireErrorCode() bool { return c.ResetOnFireErrorCode } // GetTimeFormat returns the TimeFormat field. -func (c Configuration) GetTimeFormat() string { +func (c *Configuration) GetTimeFormat() string { return c.TimeFormat } // GetCharset returns the Charset field. -func (c Configuration) GetCharset() string { +func (c *Configuration) GetCharset() string { return c.Charset } // GetPostMaxMemory returns the PostMaxMemory field. -func (c Configuration) GetPostMaxMemory() int64 { +func (c *Configuration) GetPostMaxMemory() int64 { return c.PostMaxMemory } // GetLocaleContextKey returns the LocaleContextKey field. -func (c Configuration) GetLocaleContextKey() string { +func (c *Configuration) GetLocaleContextKey() string { return c.LocaleContextKey } // GetLanguageContextKey returns the LanguageContextKey field. -func (c Configuration) GetLanguageContextKey() string { +func (c *Configuration) GetLanguageContextKey() string { return c.LanguageContextKey } // GetLanguageInputContextKey returns the LanguageInputContextKey field. -func (c Configuration) GetLanguageInputContextKey() string { +func (c *Configuration) GetLanguageInputContextKey() string { return c.LanguageInputContextKey } // GetVersionContextKey returns the VersionContextKey field. -func (c Configuration) GetVersionContextKey() string { +func (c *Configuration) GetVersionContextKey() string { return c.VersionContextKey } // GetVersionAliasesContextKey returns the VersionAliasesContextKey field. -func (c Configuration) GetVersionAliasesContextKey() string { +func (c *Configuration) GetVersionAliasesContextKey() string { return c.VersionAliasesContextKey } // GetViewEngineContextKey returns the ViewEngineContextKey field. -func (c Configuration) GetViewEngineContextKey() string { +func (c *Configuration) GetViewEngineContextKey() string { return c.ViewEngineContextKey } // GetViewLayoutContextKey returns the ViewLayoutContextKey field. -func (c Configuration) GetViewLayoutContextKey() string { +func (c *Configuration) GetViewLayoutContextKey() string { return c.ViewLayoutContextKey } // GetViewDataContextKey returns the ViewDataContextKey field. -func (c Configuration) GetViewDataContextKey() string { +func (c *Configuration) GetViewDataContextKey() string { return c.ViewDataContextKey } // GetFallbackViewContextKey returns the FallbackViewContextKey field. -func (c Configuration) GetFallbackViewContextKey() string { +func (c *Configuration) GetFallbackViewContextKey() string { return c.FallbackViewContextKey } // GetRemoteAddrHeaders returns the RemoteAddrHeaders field. -func (c Configuration) GetRemoteAddrHeaders() []string { +func (c *Configuration) GetRemoteAddrHeaders() []string { return c.RemoteAddrHeaders } // GetRemoteAddrHeadersForce returns RemoteAddrHeadersForce field. -func (c Configuration) GetRemoteAddrHeadersForce() bool { +func (c *Configuration) GetRemoteAddrHeadersForce() bool { return c.RemoteAddrHeadersForce } // GetSSLProxyHeaders returns the SSLProxyHeaders field. -func (c Configuration) GetSSLProxyHeaders() map[string]string { +func (c *Configuration) GetSSLProxyHeaders() map[string]string { return c.SSLProxyHeaders } // GetRemoteAddrPrivateSubnets returns the RemoteAddrPrivateSubnets field. -func (c Configuration) GetRemoteAddrPrivateSubnets() []netutil.IPRange { +func (c *Configuration) GetRemoteAddrPrivateSubnets() []netutil.IPRange { return c.RemoteAddrPrivateSubnets } // GetHostProxyHeaders returns the HostProxyHeaders field. -func (c Configuration) GetHostProxyHeaders() map[string]bool { +func (c *Configuration) GetHostProxyHeaders() map[string]bool { return c.HostProxyHeaders } // GetOther returns the Other field. -func (c Configuration) GetOther() map[string]interface{} { +func (c *Configuration) GetOther() map[string]interface{} { return c.Other } @@ -1166,6 +1201,14 @@ func WithConfiguration(c Configuration) Configurator { main.EnableOptimizations = v } + if v := c.EnableProtoJSON; v { + main.EnableProtoJSON = v + } + + if v := c.EnableEasyJSON; v { + main.EnableEasyJSON = v + } + if v := c.FireMethodNotAllowed; v { main.FireMethodNotAllowed = v } @@ -1342,6 +1385,8 @@ func DefaultConfiguration() Configuration { SSLProxyHeaders: make(map[string]string), HostProxyHeaders: make(map[string]bool), EnableOptimizations: false, + EnableProtoJSON: false, + EnableEasyJSON: false, Other: make(map[string]interface{}), } } diff --git a/context/application.go b/context/application.go index b376897e..15ce5abb 100644 --- a/context/application.go +++ b/context/application.go @@ -52,6 +52,10 @@ type Application interface { // is hijacked by a third-party middleware and the http handler return too fast. GetContextPool() *Pool + // GetContextErrorHandler returns the handler which handles errors + // on JSON write failures. + GetContextErrorHandler() ErrorHandler + // ServeHTTPC is the internal router, it's visible because it can be used for advanced use cases, // i.e: routing within a foreign context. // diff --git a/context/configuration.go b/context/configuration.go index 67df7e55..7d89f7f1 100644 --- a/context/configuration.go +++ b/context/configuration.go @@ -43,6 +43,11 @@ type ConfigurationReadOnly interface { // GetEnableOptimizations returns the EnableOptimizations field. GetEnableOptimizations() bool + // GetEnableProtoJSON returns the EnableProtoJSON field. + GetEnableProtoJSON() bool + // GetEnableEasyJSON returns the EnableEasyJSON field. + GetEnableEasyJSON() bool + // GetDisableBodyConsumptionOnUnmarshal returns the DisableBodyConsumptionOnUnmarshal field. GetDisableBodyConsumptionOnUnmarshal() bool // GetFireEmptyFormError returns the FireEmptyFormError field. diff --git a/context/context.go b/context/context.go index 257d0bbe..cd497fab 100644 --- a/context/context.go +++ b/context/context.go @@ -3046,6 +3046,9 @@ func (ctx *Context) ReadBody(ptr interface{}) error { // writing the response. However, such behavior may not be supported // by all HTTP/2 clients. Handlers should read before writing if // possible to maximize compatibility. +// +// It reports any write errors back to the caller, Application.SetContentErrorHandler does NOT apply here +// as this is a lower-level method which must be remain as it is. func (ctx *Context) Write(rawBody []byte) (int, error) { return ctx.writer.Write(rawBody) } @@ -3757,38 +3760,20 @@ type ProtoMarshalOptions = protojson.MarshalOptions // JSON contains the options for the JSON (Context's) Renderer. type JSON struct { // http-specific - StreamingJSON bool + StreamingJSON bool `yaml:"StreamingJSON"` // content-specific - UnescapeHTML bool - Indent string - Prefix string - ASCII bool // if true writes with unicode to ASCII content. - Secure bool // if true then it prepends a "while(1);" when Go slice (to JSON Array) value. + UnescapeHTML bool `yaml:"UnescapeHTML"` + Indent string `yaml:"Indent"` + Prefix string `yaml:"Prefix"` + ASCII bool `yaml:"ASCII"` // if true writes with unicode to ASCII content. + Secure bool `yaml:"Secure"` // if true then it prepends a "while(1);" when Go slice (to JSON Array) value. // proto.Message specific marshal options. - Proto ProtoMarshalOptions - - // Optional context cancelation of encoder when Iris optimizations field is enabled. - // On JSON method this is automatically binded to the request context. - Context stdContext.Context - - // ErrorHandler can be optionally registered to fire a customized - // error to the client on JSON write failures. - ErrorHandler ErrorHandler + Proto ProtoMarshalOptions `yaml:"ProtoMarshalOptions"` } -type ( - // ErrorHandler describes a context error handler. As for today this is only used - // to globally or per-party or per-route handle JSON writes error. - ErrorHandler interface { - HandleContextError(ctx *Context, err error) - } - // ErrorHandlerFunc a function shortcut for ErrorHandler interface. - ErrorHandlerFunc func(ctx *Context, err error) -) - -func (h ErrorHandlerFunc) HandleContextError(ctx *Context, err error) { - h(ctx, err) -} +// DefaultJSONOptions is the optional settings that are being used +// inside `Context.JSON`. +var DefaultJSONOptions = JSON{} // IsDefault reports whether this JSON options structure holds the default values. func (j *JSON) IsDefault() bool { @@ -3799,16 +3784,6 @@ func (j *JSON) IsDefault() bool { j.ASCII == DefaultJSONOptions.ASCII && j.Secure == DefaultJSONOptions.Secure && j.Proto == DefaultJSONOptions.Proto - // except context and error handler -} - -// GetContext returns the option's Context or the HTTP request's one. -func (j *JSON) GetContext(ctx *Context) stdContext.Context { - if j.Context == nil { - return ctx.request.Context() - } - - return j.Context } // JSONP contains the options for the JSONP (Context's) Renderer. @@ -3849,32 +3824,64 @@ var ( secureJSONPrefix = []byte("while(1);") ) -func handleJSONResponseValue(w io.Writer, v interface{}, options JSON) (bool, int, error) { - if m, ok := v.(proto.Message); ok { - result, err := options.Proto.Marshal(m) - if err != nil { - return true, 0, err - } +func (ctx *Context) handleSpecialJSONResponseValue(v interface{}, options *JSON) (bool, int, error) { + if ctx.app.ConfigurationReadOnly().GetEnableProtoJSON() { + if m, ok := v.(proto.Message); ok { + protoJSON := ProtoMarshalOptions{} + if options != nil { + protoJSON = options.Proto + } - n, err := w.Write(result) - return true, n, err + result, err := protoJSON.Marshal(m) + if err != nil { + return true, 0, err + } + + n, err := ctx.writer.Write(result) + return true, n, err + } } - if easyObject, ok := v.(easyjson.Marshaler); ok { - jw := jwriter.Writer{NoEscapeHTML: !options.UnescapeHTML} - easyObject.MarshalEasyJSON(&jw) - n, err := jw.DumpTo(w) - return true, n, err + if ctx.app.ConfigurationReadOnly().GetEnableEasyJSON() { + if easyObject, ok := v.(easyjson.Marshaler); ok { + noEscapeHTML := false + if options != nil { + noEscapeHTML = !options.UnescapeHTML + } + jw := jwriter.Writer{NoEscapeHTML: noEscapeHTML} + easyObject.MarshalEasyJSON(&jw) + + n, err := jw.DumpTo(ctx.writer) + return true, n, err + } } return false, 0, nil } // WriteJSON marshals the given interface object and writes the JSON response to the 'writer'. -// Ignores StatusCode and StreamingJSON options. -func WriteJSON(writer io.Writer, v interface{}, options JSON, shouldOptimize bool) (int, error) { - if handled, n, err := handleJSONResponseValue(writer, v, options); handled { - return n, err +func WriteJSON(ctx stdContext.Context, writer io.Writer, v interface{}, options *JSON, shouldOptimize bool) (int, error) { + if options.StreamingJSON { + var err error + if shouldOptimize { + // jsoniterConfig := jsoniter.Config{ + // EscapeHTML: !options.UnescapeHTML, + // IndentionStep: 4, + // }.Froze() + // enc := jsoniterConfig.NewEncoder(ctx.writer) + // err = enc.Encode(v) + enc := gojson.NewEncoder(writer) + enc.SetEscapeHTML(!options.UnescapeHTML) + enc.SetIndent(options.Prefix, options.Indent) + err = enc.EncodeContext(ctx, v) + } else { + enc := json.NewEncoder(writer) + enc.SetEscapeHTML(!options.UnescapeHTML) + enc.SetIndent(options.Prefix, options.Indent) + err = enc.Encode(v) + } + + return 0, err } var ( @@ -3899,8 +3906,8 @@ func WriteJSON(writer io.Writer, v interface{}, options JSON, shouldOptimize boo } else { if shouldOptimize { // result, err = jsoniter.ConfigCompatibleWithStandardLibrary.Marshal - if options.Context != nil { - result, err = gojson.MarshalContext(options.Context, v) + if ctx != nil { + result, err = gojson.MarshalContext(ctx, v) } else { result, err = gojson.Marshal(v) } @@ -3939,7 +3946,7 @@ func WriteJSON(writer io.Writer, v interface{}, options JSON, shouldOptimize boo if options.ASCII { if len(result) > 0 { buf := new(bytes.Buffer) - for _, s := range bytesToString(result) { + for _, s := range string(result) { char := string(s) if s >= 128 { char = fmt.Sprintf("\\u%04x", int64(s)) @@ -3967,123 +3974,84 @@ func stringToBytes(s string) []byte { return *(*[]byte)(unsafe.Pointer(&s)) } -// DefaultJSONOptions is the optional settings that are being used -// inside `ctx.JSON`. -var DefaultJSONOptions = JSON{} +type ( + // ErrorHandler describes a context error handler which applies on + // JSON, JSONP, Protobuf, MsgPack, XML, YAML and Markdown write errors. + // + // An ErrorHandler can be registered once via Application.SetErrorHandler method to override the default behavior. + // The default behavior is to simply send status internal code error + // without a body back to the client. + ErrorHandler interface { + HandleContextError(ctx *Context, err error) + } + // ErrorHandlerFunc a function shortcut for ErrorHandler interface. + ErrorHandlerFunc func(ctx *Context, err error) +) -const jsonOptionsContextKey = "iris.context.json_options" - -// SetJSONOptions stores the given JSON options to the handler -// for any next Context.JSON calls. Note that the Context.JSON's -// variadic options have priority over these given options. -// -// Usage Example: -// -// type jsonErrorHandler struct{} -// func (e *jsonErrorHandler) HandleContextError(ctx iris.Context, err error) { -// errors.InvalidArgument.Err(ctx, err) -// } -// ... -// errHandler := new(jsonErrorHandler) -// srv.Use(func(ctx iris.Context) { -// ctx.SetJSONOptions(iris.JSON{ -// ErrorHandler: errHandler, -// }) -// ctx.Next() -// }) -func (ctx *Context) SetJSONOptions(opts JSON) { - ctx.values.Set(jsonOptionsContextKey, opts) +// HandleContextError completes the ErrorHandler interface. +func (h ErrorHandlerFunc) HandleContextError(ctx *Context, err error) { + h(ctx, err) } -func (ctx *Context) getJSONOptions() (JSON, bool) { - if v := ctx.values.Get(jsonOptionsContextKey); v != nil { - opts, ok := v.(JSON) - return opts, ok +func (ctx *Context) handleContextError(err error) { + if errHandler := ctx.app.GetContextErrorHandler(); errHandler != nil { + errHandler.HandleContextError(ctx, err) + } else { + ctx.StatusCode(http.StatusInternalServerError) } - return DefaultJSONOptions, false + // keep the error non nil so the caller has control over further actions. } -// JSON marshals the given interface object and writes the JSON response to the client. -// If the value is a compatible `proto.Message` one -// then it only uses the options.Proto settings to marshal. +// JSON marshals the given "v" value to JSON and writes the response to the client. +// Look the Configuration.EnableProtoJSON/EnableEasyJSON and EnableOptimizations too. +// +// It reports any JSON parser or write errors back to the caller. +// Look the Application.SetContextErrorHandler to override the +// default status code 500 with a custom error response. +// +// It can, optionally, accept the JSON structure which may hold customizations over the +// final JSON response but keep in mind that the caller should NOT modify that JSON options +// value in another goroutine while JSON method is still running. func (ctx *Context) JSON(v interface{}, opts ...JSON) (n int, err error) { - if n, err = ctx.writeJSON(v, opts...); err != nil { - if opts, ok := ctx.getJSONOptions(); ok { - opts.ErrorHandler.HandleContextError(ctx, err) - } // keep the error so the caller has control over further actions. + var options *JSON + if len(opts) > 0 { + options = &opts[0] + } + + if n, err = ctx.writeJSON(v, options); err != nil { + ctx.handleContextError(err) } return } -func (ctx *Context) writeJSON(v interface{}, opts ...JSON) (n int, err error) { +func (ctx *Context) writeJSON(v interface{}, options *JSON) (int, error) { ctx.ContentType(ContentJSONHeaderValue) + + // After content type given and before everything else, try handle proto or easyjson, no matter the performance mode. + if handled, n, err := ctx.handleSpecialJSONResponseValue(v, options); handled { + return n, err + } + shouldOptimize := ctx.shouldOptimize() - - options := DefaultJSONOptions - optsLength := len(opts) - if optsLength > 0 { - options = opts[0] - } else { - if opt, ok := ctx.getJSONOptions(); ok { - options = opt - if !options.IsDefault() { // keep the next branch valid when only the Context or/and ErrorHandler are modified. - optsLength = 1 - } - } - } - - if shouldOptimize && optsLength == 0 { // if no options given and optimizations are enabled. - // try handle proto or easyjson. - if handled, n, err := handleJSONResponseValue(ctx, v, options); handled { - return n, err - } - - // as soon as possible, use the fast json marshaler with the http request context. - result, err := gojson.MarshalContext(ctx.request.Context(), v) - if err != nil { - return 0, err - } - - return ctx.Write(result) - } - - if options.StreamingJSON { + if options == nil { if shouldOptimize { - // jsoniterConfig := jsoniter.Config{ - // EscapeHTML: !options.UnescapeHTML, - // IndentionStep: 4, - // }.Froze() - // enc := jsoniterConfig.NewEncoder(ctx.writer) - // err = enc.Encode(v) - enc := gojson.NewEncoder(ctx.writer) - enc.SetEscapeHTML(!options.UnescapeHTML) - enc.SetIndent(options.Prefix, options.Indent) - err = enc.EncodeContext(options.GetContext(ctx), v) - } else { - enc := json.NewEncoder(ctx.writer) - enc.SetEscapeHTML(!options.UnescapeHTML) - enc.SetIndent(options.Prefix, options.Indent) - err = enc.Encode(v) + // If no options given and optimizations are enabled. + // write using the fast json marshaler with the http request context as soon as possible. + result, err := gojson.MarshalContext(ctx.request.Context(), v) + if err != nil { + return 0, err + } + + return ctx.Write(result) } - if err != nil { - ctx.app.Logger().Debugf("JSON: %v", err) - ctx.StatusCode(http.StatusInternalServerError) // it handles the fallback to normal mode here which also removes any compression headers. - return 0, err - } - return ctx.writer.Written(), err + // Else if no options given neither optimizations are enabled, then safely read the already-initialized object. + options = &DefaultJSONOptions } - n, err = WriteJSON(ctx.writer, v, options, shouldOptimize) - if err != nil { - ctx.app.Logger().Debugf("JSON: %v", err) - ctx.StatusCode(http.StatusInternalServerError) - return 0, err - } - - return n, err + return WriteJSON(ctx, ctx.writer, v, options, shouldOptimize) } var finishCallbackB = []byte(");") @@ -4134,24 +4102,23 @@ func WriteJSONP(writer io.Writer, v interface{}, options JSONP, optimize bool) ( // inside `ctx.JSONP`. var DefaultJSONPOptions = JSONP{} -// JSONP marshals the given interface object and writes the JSON response to the client. -func (ctx *Context) JSONP(v interface{}, opts ...JSONP) (int, error) { +// JSONP marshals the given "v" value to JSON and sends the response to the client. +// +// It reports any JSON parser or write errors back to the caller. +// Look the Application.SetContextErrorHandler to override the +// default status code 500 with a custom error response. +func (ctx *Context) JSONP(v interface{}, opts ...JSONP) (n int, err error) { options := DefaultJSONPOptions - if len(opts) > 0 { options = opts[0] } ctx.ContentType(ContentJavascriptHeaderValue) - - n, err := WriteJSONP(ctx.writer, v, options, ctx.shouldOptimize()) - if err != nil { - ctx.app.Logger().Debugf("JSONP: %v", err) - ctx.StatusCode(http.StatusInternalServerError) - return 0, err + if n, err = WriteJSONP(ctx.writer, v, options, ctx.shouldOptimize()); err != nil { + ctx.handleContextError(err) } - return n, err + return } type xmlMapEntry struct { @@ -4232,29 +4199,28 @@ var DefaultXMLOptions = XML{} // XML marshals the given interface object and writes the XML response to the client. // To render maps as XML see the `XMLMap` package-level function. -func (ctx *Context) XML(v interface{}, opts ...XML) (int, error) { +// +// It reports any XML parser or write errors back to the caller. +// Look the Application.SetContextErrorHandler to override the +// default status code 500 with a custom error response. +func (ctx *Context) XML(v interface{}, opts ...XML) (n int, err error) { options := DefaultXMLOptions - if len(opts) > 0 { options = opts[0] } ctx.ContentType(ContentXMLHeaderValue) - - n, err := WriteXML(ctx.writer, v, options, ctx.shouldOptimize()) - if err != nil { - ctx.app.Logger().Debugf("XML: %v", err) - ctx.StatusCode(http.StatusInternalServerError) - return 0, err + if n, err = WriteXML(ctx.writer, v, options, ctx.shouldOptimize()); err != nil { + ctx.handleContextError(err) } - return n, err + return } // Problem writes a JSON or XML problem response. // Order of Problem fields are not always rendered the same. // -// Behaves exactly like `Context.JSON` +// Behaves exactly like the `Context.JSON` method // but with default ProblemOptions.JSON indent of " " and // a response content type of "application/problem+json" instead. // @@ -4299,11 +4265,12 @@ func (ctx *Context) Problem(v interface{}, opts ...ProblemOptions) (int, error) } // WriteMarkdown parses the markdown to html and writes these contents to the writer. -func WriteMarkdown(writer io.Writer, markdownB []byte, options Markdown) (int, error) { +func WriteMarkdown(writer io.Writer, markdownB []byte, options Markdown) (n int, err error) { buf := blackfriday.Run(markdownB) if options.Sanitize { buf = bluemonday.UGCPolicy().SanitizeBytes(buf) } + return writer.Write(buf) } @@ -4312,66 +4279,90 @@ func WriteMarkdown(writer io.Writer, markdownB []byte, options Markdown) (int, e var DefaultMarkdownOptions = Markdown{} // Markdown parses the markdown to html and renders its result to the client. -func (ctx *Context) Markdown(markdownB []byte, opts ...Markdown) (int, error) { +// +// It reports any Markdown parser or write errors back to the caller. +// Look the Application.SetContextErrorHandler to override the +// default status code 500 with a custom error response. +func (ctx *Context) Markdown(markdownB []byte, opts ...Markdown) (n int, err error) { options := DefaultMarkdownOptions - if len(opts) > 0 { options = opts[0] } ctx.ContentType(ContentHTMLHeaderValue) + if n, err = WriteMarkdown(ctx.writer, markdownB, options); err != nil { + ctx.handleContextError(err) + } - n, err := WriteMarkdown(ctx.writer, markdownB, options) + return +} + +// YAML marshals the given "v" value using the yaml marshaler and writes the result to the client. +// +// It reports any YAML parser or write errors back to the caller. +// Look the Application.SetContextErrorHandler to override the +// default status code 500 with a custom error response. +func (ctx *Context) YAML(v interface{}) (int, error) { + out, err := yaml.Marshal(v) if err != nil { - ctx.app.Logger().Debugf("Markdown: %v", err) - ctx.StatusCode(http.StatusInternalServerError) + ctx.handleContextError(err) return 0, err } + ctx.ContentType(ContentYAMLHeaderValue) + n, err := ctx.Write(out) + if err != nil { + ctx.handleContextError(err) + } + return n, err } -// YAML marshals the "v" using the yaml marshaler -// and sends the result to the client. -func (ctx *Context) YAML(v interface{}) (int, error) { - out, err := yaml.Marshal(v) - if err != nil { - ctx.app.Logger().Debugf("YAML: %v", err) - ctx.StatusCode(http.StatusInternalServerError) - return 0, err - } - - ctx.ContentType(ContentYAMLHeaderValue) - return ctx.Write(out) -} - -// TextYAML marshals the "v" using the yaml marshaler -// and renders to the client. +// TextYAML calls the Context.YAML method but with the text/yaml content type instead. func (ctx *Context) TextYAML(v interface{}) (int, error) { ctx.contentTypeOnce(ContentYAMLTextHeaderValue, "") return ctx.YAML(v) } -// Protobuf parses the "v" of proto Message and renders its result to the client. +// Protobuf marshals the given "v" value of proto Message and writes its result to the client. +// +// It reports any protobuf parser or write errors back to the caller. +// Look the Application.SetContextErrorHandler to override the +// default status code 500 with a custom error response. func (ctx *Context) Protobuf(v proto.Message) (int, error) { out, err := proto.Marshal(v) if err != nil { + ctx.handleContextError(err) return 0, err } ctx.ContentType(ContentProtobufHeaderValue) - return ctx.Write(out) + n, err := ctx.Write(out) + if err != nil { + ctx.handleContextError(err) + } + + return n, err } -// MsgPack parses the "v" of msgpack format and renders its result to the client. +// MsgPack marshals the given "v" value of msgpack format and writes its result to the client. +// +// It reports any message pack or write errors back to the caller. +// Look the Application.SetContextErrorHandler to override the +// default status code 500 with a custom error response. func (ctx *Context) MsgPack(v interface{}) (int, error) { out, err := msgpack.Marshal(v) if err != nil { - return 0, err + ctx.handleContextError(err) } ctx.ContentType(ContentMsgPackHeaderValue) - return ctx.Write(out) + n, err := ctx.Write(out) + if err != nil { + ctx.handleContextError(err) + } + + return n, err } // +-----------------------------------------------------------------------+ diff --git a/iris.go b/iris.go index 0405d763..c28a7cdf 100644 --- a/iris.go +++ b/iris.go @@ -59,6 +59,8 @@ type Application struct { *router.Router router.HTTPErrorHandler // if Router is Downgraded this is nil. ContextPool *context.Pool + // See SetContextErrorHandler, defaults to nil. + contextErrorHandler context.ErrorHandler // config contains the configuration fields // all fields defaults to something that is working, developers don't have to set it. @@ -429,6 +431,28 @@ func (app *Application) GetContextPool() *context.Pool { return app.ContextPool } +// SetContextErrorHandler can optionally register a handler to handle +// and fire a customized error body to the client on JSON write failures. +// +// ExampleCode: +// +// type contextErrorHandler struct{} +// func (e *contextErrorHandler) HandleContextError(ctx iris.Context, err error) { +// errors.InvalidArgument.Err(ctx, err) +// } +// ... +// app.SetContextErrorHandler(new(contextErrorHandler)) +func (app *Application) SetContextErrorHandler(errHandler context.ErrorHandler) *Application { + app.contextErrorHandler = errHandler + return app +} + +// GetContextErrorHandler returns the handler which handles errors +// on JSON write failures. +func (app *Application) GetContextErrorHandler() context.ErrorHandler { + return app.contextErrorHandler +} + // ConfigureHost accepts one or more `host#Configuration`, these configurators functions // can access the host created by `app.Run` or `app.Listen`, // they're being executed when application is ready to being served to the public.