diff --git a/_examples/routing/http-errors/main.go b/_examples/routing/http-errors/main.go
index c41dfbf4..c33a9c0c 100644
--- a/_examples/routing/http-errors/main.go
+++ b/_examples/routing/http-errors/main.go
@@ -7,12 +7,15 @@ import (
func main() {
app := iris.New()
+ // Catch a specific error code.
app.OnErrorCode(iris.StatusInternalServerError, func(ctx iris.Context) {
ctx.HTML("Message: " + ctx.Values().GetString("message") + "")
})
+ // Catch all error codes [app.OnAnyErrorCode...]
+
app.Get("/", func(ctx iris.Context) {
- ctx.HTML(`Click here to fire the 500 status code`)
+ ctx.HTML(`Click here to pretend an HTTP error`)
})
app.Get("/my500", func(ctx iris.Context) {
@@ -24,5 +27,60 @@ func main() {
ctx.Writef("Hello %s", ctx.Params().Get("firstname"))
})
+ app.Get("/product-problem", problemExample)
+
+ app.Get("/product-error", func(ctx iris.Context) {
+ ctx.Writef("explain the error")
+ })
+
+ // http://localhost:8080
+ // http://localhost:8080/my500
+ // http://localhost:8080/u/gerasimos
+ // http://localhost:8080/product-problem
app.Run(iris.Addr(":8080"))
}
+
+func newProductProblem(productName, detail string) iris.Problem {
+ return iris.NewProblem().
+ // The type URI, if relative it automatically convert to absolute.
+ Type("/product-error").
+ // The title, if empty then it gets it from the status code.
+ Title("Product validation problem").
+ // Any optional details.
+ Detail(detail).
+ // The status error code, required.
+ Status(iris.StatusBadRequest).
+ // Any custom key-value pair.
+ Key("productName", productName)
+ // Optional cause of the problem, chain of Problems.
+ // Cause(iris.NewProblem().Type("/error").Title("cause of the problem").Status(400))
+}
+
+func problemExample(ctx iris.Context) {
+ /*
+ p := iris.NewProblem().
+ Type("/validation-error").
+ Title("Your request parameters didn't validate").
+ Detail("Optional details about the error.").
+ Status(iris.StatusBadRequest).
+ Key("customField1", customValue1)
+ Key("customField2", customValue2)
+ ctx.Problem(p)
+
+ // OR
+ ctx.Problem(iris.Problem{
+ "type": "/validation-error",
+ "title": "Your request parameters didn't validate",
+ "detail": "Optional details about the error.",
+ "status": iris.StatusBadRequest,
+ "customField1": customValue1,
+ "customField2": customValue2,
+ })
+
+ // OR
+ */
+
+ // Response like JSON but with indent of " " and
+ // content type of "application/problem+json"
+ ctx.Problem(newProductProblem("product name", "problem error details"))
+}
diff --git a/context/context.go b/context/context.go
index ee33c9f5..b281690f 100644
--- a/context/context.go
+++ b/context/context.go
@@ -385,6 +385,8 @@ type Context interface {
// Look `StatusCode` too.
GetStatusCode() int
+ // AbsoluteURI parses the "s" and returns its absolute URI form.
+ AbsoluteURI(s string) string
// Redirect sends a redirect response to the client
// to a specific url or relative path.
// accepts 2 parameters string and an optional int
@@ -395,7 +397,6 @@ type Context interface {
// or 303 (StatusSeeOther) if POST method,
// or StatusTemporaryRedirect(307) if that's nessecery.
Redirect(urlToRedirect string, statusHeader ...int)
-
// +------------------------------------------------------------+
// | Various Request and Post Data |
// +------------------------------------------------------------+
@@ -777,6 +778,14 @@ type Context interface {
HTML(format string, args ...interface{}) (int, error)
// 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.
+ //
+ // Behaves exactly like `Context.JSON`
+ // but with indent of " " and a 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)
// 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.
@@ -1612,19 +1621,7 @@ func (ctx *context) IsWWW() bool {
// FullRqeuestURI returns the full URI,
// including the scheme, the host and the relative requested path/resource.
func (ctx *context) FullRequestURI() string {
- scheme := ctx.request.URL.Scheme
- if scheme == "" {
- if ctx.request.TLS != nil {
- scheme = "https:"
- } else {
- scheme = "http:"
- }
- }
-
- host := ctx.Host()
- path := ctx.Path()
-
- return scheme + "//" + host + path
+ return ctx.AbsoluteURI(ctx.Path())
}
const xForwardedForHeaderKey = "X-Forwarded-For"
@@ -2367,6 +2364,59 @@ func uploadTo(fh *multipart.FileHeader, destDirectory string) (int64, error) {
return io.Copy(out, src)
}
+// AbsoluteURI parses the "s" and returns its absolute URI form.
+func (ctx *context) AbsoluteURI(s string) string {
+ if s == "" {
+ return ""
+ }
+
+ if s[0] == '/' {
+ scheme := ctx.request.URL.Scheme
+ if scheme == "" {
+ if ctx.request.TLS != nil {
+ scheme = "https:"
+ } else {
+ scheme = "http:"
+ }
+ }
+
+ host := ctx.Host()
+
+ return scheme + "//" + host + path.Clean(s)
+ }
+
+ if u, err := url.Parse(s); err == nil {
+ r := ctx.request
+
+ if u.Scheme == "" && u.Host == "" {
+ oldpath := r.URL.Path
+ if oldpath == "" {
+ oldpath = "/"
+ }
+
+ if s == "" || s[0] != '/' {
+ olddir, _ := path.Split(oldpath)
+ s = olddir + s
+ }
+
+ var query string
+ if i := strings.Index(s, "?"); i != -1 {
+ s, query = s[:i], s[i:]
+ }
+
+ // clean up but preserve trailing slash
+ trailing := strings.HasSuffix(s, "/")
+ s = path.Clean(s)
+ if trailing && !strings.HasSuffix(s, "/") {
+ s += "/"
+ }
+ s += query
+ }
+ }
+
+ return s
+}
+
// Redirect sends a redirect response to the client
// to a specific url or relative path.
// accepts 2 parameters string and an optional int
@@ -2964,6 +3014,9 @@ const (
ContentHTMLHeaderValue = "text/html"
// ContentJSONHeaderValue header value for JSON data.
ContentJSONHeaderValue = "application/json"
+ // ContentJSONProblemHeaderValue header value for API problem error.
+ // Read more at: https://tools.ietf.org/html/rfc7807
+ ContentJSONProblemHeaderValue = "application/problem+json"
// ContentJavascriptHeaderValue header value for JSONP & Javascript data.
ContentJavascriptHeaderValue = "application/javascript"
// ContentTextHeaderValue header value for Text data.
@@ -3133,6 +3186,30 @@ func (ctx *context) JSON(v interface{}, opts ...JSON) (n int, err error) {
return n, err
}
+// Problem writes a JSON problem response.
+// Order of fields are not always the same.
+//
+// Behaves exactly like `Context.JSON`
+// but with indent of " " and a 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
+ if len(opts) > 0 {
+ options = opts[0]
+ } else {
+ options.Indent = " "
+ }
+
+ ctx.contentTypeOnce(ContentJSONProblemHeaderValue, "")
+
+ if p, ok := v.(Problem); ok {
+ p.updateTypeToAbsolute(ctx)
+ }
+
+ return ctx.JSON(v, options)
+}
+
var (
finishCallbackB = []byte(");")
)
@@ -3333,10 +3410,11 @@ type N struct {
Markdown []byte
Binary []byte
- JSON interface{}
- JSONP interface{}
- XML interface{}
- YAML interface{}
+ JSON interface{}
+ Problem Problem
+ JSONP interface{}
+ XML interface{}
+ YAML interface{}
Other []byte // custom content types.
}
@@ -3354,6 +3432,8 @@ func (n N) SelectContent(mime string) interface{} {
return n.Binary
case ContentJSONHeaderValue:
return n.JSON
+ case ContentJSONProblemHeaderValue:
+ return n.Problem
case ContentJavascriptHeaderValue:
return n.JSONP
case ContentXMLHeaderValue, ContentXMLUnreadableHeaderValue:
@@ -3485,6 +3565,8 @@ func (ctx *context) Negotiate(v interface{}) (int, error) {
return ctx.Markdown(v.([]byte))
case ContentJSONHeaderValue:
return ctx.JSON(v)
+ case ContentJSONProblemHeaderValue:
+ return ctx.Problem(v)
case ContentJavascriptHeaderValue:
return ctx.JSONP(v)
case ContentXMLHeaderValue, ContentXMLUnreadableHeaderValue:
@@ -3614,6 +3696,19 @@ func (n *NegotiationBuilder) JSON(v ...interface{}) *NegotiationBuilder {
return n.MIME(ContentJSONHeaderValue, content)
}
+// Problem registers the "application/problem+json" content type and, optionally,
+// a value that `Context.Negotiate` will render
+// when a client accepts the "application/problem+json" content type.
+//
+// Returns itself for recursive calls.
+func (n *NegotiationBuilder) Problem(v ...interface{}) *NegotiationBuilder {
+ var content interface{}
+ if len(v) > 0 {
+ content = v[0]
+ }
+ return n.MIME(ContentJSONProblemHeaderValue, content)
+}
+
// JSONP registers the "application/javascript" content type and, optionally,
// a value that `Context.Negotiate` will render
// when a client accepts the "application/javascript" content type.
@@ -3867,6 +3962,12 @@ func (n *NegotiationAcceptBuilder) JSON() *NegotiationAcceptBuilder {
return n.MIME(ContentJSONHeaderValue)
}
+// Problem adds the "application/problem+json" as accepted client content type.
+// Returns itself.
+func (n *NegotiationAcceptBuilder) Problem() *NegotiationAcceptBuilder {
+ return n.MIME(ContentJSONProblemHeaderValue)
+}
+
// JSONP adds the "application/javascript" as accepted client content type.
// Returns itself.
func (n *NegotiationAcceptBuilder) JSONP() *NegotiationAcceptBuilder {
diff --git a/context/problem.go b/context/problem.go
new file mode 100644
index 00000000..6f3cf9a7
--- /dev/null
+++ b/context/problem.go
@@ -0,0 +1,172 @@
+package context
+
+import (
+ "fmt"
+ "net/http"
+)
+
+// Problem Details for HTTP APIs.
+// Pass a Problem value to `context.Problem` to
+// write an "application/problem+json" response.
+//
+// Read more at: https://github.com/kataras/iris/wiki/Routing-error-handlers
+type Problem map[string]interface{}
+
+// NewProblem retruns a new Problem.
+// Head over to the `Problem` type godoc for more.
+func NewProblem() Problem {
+ p := make(Problem)
+ return p
+}
+
+func (p Problem) keyExists(key string) bool {
+ if p == nil {
+ return false
+ }
+
+ _, found := p[key]
+ return found
+}
+
+// DefaultProblemStatusCode is being sent to the client
+// when Problem's status is not a valid one.
+var DefaultProblemStatusCode = http.StatusBadRequest
+
+func (p Problem) getStatus() (int, bool) {
+ statusField, found := p["status"]
+ if !found {
+ return DefaultProblemStatusCode, false
+ }
+
+ status, ok := statusField.(int)
+ if !ok {
+ return DefaultProblemStatusCode, false
+ }
+
+ if !StatusCodeNotSuccessful(status) {
+ return DefaultProblemStatusCode, false
+ }
+
+ return status, true
+}
+
+func isEmptyTypeURI(uri string) bool {
+ return uri == "" || uri == "about:blank"
+}
+
+func (p Problem) getType() string {
+ typeField, found := p["type"]
+ if found {
+ if typ, ok := typeField.(string); ok {
+ if !isEmptyTypeURI(typ) {
+ return typ
+ }
+ }
+ }
+
+ return ""
+}
+
+// Updates "type" field to absolute URI, recursively.
+func (p Problem) updateTypeToAbsolute(ctx Context) {
+ if p == nil {
+ return
+ }
+
+ if uriRef := p.getType(); uriRef != "" {
+ p.Type(ctx.AbsoluteURI(uriRef))
+ }
+
+ if cause, ok := p["cause"]; ok {
+ if causeP, ok := cause.(Problem); ok {
+ causeP.updateTypeToAbsolute(ctx)
+ }
+ }
+}
+
+// Key sets a custom key-value pair.
+func (p Problem) Key(key string, value interface{}) Problem {
+ p[key] = value
+ return p
+}
+
+// Type URI SHOULD resolve to HTML [W3C.REC-html5-20141028]
+// documentation that explains how to resolve the problem.
+// Example: "https://example.net/validation-error"
+//
+// Empty URI or "about:blank", when used as a problem type,
+// indicates that the problem has no additional semantics beyond that of the HTTP status code.
+// When "about:blank" is used,
+// the title is being automatically set the same as the recommended HTTP status phrase for that code
+// (e.g., "Not Found" for 404, and so on) on `Status` call.
+//
+// Relative paths are also valid when writing this Problem to an Iris Context.
+func (p Problem) Type(uri string) Problem {
+ return p.Key("type", uri)
+}
+
+// Title sets the problem's title field.
+// Example: "Your request parameters didn't validate."
+// It is set to status Code text if missing,
+// (e.g., "Not Found" for 404, and so on).
+func (p Problem) Title(title string) Problem {
+ return p.Key("title", title)
+}
+
+// Status sets HTTP error code for problem's status field.
+// Example: 404
+//
+// It is required.
+func (p Problem) Status(statusCode int) Problem {
+ shouldOverrideTitle := !p.keyExists("title")
+
+ if !shouldOverrideTitle {
+ typ, found := p["type"]
+ shouldOverrideTitle = !found || isEmptyTypeURI(typ.(string))
+ }
+
+ if shouldOverrideTitle {
+ // Set title by code.
+ p.Title(http.StatusText(statusCode))
+ }
+ return p.Key("status", statusCode)
+}
+
+// Detail sets the problem's detail field.
+// Example: "Optional details about the error...".
+func (p Problem) Detail(detail string) Problem {
+ return p.Key("detail", detail)
+}
+
+// Cause sets the problem's cause field.
+// Any chain of problems.
+func (p Problem) Cause(cause Problem) Problem {
+ if !cause.Validate() {
+ return p
+ }
+
+ return p.Key("cause", cause)
+}
+
+// Validate reports whether this Problem value is a valid problem one.
+func (p Problem) Validate() bool {
+ // A nil problem is not a valid one.
+ if p == nil {
+ return false
+ }
+
+ return p.keyExists("type") &&
+ p.keyExists("title") &&
+ p.keyExists("status")
+}
+
+// Error method completes the go error.
+// Returns the "[Status] Title" string form of this Problem.
+// If Problem is not a valid one, it returns "invalid problem".
+func (p Problem) Error() string {
+ if !p.Validate() {
+ return "invalid problem"
+ }
+
+ return fmt.Sprintf("[%d] %s", p["status"], p["title"])
+}
diff --git a/go19.go b/go19.go
index d98299c5..25ad84a0 100644
--- a/go19.go
+++ b/go19.go
@@ -48,8 +48,16 @@ type (
// See `NewConditionalHandler` for more.
// An alias for the `context/Filter`.
Filter = context.Filter
- // A Map is a shortcut of the map[string]interface{}.
+ // A Map is an alias of map[string]interface{}.
Map = context.Map
+ // Problem Details for HTTP APIs.
+ // Pass a Problem value to `context.Problem` to
+ // write an "application/problem+json" response.
+ //
+ // Read more at: https://github.com/kataras/iris/wiki/Routing-error-handlers
+ //
+ // It is an alias of `context.Problem` type.
+ Problem = context.Problem
// Supervisor is a shortcut of the `host#Supervisor`.
// Used to add supervisor configurators on common Runners
diff --git a/iris.go b/iris.go
index 5db0d927..507ef2f8 100644
--- a/iris.go
+++ b/iris.go
@@ -453,7 +453,6 @@ var (
//
// A shortcut of the `cache#Cache304`.
Cache304 = cache.Cache304
-
// CookiePath is a `CookieOption`.
// Use it to change the cookie's Path field.
//
@@ -499,6 +498,11 @@ var (
//
// A shortcut for the `context#IsErrPath`.
IsErrPath = context.IsErrPath
+ // NewProblem retruns a new Problem.
+ // Head over to the `Problem` type godoc for more.
+ //
+ // A shortcut for the `context#NewProblem`.
+ NewProblem = context.NewProblem
)
// Contains the enum values of the `Context.GetReferrer()` method,