mirror of
https://github.com/kataras/iris.git
synced 2025-02-02 15:30:36 +01:00
implement Problem Details for HTTP APIs #1335
Former-commit-id: ff789b6d535080c88e05c81ab3fb7d9689801ec7
This commit is contained in:
parent
5c91440e46
commit
8e83959c61
|
@ -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) {
|
||||
|
@ -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"))
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
172
context/problem.go
Normal file
172
context/problem.go
Normal file
|
@ -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"])
|
||||
}
|
10
go19.go
10
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
|
||||
|
|
6
iris.go
6
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,
|
||||
|
|
Loading…
Reference in New Issue
Block a user