-
-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Expand file tree
/
Copy pathrfc9457.go
More file actions
133 lines (120 loc) · 4.59 KB
/
Copy pathrfc9457.go
File metadata and controls
133 lines (120 loc) · 4.59 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package echo
import (
"errors"
"fmt"
"net/http"
)
// ProblemError represents a "problem detail" as defined in RFC 9457 (Problem Details for
// HTTP APIs). https://www.rfc-editor.org/rfc/rfc9457
type ProblemError struct {
// Type is a URI reference that identifies the problem type. Defaults to "about:blank"
// when empty, which means the problem is the HTTP status code itself.
Type string `json:"type"`
// Title is a short, human-readable summary of the problem type. Defaults to the status
// text of Status when empty.
Title string `json:"title"`
// Status is the HTTP status code for this occurrence of the problem.
Status int `json:"status"`
// Detail is a human-readable explanation specific to this occurrence of the problem.
Detail string `json:"detail,omitempty"`
// Instance is a URI reference that identifies the specific occurrence of the problem.
Instance string `json:"instance,omitempty"`
}
// Error makes ProblemError compatible with the `error` interface.
func (pe *ProblemError) Error() string {
msg := pe.Title
if pe.Detail != "" {
msg = fmt.Sprintf("%v: %v", pe.Title, pe.Detail)
}
return fmt.Sprintf("code=%d, message=%v", pe.Status, msg)
}
// StatusCode returns status code for HTTP response, implementing HTTPStatusCoder interface.
func (pe *ProblemError) StatusCode() int {
return pe.Status
}
// ProblemErrorer is the interface that custom error types can implement so they can be
// converted into a *ProblemError by ProblemDetailsHTTPErrorHandler.
type ProblemErrorer interface {
ProblemError() *ProblemError
}
// ProblemDetailsHTTPErrorHandler creates a new HTTP error handler that converts every error
// into a RFC 9457 (Problem Details for HTTP APIs) response and sends it with
// `application/problem+json` content type. `exposeError` parameter decides if the returned
// problem detail will contain the underlying error message for errors that are not *HTTPError
// or do not implement ProblemErrorer.
//
// Precedence used to build the response for a given error:
// 1. If err (or any error wrapped by it) is a *ProblemError, it is used as-is.
// 2. Else if err (or any error wrapped by it) implements ProblemErrorer, ProblemError() is used.
// 3. Else a *ProblemError is built from err's HTTPStatusCoder status code (defaulting to 500).
// For *HTTPError, Detail is set to its Message, further extended with the wrapped error's
// message when exposeError is true. For any other error, Detail is only populated (with
// err.Error()) when exposeError is true.
//
// Any zero-value Type, Title or Status field is defaulted to "about:blank", the status text
// of Status, and 500 respectively.
//
// Note: ProblemDetailsHTTPErrorHandler does not log errors. Use middleware for it if errors
// need to be logged (separately).
func ProblemDetailsHTTPErrorHandler(exposeError bool) HTTPErrorHandler {
return func(c *Context, err error) {
if r, _ := UnwrapResponse(c.response); r != nil && r.Committed {
return
}
var pe *ProblemError
var pder ProblemErrorer
switch {
case errors.As(err, &pe):
case errors.As(err, &pder):
if pe = pder.ProblemError(); pe == nil {
pe = &ProblemError{}
}
default:
pe = &ProblemError{}
var sc HTTPStatusCoder
if errors.As(err, &sc) {
pe.Status = sc.StatusCode()
}
var he *HTTPError
if errors.As(err, &he) {
pe.Detail = he.Message
if exposeError {
if wrapped := he.Unwrap(); wrapped != nil {
if pe.Detail == "" {
pe.Detail = wrapped.Error()
} else {
pe.Detail = fmt.Sprintf("%s: %s", pe.Detail, wrapped.Error())
}
}
}
} else if exposeError {
pe.Detail = err.Error()
}
}
// The problem can be a value the application reuses, for example a package
// level sentinel error, so the defaults are applied to a copy. Writing them
// back would race between requests and would permanently alter the error.
problem := *pe
if problem.Status == 0 {
problem.Status = http.StatusInternalServerError
}
if problem.Type == "" {
problem.Type = "about:blank"
}
if problem.Title == "" {
problem.Title = http.StatusText(problem.Status)
}
c.Response().Header().Set(HeaderContentType, MIMEApplicationProblemJSON)
var cErr error
if c.Request().Method == http.MethodHead { // Issue #608
cErr = c.NoContent(problem.Status)
} else {
cErr = c.JSON(problem.Status, &problem)
}
if cErr != nil {
c.Logger().Error("echo RFC 9457 error handler failed to send error to client", "error", cErr) // truly rare case. ala client already disconnected
}
}
}