diff --git a/_examples/routing/http-errors/main.go b/_examples/routing/http-errors/main.go index c33a9c0c..cd83feac 100644 --- a/_examples/routing/http-errors/main.go +++ b/_examples/routing/http-errors/main.go @@ -82,5 +82,27 @@ func problemExample(ctx iris.Context) { // Response like JSON but with indent of " " and // content type of "application/problem+json" - ctx.Problem(newProductProblem("product name", "problem error details")) + ctx.Problem(newProductProblem("product name", "problem error details"), iris.ProblemOptions{ + // Optional JSON renderer settings. + JSON: iris.JSON{ + Indent: " ", + }, + // Sets the "Retry-After" response header. + // + // Can accept: + // time.Time for HTTP-Date, + // time.Duration, int64, float64, int for seconds + // or string for date or duration. + // Examples: + // time.Now().Add(5 * time.Minute), + // 300 * time.Second, + // "5m", + // + RetryAfter: 300, + // A function that, if specified, can dynamically set + // retry-after based on the request. Useful for ProblemOptions reusability. + // Overrides the RetryAfter field. + // + // RetryAfterFunc: func(iris.Context) interface{} { [...] } + }) } diff --git a/context/context.go b/context/context.go index 46ba63c5..c1fac518 100644 --- a/context/context.go +++ b/context/context.go @@ -779,13 +779,14 @@ type Context interface { // JSON marshals the given interface object and writes the JSON response. JSON(v interface{}, options ...JSON) (int, error) // Problem writes a JSON problem response. - // Order of fields are not always the same. + // Order of Problem fields are not always rendered the same. // // Behaves exactly like `Context.JSON` - // but with indent of " " and a content type of "application/problem+json" instead. + // but with default ProblemOptions.JSON indent of " " and + // a response content type of "application/problem+json" instead. // // Read more at: https://github.com/kataras/iris/wiki/Routing-error-handlers - Problem(v interface{}, opts ...JSON) (int, error) + Problem(v interface{}, opts ...ProblemOptions) (int, error) // JSONP marshals the given interface object and writes the JSON response. JSONP(v interface{}, options ...JSONP) (int, error) // XML marshals the given interface object and writes the XML response. @@ -3187,18 +3188,21 @@ func (ctx *context) JSON(v interface{}, opts ...JSON) (n int, err error) { } // Problem writes a JSON problem response. -// Order of fields are not always the same. +// Order of Problem fields are not always rendered the same. // // Behaves exactly like `Context.JSON` -// but with indent of " " and a content type of "application/problem+json" instead. +// but with default ProblemOptions.JSON indent of " " and +// a response content type of "application/problem+json" instead. // // Read more at: https://github.com/kataras/iris/wiki/Routing-error-handlers -func (ctx *context) Problem(v interface{}, opts ...JSON) (int, error) { - options := DefaultJSONOptions +func (ctx *context) Problem(v interface{}, opts ...ProblemOptions) (int, error) { + options := DefaultProblemOptions if len(opts) > 0 { options = opts[0] - } else { - options.Indent = " " + // Currently apply only if custom options passsed, otherwise, + // with the current settings, it's not required. + // This may change in the future though. + options.Apply(ctx) } if p, ok := v.(Problem); ok { @@ -3209,7 +3213,7 @@ func (ctx *context) Problem(v interface{}, opts ...JSON) (int, error) { ctx.contentTypeOnce(ContentJSONProblemHeaderValue, "") - return ctx.JSON(v, options) + return ctx.JSON(v, options.JSON) } var ( diff --git a/context/problem.go b/context/problem.go index 6f3cf9a7..25b61a00 100644 --- a/context/problem.go +++ b/context/problem.go @@ -2,7 +2,10 @@ package context import ( "fmt" + "math" "net/http" + "strconv" + "time" ) // Problem Details for HTTP APIs. @@ -84,6 +87,28 @@ func (p Problem) updateTypeToAbsolute(ctx Context) { } } +const ( + problemTempKeyPrefix = "@temp_" +) + +// TempKey sets a temporary key-value pair, which is being removed +// on the its first get. +func (p Problem) TempKey(key string, value interface{}) Problem { + return p.Key(problemTempKeyPrefix+key, value) +} + +// GetTempKey returns the temp value based on "key" and removes it. +func (p Problem) GetTempKey(key string) interface{} { + key = problemTempKeyPrefix + key + v, ok := p[key] + if ok { + delete(p, key) + return v + } + + return nil +} + // Key sets a custom key-value pair. func (p Problem) Key(key string, value interface{}) Problem { p[key] = value @@ -170,3 +195,85 @@ func (p Problem) Error() string { return fmt.Sprintf("[%d] %s", p["status"], p["title"]) } + +// DefaultProblemOptions the default options for `Context.Problem` method. +var DefaultProblemOptions = ProblemOptions{ + JSON: JSON{Indent: " "}, +} + +// ProblemOptions the optional settings when server replies with a Problem. +// See `Context.Problem` method and `Problem` type for more details. +type ProblemOptions struct { + // JSON are the optional JSON renderer options. + JSON JSON + + // RetryAfter sets the Retry-After response header. + // https://tools.ietf.org/html/rfc7231#section-7.1.3 + // The value can be one of those: + // time.Time + // time.Duration for seconds + // int64, int, float64 for seconds + // string for duration string or for datetime string. + // + // Examples: + // time.Now().Add(5 * time.Minute), + // 300 * time.Second, + // "5m", + // 300 + RetryAfter interface{} + // A function that, if specified, can dynamically set + // retry-after based on the request. Useful for ProblemOptions reusability. + // Should return time.Time, time.Duration, int64, int, float64 or string. + // + // Overrides the RetryAfter field. + RetryAfterFunc func(Context) interface{} +} + +func parseDurationToSeconds(dur time.Duration) int64 { + return int64(math.Round(dur.Seconds())) +} + +func (o *ProblemOptions) parseRetryAfter(value interface{}, timeLayout string) string { + // https://tools.ietf.org/html/rfc7231#section-7.1.3 + // Retry-After = HTTP-date / delay-seconds + switch v := value.(type) { + case int64: + return strconv.FormatInt(v, 10) + case int: + return o.parseRetryAfter(int64(v), timeLayout) + case float64: + return o.parseRetryAfter(int64(math.Round(v)), timeLayout) + case time.Time: + return v.Format(timeLayout) + case time.Duration: + return o.parseRetryAfter(parseDurationToSeconds(v), timeLayout) + case string: + dur, err := time.ParseDuration(v) + if err != nil { + t, err := time.Parse(timeLayout, v) + if err != nil { + return "" + } + + return o.parseRetryAfter(t, timeLayout) + } + + return o.parseRetryAfter(parseDurationToSeconds(dur), timeLayout) + } + + return "" +} + +// Apply accepts a Context and applies specific response-time options. +func (o *ProblemOptions) Apply(ctx Context) { + retryAfterHeaderValue := "" + timeLayout := ctx.Application().ConfigurationReadOnly().GetTimeFormat() + + if o.RetryAfterFunc != nil { + retryAfterHeaderValue = o.parseRetryAfter(o.RetryAfterFunc(ctx), timeLayout) + } else if o.RetryAfter != nil { + retryAfterHeaderValue = o.parseRetryAfter(o.RetryAfter, timeLayout) + } + + ctx.Header("Retry-After", retryAfterHeaderValue) +} diff --git a/go19.go b/go19.go index 25ad84a0..00f14b93 100644 --- a/go19.go +++ b/go19.go @@ -56,8 +56,17 @@ type ( // // Read more at: https://github.com/kataras/iris/wiki/Routing-error-handlers // - // It is an alias of `context.Problem` type. + // It is an alias of the `context#Problem` type. Problem = context.Problem + // ProblemOptions the optional settings when server replies with a Problem. + // See `Context.Problem` method and `Problem` type for more details. + // + // It is an alias of the `context#ProblemOptions` type. + ProblemOptions = context.ProblemOptions + // JSON the optional settings for JSON renderer. + // + // It is an alias of the `context#JSON` type. + JSON = context.JSON // Supervisor is a shortcut of the `host#Supervisor`. // Used to add supervisor configurators on common Runners