1
0
mirror of https://github.com/kataras/iris.git synced 2025-12-20 03:17:04 +00:00

new Application.SetContextErrorHandler method

This commit is contained in:
Gerasimos (Makis) Maropoulos
2022-04-13 01:00:53 +03:00
parent 73dfabf412
commit ae828d8db9
7 changed files with 314 additions and 243 deletions

View File

@@ -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
}
// +-----------------------------------------------------------------------+