forked from go-fuego/fuego
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy patherrors.go
More file actions
273 lines (212 loc) · 7.63 KB
/
Copy patherrors.go
File metadata and controls
273 lines (212 loc) · 7.63 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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
package fuego
import (
"context"
"errors"
"log/slog"
"net/http"
"strconv"
"strings"
)
// ErrorWithStatus is an interface that can be implemented by an error to provide
// a status code
type ErrorWithStatus interface {
error
StatusCode() int
}
// ErrorWithDetail is an interface that can be implemented by an error to provide
// an additional detail message about the error
type ErrorWithDetail interface {
error
DetailMsg() string
}
type ErrorWithTitle interface {
error
ErrorTitle() string
}
// HTTPError is the error response used by the serialization part of the framework.
type HTTPError struct {
// Developer readable error message. Not shown to the user to avoid security leaks.
Err error `json:"-" xml:"-" yaml:"-"`
// URL of the error type. Can be used to lookup the error in a documentation
Type string `json:"type,omitempty" xml:"type,omitempty" yaml:"type,omitempty" description:"URL of the error type. Can be used to lookup the error in a documentation"`
// Short title of the error
Title string `json:"title,omitempty" xml:"title,omitempty" yaml:"title,omitempty" description:"Short title of the error"`
// HTTP status code. If using a different type than [HTTPError], for example [BadRequestError], this will be automatically overridden after Fuego error handling.
Status int `json:"status,omitempty" xml:"status,omitempty" yaml:"status,omitempty" description:"HTTP status code" example:"403"`
// Human readable error message
Detail string `json:"detail,omitempty" xml:"detail,omitempty" yaml:"detail,omitempty" description:"Human readable error message"`
Instance string `json:"instance,omitempty" xml:"instance,omitempty" yaml:"instance,omitempty"`
Errors []ErrorItem `json:"errors,omitempty" xml:"errors,omitempty" yaml:"errors,omitempty"`
}
type ErrorItem struct {
More map[string]any `json:"more,omitempty" xml:"more,omitempty" description:"Additional information about the error"`
Name string `json:"name" xml:"name" description:"For example, name of the parameter that caused the error"`
Reason string `json:"reason" xml:"reason" description:"Human readable error message"`
}
// PublicError returns a human readable error message.
// It ignores the underlying error for security and only returns the status code, title and detail.
func (e HTTPError) PublicError() string {
var msgBuilder strings.Builder
code := e.StatusCode()
msgBuilder.WriteString(strconv.Itoa(code))
title := e.Title
if title == "" {
title = http.StatusText(code)
if title == "" {
title = "HTTP Error"
}
}
msgBuilder.WriteString(" ")
msgBuilder.WriteString(title)
if e.Detail != "" {
msgBuilder.WriteString(" (")
msgBuilder.WriteString(e.Detail)
msgBuilder.WriteString(")")
}
return msgBuilder.String()
}
func (e HTTPError) Error() string {
msg := e.PublicError()
if e.Err != nil {
msg = msg + ": " + e.Err.Error()
}
return msg
}
func (e HTTPError) StatusCode() int {
if e.Status == 0 {
return http.StatusInternalServerError
}
return e.Status
}
func (e HTTPError) DetailMsg() string {
return e.Detail
}
func (e HTTPError) ErrorTitle() string { return e.Title }
func (e HTTPError) Unwrap() error { return e.Err }
// BadRequestError is an error used to return a 400 status code.
type BadRequestError HTTPError
var _ ErrorWithStatus = BadRequestError{}
func (e BadRequestError) Error() string {
e.Status = http.StatusBadRequest
return HTTPError(e).Error()
}
func (e BadRequestError) StatusCode() int { return http.StatusBadRequest }
func (e BadRequestError) Unwrap() error { return HTTPError(e) }
// NotFoundError is an error used to return a 404 status code.
type NotFoundError HTTPError
var _ ErrorWithStatus = NotFoundError{}
func (e NotFoundError) Error() string {
e.Status = http.StatusNotFound
return HTTPError(e).Error()
}
func (e NotFoundError) StatusCode() int { return http.StatusNotFound }
func (e NotFoundError) Unwrap() error { return HTTPError(e) }
// UnauthorizedError is an error used to return a 401 status code.
type UnauthorizedError HTTPError
var _ ErrorWithStatus = UnauthorizedError{}
func (e UnauthorizedError) Error() string {
e.Status = http.StatusUnauthorized
return HTTPError(e).Error()
}
func (e UnauthorizedError) StatusCode() int { return http.StatusUnauthorized }
func (e UnauthorizedError) Unwrap() error { return HTTPError(e) }
// ForbiddenError is an error used to return a 403 status code.
type ForbiddenError HTTPError
var _ ErrorWithStatus = ForbiddenError{}
func (e ForbiddenError) Error() string {
e.Status = http.StatusForbidden
return HTTPError(e).Error()
}
func (e ForbiddenError) StatusCode() int { return http.StatusForbidden }
func (e ForbiddenError) Unwrap() error { return HTTPError(e) }
// ConflictError is an error used to return a 409 status code.
type ConflictError HTTPError
var _ ErrorWithStatus = ConflictError{}
func (e ConflictError) Error() string {
e.Status = http.StatusConflict
return HTTPError(e).Error()
}
func (e ConflictError) StatusCode() int { return http.StatusConflict }
func (e ConflictError) Unwrap() error { return HTTPError(e) }
// InternalServerError is an error used to return a 500 status code.
type InternalServerError = HTTPError
// NotAcceptableError is an error used to return a 406 status code.
type NotAcceptableError HTTPError
var _ ErrorWithStatus = NotAcceptableError{}
func (e NotAcceptableError) Error() string {
e.Status = http.StatusNotAcceptable
return HTTPError(e).Error()
}
func (e NotAcceptableError) StatusCode() int { return http.StatusNotAcceptable }
func (e NotAcceptableError) Unwrap() error { return HTTPError(e) }
// ErrorHandler is the default error handler used by the framework.
// If the error is an [HTTPError] that error is returned.
// If the error adheres to the [ErrorWithStatus] interface
// the error is transformed to a [HTTPError] using [HandleHTTPError].
// If the error is not an [HTTPError] nor does it adhere to an
// interface the error is returned as is.
func ErrorHandler(ctx context.Context, err error) error {
var errorStatus ErrorWithStatus
switch {
case errors.As(err, &HTTPError{}),
errors.As(err, &errorStatus):
return HandleHTTPError(ctx, err)
}
slog.ErrorContext(ctx, "Error in controller", "error", err.Error())
return err
}
// HandleHTTPError is the core logic
// of handling fuego [HTTPError]'s. This
// function takes any error and coerces it into a fuego HTTPError.
// This can be used override the default handler:
//
// engine := fuego.NewEngine(
// WithErrorHandler(HandleHTTPError),
// )
//
// or
//
// server := fuego.NewServer(
// fuego.WithEngineOptions(
// fuego.WithErrorHandler(HandleHTTPError),
// ),
// )
func HandleHTTPError(ctx context.Context, err error) error {
errResponse := HTTPError{
Err: err,
}
if v := castHTTPError(err); v != nil {
errResponse = *v
}
// Check status code
var errWithStatus ErrorWithStatus
if errors.As(err, &errWithStatus) {
errResponse.Status = errWithStatus.StatusCode()
}
// Check for detail
var errWithDetail ErrorWithDetail
if errors.As(err, &errWithDetail) {
errResponse.Detail = errWithDetail.DetailMsg()
}
// Check for title
var errWithTitle ErrorWithTitle
if errors.As(err, &errWithTitle) {
errResponse.Title = errWithTitle.ErrorTitle()
}
if errResponse.Title == "" {
errResponse.Title = http.StatusText(errResponse.Status)
}
slog.ErrorContext(ctx, "Error "+errResponse.Title, "status", errResponse.StatusCode(), "detail", errResponse.DetailMsg(), "error", errResponse.Err)
return errResponse
}
func castHTTPError(err error) *HTTPError {
var errorInfo *HTTPError
if errors.As(err, &errorInfo) {
return errorInfo
}
var errorInfoValue HTTPError
if errors.As(err, &errorInfoValue) {
return &errorInfoValue
}
return nil
}