Skip to content

Commit

Permalink
implement Problem Details for HTTP APIs kataras#1335
Browse files Browse the repository at this point in the history
Former-commit-id: ff789b6d535080c88e05c81ab3fb7d9689801ec7
  • Loading branch information
kataras committed Aug 12, 2019
1 parent 5c91440 commit 8e83959
Show file tree
Hide file tree
Showing 5 changed files with 364 additions and 21 deletions.
60 changes: 59 additions & 1 deletion _examples/routing/http-errors/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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: <b>" + ctx.Values().GetString("message") + "</b>")
})

// Catch all error codes [app.OnAnyErrorCode...]

app.Get("/", func(ctx iris.Context) {
ctx.HTML(`Click <a href="/my500">here</a> to fire the 500 status code`)
ctx.HTML(`Click <a href="/my500">here</a> to pretend an HTTP error`)
})

app.Get("/my500", func(ctx iris.Context) {
Expand All @@ -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"))
}
137 changes: 119 additions & 18 deletions context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 |
// +------------------------------------------------------------+
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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(");")
)
Expand Down Expand Up @@ -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.
}
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit 8e83959

Please sign in to comment.