Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Problem Details for HTTP APIs (v11.2.5) #1336

Merged
merged 2 commits into from
Aug 12, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ Developers are not forced to upgrade if they don't really need it. Upgrade whene

**How to upgrade**: Open your command-line and execute this command: `go get github.com/kataras/iris@master`.

# Mo, 12 August 2019 | v11.2.5

- [New Feature: Problem Details for HTTP APIs based](https://github.com/kataras/iris/pull/1336)
- [Add Context.AbsoluteURI](https://github.com/kataras/iris/pull/1336/files#diff-15cce7299aae8810bcab9b0bf9a2fdb1R2368)

Commit log: https://github.com/kataras/iris/compare/v11.2.4...v11.2.5

# Fr, 09 August 2019 | v11.2.4

- Fixes [iris.Jet: no view engine found for '.jet' or '.html'](https://github.com/kataras/iris/issues/1327)
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
11.2.4:https://github.com/kataras/iris/releases/tag/v11.2.4
11.2.5:https://github.com/kataras/iris/releases/tag/v11.2.5
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