new Application.SetContextErrorHandler method

This commit is contained in:
Gerasimos (Makis) Maropoulos 2022-04-13 01:00:53 +03:00
parent 73dfabf412
commit ae828d8db9
No known key found for this signature in database
GPG Key ID: 66FCC29BD385FCA6
7 changed files with 314 additions and 243 deletions

View File

@ -1,8 +1,9 @@
version: 2 version: 3
cli: cli:
server: https://app.fossa.com server: https://app.fossa.com
fetcher: custom fetcher: git
project: https://github.com/kataras/iris.git package: github.com/kataras/iris
project: github.com/kataras/iris
analyze: analyze:
modules: modules:
- name: iris - name: iris

View File

@ -28,11 +28,12 @@ The codebase for Dependency Injection, Internationalization and localization and
## Fixes and Improvements ## 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 [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 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 `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 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). - 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. - 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. - 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.

View File

@ -10,11 +10,11 @@ import (
"strings" "strings"
"time" "time"
"github.com/kataras/golog"
"github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/context"
"github.com/kataras/iris/v12/core/netutil" "github.com/kataras/iris/v12/core/netutil"
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
"github.com/kataras/golog"
"github.com/kataras/sitemap" "github.com/kataras/sitemap"
"github.com/kataras/tunnel" "github.com/kataras/tunnel"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
@ -317,6 +317,20 @@ var WithOptimizations = func(app *Application) {
app.config.EnableOptimizations = true 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. // WithFireMethodNotAllowed enables the FireMethodNotAllowed setting.
// //
// See `Configuration`. // See `Configuration`.
@ -740,6 +754,17 @@ type Configuration struct {
// //
// Defaults to false. // Defaults to false.
EnableOptimizations bool `ini:"enable_optimizations" json:"enableOptimizations,omitempty" yaml:"EnableOptimizations" toml:"EnableOptimizations"` 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. // DisableBodyConsumptionOnUnmarshal manages the reading behavior of the context's body readers/binders.
// If set to true then it // If set to true then it
// disables the body consumption by the `context.UnmarshalBody/ReadJSON/ReadXML`. // disables the body consumption by the `context.UnmarshalBody/ReadJSON/ReadXML`.
@ -915,177 +940,187 @@ type Configuration struct {
var _ context.ConfigurationReadOnly = (*Configuration)(nil) var _ context.ConfigurationReadOnly = (*Configuration)(nil)
// GetVHost returns the non-exported vhost config field. // GetVHost returns the non-exported vhost config field.
func (c Configuration) GetVHost() string { func (c *Configuration) GetVHost() string {
return c.vhost return c.vhost
} }
// GetLogLevel returns the LogLevel field. // GetLogLevel returns the LogLevel field.
func (c Configuration) GetLogLevel() string { func (c *Configuration) GetLogLevel() string {
return c.vhost return c.vhost
} }
// GetSocketSharding returns the SocketSharding field. // GetSocketSharding returns the SocketSharding field.
func (c Configuration) GetSocketSharding() bool { func (c *Configuration) GetSocketSharding() bool {
return c.SocketSharding return c.SocketSharding
} }
// GetKeepAlive returns the KeepAlive field. // GetKeepAlive returns the KeepAlive field.
func (c Configuration) GetKeepAlive() time.Duration { func (c *Configuration) GetKeepAlive() time.Duration {
return c.KeepAlive return c.KeepAlive
} }
// GetKeepAlive returns the Timeout field. // GetKeepAlive returns the Timeout field.
func (c Configuration) GetTimeout() time.Duration { func (c *Configuration) GetTimeout() time.Duration {
return c.Timeout return c.Timeout
} }
// GetKeepAlive returns the TimeoutMessage field. // GetKeepAlive returns the TimeoutMessage field.
func (c Configuration) GetTimeoutMessage() string { func (c *Configuration) GetTimeoutMessage() string {
return c.TimeoutMessage return c.TimeoutMessage
} }
// GetDisablePathCorrection returns the DisablePathCorrection field. // GetDisablePathCorrection returns the DisablePathCorrection field.
func (c Configuration) GetDisablePathCorrection() bool { func (c *Configuration) GetDisablePathCorrection() bool {
return c.DisablePathCorrection return c.DisablePathCorrection
} }
// GetDisablePathCorrectionRedirection returns the DisablePathCorrectionRedirection field. // GetDisablePathCorrectionRedirection returns the DisablePathCorrectionRedirection field.
func (c Configuration) GetDisablePathCorrectionRedirection() bool { func (c *Configuration) GetDisablePathCorrectionRedirection() bool {
return c.DisablePathCorrectionRedirection return c.DisablePathCorrectionRedirection
} }
// GetEnablePathIntelligence returns the EnablePathIntelligence field. // GetEnablePathIntelligence returns the EnablePathIntelligence field.
func (c Configuration) GetEnablePathIntelligence() bool { func (c *Configuration) GetEnablePathIntelligence() bool {
return c.EnablePathIntelligence return c.EnablePathIntelligence
} }
// GetEnablePathEscape returns the EnablePathEscape field. // GetEnablePathEscape returns the EnablePathEscape field.
func (c Configuration) GetEnablePathEscape() bool { func (c *Configuration) GetEnablePathEscape() bool {
return c.EnablePathEscape return c.EnablePathEscape
} }
// GetForceLowercaseRouting returns the ForceLowercaseRouting field. // GetForceLowercaseRouting returns the ForceLowercaseRouting field.
func (c Configuration) GetForceLowercaseRouting() bool { func (c *Configuration) GetForceLowercaseRouting() bool {
return c.ForceLowercaseRouting return c.ForceLowercaseRouting
} }
// GetFireMethodNotAllowed returns the FireMethodNotAllowed field. // GetFireMethodNotAllowed returns the FireMethodNotAllowed field.
func (c Configuration) GetFireMethodNotAllowed() bool { func (c *Configuration) GetFireMethodNotAllowed() bool {
return c.FireMethodNotAllowed return c.FireMethodNotAllowed
} }
// GetEnableOptimizations returns the EnableOptimizations. // GetEnableOptimizations returns the EnableOptimizations.
func (c Configuration) GetEnableOptimizations() bool { func (c *Configuration) GetEnableOptimizations() bool {
return c.EnableOptimizations 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. // GetDisableBodyConsumptionOnUnmarshal returns the DisableBodyConsumptionOnUnmarshal field.
func (c Configuration) GetDisableBodyConsumptionOnUnmarshal() bool { func (c *Configuration) GetDisableBodyConsumptionOnUnmarshal() bool {
return c.DisableBodyConsumptionOnUnmarshal return c.DisableBodyConsumptionOnUnmarshal
} }
// GetFireEmptyFormError returns the DisableBodyConsumptionOnUnmarshal field. // GetFireEmptyFormError returns the DisableBodyConsumptionOnUnmarshal field.
func (c Configuration) GetFireEmptyFormError() bool { func (c *Configuration) GetFireEmptyFormError() bool {
return c.FireEmptyFormError return c.FireEmptyFormError
} }
// GetDisableAutoFireStatusCode returns the DisableAutoFireStatusCode field. // GetDisableAutoFireStatusCode returns the DisableAutoFireStatusCode field.
func (c Configuration) GetDisableAutoFireStatusCode() bool { func (c *Configuration) GetDisableAutoFireStatusCode() bool {
return c.DisableAutoFireStatusCode return c.DisableAutoFireStatusCode
} }
// GetResetOnFireErrorCode returns ResetOnFireErrorCode field. // GetResetOnFireErrorCode returns ResetOnFireErrorCode field.
func (c Configuration) GetResetOnFireErrorCode() bool { func (c *Configuration) GetResetOnFireErrorCode() bool {
return c.ResetOnFireErrorCode return c.ResetOnFireErrorCode
} }
// GetTimeFormat returns the TimeFormat field. // GetTimeFormat returns the TimeFormat field.
func (c Configuration) GetTimeFormat() string { func (c *Configuration) GetTimeFormat() string {
return c.TimeFormat return c.TimeFormat
} }
// GetCharset returns the Charset field. // GetCharset returns the Charset field.
func (c Configuration) GetCharset() string { func (c *Configuration) GetCharset() string {
return c.Charset return c.Charset
} }
// GetPostMaxMemory returns the PostMaxMemory field. // GetPostMaxMemory returns the PostMaxMemory field.
func (c Configuration) GetPostMaxMemory() int64 { func (c *Configuration) GetPostMaxMemory() int64 {
return c.PostMaxMemory return c.PostMaxMemory
} }
// GetLocaleContextKey returns the LocaleContextKey field. // GetLocaleContextKey returns the LocaleContextKey field.
func (c Configuration) GetLocaleContextKey() string { func (c *Configuration) GetLocaleContextKey() string {
return c.LocaleContextKey return c.LocaleContextKey
} }
// GetLanguageContextKey returns the LanguageContextKey field. // GetLanguageContextKey returns the LanguageContextKey field.
func (c Configuration) GetLanguageContextKey() string { func (c *Configuration) GetLanguageContextKey() string {
return c.LanguageContextKey return c.LanguageContextKey
} }
// GetLanguageInputContextKey returns the LanguageInputContextKey field. // GetLanguageInputContextKey returns the LanguageInputContextKey field.
func (c Configuration) GetLanguageInputContextKey() string { func (c *Configuration) GetLanguageInputContextKey() string {
return c.LanguageInputContextKey return c.LanguageInputContextKey
} }
// GetVersionContextKey returns the VersionContextKey field. // GetVersionContextKey returns the VersionContextKey field.
func (c Configuration) GetVersionContextKey() string { func (c *Configuration) GetVersionContextKey() string {
return c.VersionContextKey return c.VersionContextKey
} }
// GetVersionAliasesContextKey returns the VersionAliasesContextKey field. // GetVersionAliasesContextKey returns the VersionAliasesContextKey field.
func (c Configuration) GetVersionAliasesContextKey() string { func (c *Configuration) GetVersionAliasesContextKey() string {
return c.VersionAliasesContextKey return c.VersionAliasesContextKey
} }
// GetViewEngineContextKey returns the ViewEngineContextKey field. // GetViewEngineContextKey returns the ViewEngineContextKey field.
func (c Configuration) GetViewEngineContextKey() string { func (c *Configuration) GetViewEngineContextKey() string {
return c.ViewEngineContextKey return c.ViewEngineContextKey
} }
// GetViewLayoutContextKey returns the ViewLayoutContextKey field. // GetViewLayoutContextKey returns the ViewLayoutContextKey field.
func (c Configuration) GetViewLayoutContextKey() string { func (c *Configuration) GetViewLayoutContextKey() string {
return c.ViewLayoutContextKey return c.ViewLayoutContextKey
} }
// GetViewDataContextKey returns the ViewDataContextKey field. // GetViewDataContextKey returns the ViewDataContextKey field.
func (c Configuration) GetViewDataContextKey() string { func (c *Configuration) GetViewDataContextKey() string {
return c.ViewDataContextKey return c.ViewDataContextKey
} }
// GetFallbackViewContextKey returns the FallbackViewContextKey field. // GetFallbackViewContextKey returns the FallbackViewContextKey field.
func (c Configuration) GetFallbackViewContextKey() string { func (c *Configuration) GetFallbackViewContextKey() string {
return c.FallbackViewContextKey return c.FallbackViewContextKey
} }
// GetRemoteAddrHeaders returns the RemoteAddrHeaders field. // GetRemoteAddrHeaders returns the RemoteAddrHeaders field.
func (c Configuration) GetRemoteAddrHeaders() []string { func (c *Configuration) GetRemoteAddrHeaders() []string {
return c.RemoteAddrHeaders return c.RemoteAddrHeaders
} }
// GetRemoteAddrHeadersForce returns RemoteAddrHeadersForce field. // GetRemoteAddrHeadersForce returns RemoteAddrHeadersForce field.
func (c Configuration) GetRemoteAddrHeadersForce() bool { func (c *Configuration) GetRemoteAddrHeadersForce() bool {
return c.RemoteAddrHeadersForce return c.RemoteAddrHeadersForce
} }
// GetSSLProxyHeaders returns the SSLProxyHeaders field. // GetSSLProxyHeaders returns the SSLProxyHeaders field.
func (c Configuration) GetSSLProxyHeaders() map[string]string { func (c *Configuration) GetSSLProxyHeaders() map[string]string {
return c.SSLProxyHeaders return c.SSLProxyHeaders
} }
// GetRemoteAddrPrivateSubnets returns the RemoteAddrPrivateSubnets field. // GetRemoteAddrPrivateSubnets returns the RemoteAddrPrivateSubnets field.
func (c Configuration) GetRemoteAddrPrivateSubnets() []netutil.IPRange { func (c *Configuration) GetRemoteAddrPrivateSubnets() []netutil.IPRange {
return c.RemoteAddrPrivateSubnets return c.RemoteAddrPrivateSubnets
} }
// GetHostProxyHeaders returns the HostProxyHeaders field. // GetHostProxyHeaders returns the HostProxyHeaders field.
func (c Configuration) GetHostProxyHeaders() map[string]bool { func (c *Configuration) GetHostProxyHeaders() map[string]bool {
return c.HostProxyHeaders return c.HostProxyHeaders
} }
// GetOther returns the Other field. // GetOther returns the Other field.
func (c Configuration) GetOther() map[string]interface{} { func (c *Configuration) GetOther() map[string]interface{} {
return c.Other return c.Other
} }
@ -1166,6 +1201,14 @@ func WithConfiguration(c Configuration) Configurator {
main.EnableOptimizations = v main.EnableOptimizations = v
} }
if v := c.EnableProtoJSON; v {
main.EnableProtoJSON = v
}
if v := c.EnableEasyJSON; v {
main.EnableEasyJSON = v
}
if v := c.FireMethodNotAllowed; v { if v := c.FireMethodNotAllowed; v {
main.FireMethodNotAllowed = v main.FireMethodNotAllowed = v
} }
@ -1342,6 +1385,8 @@ func DefaultConfiguration() Configuration {
SSLProxyHeaders: make(map[string]string), SSLProxyHeaders: make(map[string]string),
HostProxyHeaders: make(map[string]bool), HostProxyHeaders: make(map[string]bool),
EnableOptimizations: false, EnableOptimizations: false,
EnableProtoJSON: false,
EnableEasyJSON: false,
Other: make(map[string]interface{}), Other: make(map[string]interface{}),
} }
} }

View File

@ -52,6 +52,10 @@ type Application interface {
// is hijacked by a third-party middleware and the http handler return too fast. // is hijacked by a third-party middleware and the http handler return too fast.
GetContextPool() *Pool 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, // ServeHTTPC is the internal router, it's visible because it can be used for advanced use cases,
// i.e: routing within a foreign context. // i.e: routing within a foreign context.
// //

View File

@ -43,6 +43,11 @@ type ConfigurationReadOnly interface {
// GetEnableOptimizations returns the EnableOptimizations field. // GetEnableOptimizations returns the EnableOptimizations field.
GetEnableOptimizations() bool GetEnableOptimizations() bool
// GetEnableProtoJSON returns the EnableProtoJSON field.
GetEnableProtoJSON() bool
// GetEnableEasyJSON returns the EnableEasyJSON field.
GetEnableEasyJSON() bool
// GetDisableBodyConsumptionOnUnmarshal returns the DisableBodyConsumptionOnUnmarshal field. // GetDisableBodyConsumptionOnUnmarshal returns the DisableBodyConsumptionOnUnmarshal field.
GetDisableBodyConsumptionOnUnmarshal() bool GetDisableBodyConsumptionOnUnmarshal() bool
// GetFireEmptyFormError returns the FireEmptyFormError field. // GetFireEmptyFormError returns the FireEmptyFormError field.

View File

@ -3046,6 +3046,9 @@ func (ctx *Context) ReadBody(ptr interface{}) error {
// writing the response. However, such behavior may not be supported // writing the response. However, such behavior may not be supported
// by all HTTP/2 clients. Handlers should read before writing if // by all HTTP/2 clients. Handlers should read before writing if
// possible to maximize compatibility. // 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) { func (ctx *Context) Write(rawBody []byte) (int, error) {
return ctx.writer.Write(rawBody) return ctx.writer.Write(rawBody)
} }
@ -3757,38 +3760,20 @@ type ProtoMarshalOptions = protojson.MarshalOptions
// JSON contains the options for the JSON (Context's) Renderer. // JSON contains the options for the JSON (Context's) Renderer.
type JSON struct { type JSON struct {
// http-specific // http-specific
StreamingJSON bool StreamingJSON bool `yaml:"StreamingJSON"`
// content-specific // content-specific
UnescapeHTML bool UnescapeHTML bool `yaml:"UnescapeHTML"`
Indent string Indent string `yaml:"Indent"`
Prefix string Prefix string `yaml:"Prefix"`
ASCII bool // if true writes with unicode to ASCII content. ASCII bool `yaml:"ASCII"` // 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. 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.Message specific marshal options.
Proto ProtoMarshalOptions Proto ProtoMarshalOptions `yaml:"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
} }
type ( // DefaultJSONOptions is the optional settings that are being used
// ErrorHandler describes a context error handler. As for today this is only used // inside `Context.JSON`.
// to globally or per-party or per-route handle JSON writes error. var DefaultJSONOptions = JSON{}
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)
}
// IsDefault reports whether this JSON options structure holds the default values. // IsDefault reports whether this JSON options structure holds the default values.
func (j *JSON) IsDefault() bool { func (j *JSON) IsDefault() bool {
@ -3799,16 +3784,6 @@ func (j *JSON) IsDefault() bool {
j.ASCII == DefaultJSONOptions.ASCII && j.ASCII == DefaultJSONOptions.ASCII &&
j.Secure == DefaultJSONOptions.Secure && j.Secure == DefaultJSONOptions.Secure &&
j.Proto == DefaultJSONOptions.Proto 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. // JSONP contains the options for the JSONP (Context's) Renderer.
@ -3849,32 +3824,64 @@ var (
secureJSONPrefix = []byte("while(1);") secureJSONPrefix = []byte("while(1);")
) )
func handleJSONResponseValue(w io.Writer, v interface{}, options JSON) (bool, int, error) { func (ctx *Context) handleSpecialJSONResponseValue(v interface{}, options *JSON) (bool, int, error) {
if ctx.app.ConfigurationReadOnly().GetEnableProtoJSON() {
if m, ok := v.(proto.Message); ok { if m, ok := v.(proto.Message); ok {
result, err := options.Proto.Marshal(m) protoJSON := ProtoMarshalOptions{}
if options != nil {
protoJSON = options.Proto
}
result, err := protoJSON.Marshal(m)
if err != nil { if err != nil {
return true, 0, err return true, 0, err
} }
n, err := w.Write(result) n, err := ctx.writer.Write(result)
return true, n, err return true, n, err
} }
}
if ctx.app.ConfigurationReadOnly().GetEnableEasyJSON() {
if easyObject, ok := v.(easyjson.Marshaler); ok { if easyObject, ok := v.(easyjson.Marshaler); ok {
jw := jwriter.Writer{NoEscapeHTML: !options.UnescapeHTML} noEscapeHTML := false
if options != nil {
noEscapeHTML = !options.UnescapeHTML
}
jw := jwriter.Writer{NoEscapeHTML: noEscapeHTML}
easyObject.MarshalEasyJSON(&jw) easyObject.MarshalEasyJSON(&jw)
n, err := jw.DumpTo(w)
n, err := jw.DumpTo(ctx.writer)
return true, n, err return true, n, err
} }
}
return false, 0, nil return false, 0, nil
} }
// WriteJSON marshals the given interface object and writes the JSON response to the 'writer'. // WriteJSON marshals the given interface object and writes the JSON response to the 'writer'.
// Ignores StatusCode and StreamingJSON options. func WriteJSON(ctx stdContext.Context, writer io.Writer, v interface{}, options *JSON, shouldOptimize bool) (int, error) {
func WriteJSON(writer io.Writer, v interface{}, options JSON, shouldOptimize bool) (int, error) { if options.StreamingJSON {
if handled, n, err := handleJSONResponseValue(writer, v, options); handled { var err error
return n, err 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 ( var (
@ -3899,8 +3906,8 @@ func WriteJSON(writer io.Writer, v interface{}, options JSON, shouldOptimize boo
} else { } else {
if shouldOptimize { if shouldOptimize {
// result, err = jsoniter.ConfigCompatibleWithStandardLibrary.Marshal // result, err = jsoniter.ConfigCompatibleWithStandardLibrary.Marshal
if options.Context != nil { if ctx != nil {
result, err = gojson.MarshalContext(options.Context, v) result, err = gojson.MarshalContext(ctx, v)
} else { } else {
result, err = gojson.Marshal(v) result, err = gojson.Marshal(v)
} }
@ -3939,7 +3946,7 @@ func WriteJSON(writer io.Writer, v interface{}, options JSON, shouldOptimize boo
if options.ASCII { if options.ASCII {
if len(result) > 0 { if len(result) > 0 {
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
for _, s := range bytesToString(result) { for _, s := range string(result) {
char := string(s) char := string(s)
if s >= 128 { if s >= 128 {
char = fmt.Sprintf("\\u%04x", int64(s)) char = fmt.Sprintf("\\u%04x", int64(s))
@ -3967,80 +3974,71 @@ func stringToBytes(s string) []byte {
return *(*[]byte)(unsafe.Pointer(&s)) return *(*[]byte)(unsafe.Pointer(&s))
} }
// DefaultJSONOptions is the optional settings that are being used type (
// inside `ctx.JSON`. // ErrorHandler describes a context error handler which applies on
var DefaultJSONOptions = JSON{} // JSON, JSONP, Protobuf, MsgPack, XML, YAML and Markdown write errors.
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: // 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)
)
// HandleContextError completes the ErrorHandler interface.
func (h ErrorHandlerFunc) HandleContextError(ctx *Context, err error) {
h(ctx, err)
}
func (ctx *Context) handleContextError(err error) {
if errHandler := ctx.app.GetContextErrorHandler(); errHandler != nil {
errHandler.HandleContextError(ctx, err)
} else {
ctx.StatusCode(http.StatusInternalServerError)
}
// keep the error non nil so the caller has control over further actions.
}
// JSON marshals the given "v" value to JSON and writes the response to the client.
// Look the Configuration.EnableProtoJSON/EnableEasyJSON and EnableOptimizations too.
// //
// type jsonErrorHandler struct{} // It reports any JSON parser or write errors back to the caller.
// func (e *jsonErrorHandler) HandleContextError(ctx iris.Context, err error) { // Look the Application.SetContextErrorHandler to override the
// errors.InvalidArgument.Err(ctx, err) // default status code 500 with a custom error response.
// } //
// ... // It can, optionally, accept the JSON structure which may hold customizations over the
// errHandler := new(jsonErrorHandler) // final JSON response but keep in mind that the caller should NOT modify that JSON options
// srv.Use(func(ctx iris.Context) { // value in another goroutine while JSON method is still running.
// ctx.SetJSONOptions(iris.JSON{
// ErrorHandler: errHandler,
// })
// ctx.Next()
// })
func (ctx *Context) SetJSONOptions(opts JSON) {
ctx.values.Set(jsonOptionsContextKey, opts)
}
func (ctx *Context) getJSONOptions() (JSON, bool) {
if v := ctx.values.Get(jsonOptionsContextKey); v != nil {
opts, ok := v.(JSON)
return opts, ok
}
return DefaultJSONOptions, false
}
// 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.
func (ctx *Context) JSON(v interface{}, opts ...JSON) (n int, err error) { func (ctx *Context) JSON(v interface{}, opts ...JSON) (n int, err error) {
if n, err = ctx.writeJSON(v, opts...); err != nil { var options *JSON
if opts, ok := ctx.getJSONOptions(); ok { if len(opts) > 0 {
opts.ErrorHandler.HandleContextError(ctx, err) options = &opts[0]
} // keep the error so the caller has control over further actions. }
if n, err = ctx.writeJSON(v, options); err != nil {
ctx.handleContextError(err)
} }
return 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) ctx.ContentType(ContentJSONHeaderValue)
shouldOptimize := ctx.shouldOptimize()
options := DefaultJSONOptions // After content type given and before everything else, try handle proto or easyjson, no matter the performance mode.
optsLength := len(opts) if handled, n, err := ctx.handleSpecialJSONResponseValue(v, options); handled {
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 return n, err
} }
// as soon as possible, use the fast json marshaler with the http request context. shouldOptimize := ctx.shouldOptimize()
if options == nil {
if shouldOptimize {
// 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) result, err := gojson.MarshalContext(ctx.request.Context(), v)
if err != nil { if err != nil {
return 0, err return 0, err
@ -4049,41 +4047,11 @@ func (ctx *Context) writeJSON(v interface{}, opts ...JSON) (n int, err error) {
return ctx.Write(result) return ctx.Write(result)
} }
if options.StreamingJSON { // Else if no options given neither optimizations are enabled, then safely read the already-initialized object.
if shouldOptimize { options = &DefaultJSONOptions
// 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 err != nil { return WriteJSON(ctx, ctx.writer, v, options, shouldOptimize)
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
}
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
} }
var finishCallbackB = []byte(");") var finishCallbackB = []byte(");")
@ -4134,24 +4102,23 @@ func WriteJSONP(writer io.Writer, v interface{}, options JSONP, optimize bool) (
// inside `ctx.JSONP`. // inside `ctx.JSONP`.
var DefaultJSONPOptions = JSONP{} var DefaultJSONPOptions = JSONP{}
// JSONP marshals the given interface object and writes the JSON response to the client. // JSONP marshals the given "v" value to JSON and sends the response to the client.
func (ctx *Context) JSONP(v interface{}, opts ...JSONP) (int, error) { //
// 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 options := DefaultJSONPOptions
if len(opts) > 0 { if len(opts) > 0 {
options = opts[0] options = opts[0]
} }
ctx.ContentType(ContentJavascriptHeaderValue) ctx.ContentType(ContentJavascriptHeaderValue)
if n, err = WriteJSONP(ctx.writer, v, options, ctx.shouldOptimize()); err != nil {
n, err := WriteJSONP(ctx.writer, v, options, ctx.shouldOptimize()) ctx.handleContextError(err)
if err != nil {
ctx.app.Logger().Debugf("JSONP: %v", err)
ctx.StatusCode(http.StatusInternalServerError)
return 0, err
} }
return n, err return
} }
type xmlMapEntry struct { type xmlMapEntry struct {
@ -4232,29 +4199,28 @@ var DefaultXMLOptions = XML{}
// XML marshals the given interface object and writes the XML response to the client. // 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. // 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 options := DefaultXMLOptions
if len(opts) > 0 { if len(opts) > 0 {
options = opts[0] options = opts[0]
} }
ctx.ContentType(ContentXMLHeaderValue) ctx.ContentType(ContentXMLHeaderValue)
if n, err = WriteXML(ctx.writer, v, options, ctx.shouldOptimize()); err != nil {
n, err := WriteXML(ctx.writer, v, options, ctx.shouldOptimize()) ctx.handleContextError(err)
if err != nil {
ctx.app.Logger().Debugf("XML: %v", err)
ctx.StatusCode(http.StatusInternalServerError)
return 0, err
} }
return n, err return
} }
// Problem writes a JSON or XML problem response. // Problem writes a JSON or XML problem response.
// Order of Problem fields are not always rendered the same. // 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 // but with default ProblemOptions.JSON indent of " " and
// a response content type of "application/problem+json" instead. // 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. // 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) buf := blackfriday.Run(markdownB)
if options.Sanitize { if options.Sanitize {
buf = bluemonday.UGCPolicy().SanitizeBytes(buf) buf = bluemonday.UGCPolicy().SanitizeBytes(buf)
} }
return writer.Write(buf) return writer.Write(buf)
} }
@ -4312,66 +4279,90 @@ func WriteMarkdown(writer io.Writer, markdownB []byte, options Markdown) (int, e
var DefaultMarkdownOptions = Markdown{} var DefaultMarkdownOptions = Markdown{}
// Markdown parses the markdown to html and renders its result to the client. // 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 options := DefaultMarkdownOptions
if len(opts) > 0 { if len(opts) > 0 {
options = opts[0] options = opts[0]
} }
ctx.ContentType(ContentHTMLHeaderValue) 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 { if err != nil {
ctx.app.Logger().Debugf("Markdown: %v", err) ctx.handleContextError(err)
ctx.StatusCode(http.StatusInternalServerError)
return 0, err return 0, err
} }
ctx.ContentType(ContentYAMLHeaderValue)
n, err := ctx.Write(out)
if err != nil {
ctx.handleContextError(err)
}
return n, err return n, err
} }
// YAML marshals the "v" using the yaml marshaler // TextYAML calls the Context.YAML method but with the text/yaml content type instead.
// 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.
func (ctx *Context) TextYAML(v interface{}) (int, error) { func (ctx *Context) TextYAML(v interface{}) (int, error) {
ctx.contentTypeOnce(ContentYAMLTextHeaderValue, "") ctx.contentTypeOnce(ContentYAMLTextHeaderValue, "")
return ctx.YAML(v) 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) { func (ctx *Context) Protobuf(v proto.Message) (int, error) {
out, err := proto.Marshal(v) out, err := proto.Marshal(v)
if err != nil { if err != nil {
ctx.handleContextError(err)
return 0, err return 0, err
} }
ctx.ContentType(ContentProtobufHeaderValue) ctx.ContentType(ContentProtobufHeaderValue)
return ctx.Write(out) n, err := ctx.Write(out)
if err != nil {
ctx.handleContextError(err)
} }
// MsgPack parses the "v" of msgpack format and renders its result to the client. return n, err
}
// 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) { func (ctx *Context) MsgPack(v interface{}) (int, error) {
out, err := msgpack.Marshal(v) out, err := msgpack.Marshal(v)
if err != nil { if err != nil {
return 0, err ctx.handleContextError(err)
} }
ctx.ContentType(ContentMsgPackHeaderValue) ctx.ContentType(ContentMsgPackHeaderValue)
return ctx.Write(out) n, err := ctx.Write(out)
if err != nil {
ctx.handleContextError(err)
}
return n, err
} }
// +-----------------------------------------------------------------------+ // +-----------------------------------------------------------------------+

24
iris.go
View File

@ -59,6 +59,8 @@ type Application struct {
*router.Router *router.Router
router.HTTPErrorHandler // if Router is Downgraded this is nil. router.HTTPErrorHandler // if Router is Downgraded this is nil.
ContextPool *context.Pool ContextPool *context.Pool
// See SetContextErrorHandler, defaults to nil.
contextErrorHandler context.ErrorHandler
// config contains the configuration fields // config contains the configuration fields
// all fields defaults to something that is working, developers don't have to set it. // 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 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 // ConfigureHost accepts one or more `host#Configuration`, these configurators functions
// can access the host created by `app.Run` or `app.Listen`, // 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. // they're being executed when application is ready to being served to the public.