forked from kataras/iris
-
Notifications
You must be signed in to change notification settings - Fork 0
/
errors.go
319 lines (276 loc) · 11.3 KB
/
errors.go
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
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
package errors
import (
"encoding/json"
"fmt"
"net/http"
"github.com/kataras/iris/v12/context"
"github.com/kataras/iris/v12/x/client"
)
// LogErrorFunc is an alias of a function type which accepts the Iris request context and an error
// and it's fired whenever an error should be logged.
//
// See "OnErrorLog" variable to change the way an error is logged,
// by default the error is logged using the Application's Logger's Error method.
type LogErrorFunc = func(ctx *context.Context, err error)
// LogError can be modified to customize the way an error is logged to the server (most common: internal server errors, database errors et.c.).
// Can be used to customize the error logging, e.g. using Sentry (cloud-based error console).
var LogError LogErrorFunc = func(ctx *context.Context, err error) {
ctx.Application().Logger().Error(err)
}
// SkipCanceled is a package-level setting which by default
// skips the logging of a canceled response or operation.
// See the "Context.IsCanceled()" method and "iris.IsCanceled()" function
// that decide if the error is caused by a canceled operation.
//
// Change of this setting MUST be done on initialization of the program.
var SkipCanceled = true
type (
// ErrorCodeName is a custom string type represents canonical error names.
//
// It contains functionality for safe and easy error populating.
// See its "Message", "Details", "Data" and "Log" methods.
ErrorCodeName string
// ErrorCode represents the JSON form ErrorCode of the Error.
ErrorCode struct {
CanonicalName ErrorCodeName `json:"canonical_name" yaml:"CanonicalName"`
Status int `json:"status" yaml:"Status"`
}
)
// A read-only map of valid http error codes.
var errorCodeMap = make(map[ErrorCodeName]ErrorCode)
// E registers a custom HTTP Error and returns its canonical name for future use.
// The method "New" is reserved and was kept as it is for compatibility
// with the standard errors package, therefore the "E" name was chosen instead.
// The key stroke "e" is near and accessible while typing the "errors" word
// so developers may find it easy to use.
//
// See "RegisterErrorCode" and "RegisterErrorCodeMap" for alternatives.
//
// Example:
//
// var (
// NotFound = errors.E("NOT_FOUND", http.StatusNotFound)
// )
// ...
// NotFound.Details(ctx, "resource not found", "user with id: %q was not found", userID)
//
// This method MUST be called on initialization, before HTTP server starts as
// the internal map is not protected by mutex.
func E(httpErrorCanonicalName string, httpStatusCode int) ErrorCodeName {
canonicalName := ErrorCodeName(httpErrorCanonicalName)
RegisterErrorCode(canonicalName, httpStatusCode)
return canonicalName
}
// RegisterErrorCode registers a custom HTTP Error.
//
// This method MUST be called on initialization, before HTTP server starts as
// the internal map is not protected by mutex.
func RegisterErrorCode(canonicalName ErrorCodeName, httpStatusCode int) {
errorCodeMap[canonicalName] = ErrorCode{
CanonicalName: canonicalName,
Status: httpStatusCode,
}
}
// RegisterErrorCodeMap registers one or more custom HTTP Errors.
//
// This method MUST be called on initialization, before HTTP server starts as
// the internal map is not protected by mutex.
func RegisterErrorCodeMap(errorMap map[ErrorCodeName]int) {
if len(errorMap) == 0 {
return
}
for canonicalName, httpStatusCode := range errorMap {
RegisterErrorCode(canonicalName, httpStatusCode)
}
}
// List of default error codes a server should follow and send back to the client.
var (
Cancelled ErrorCodeName = E("CANCELLED", context.StatusTokenRequired)
Unknown ErrorCodeName = E("UNKNOWN", http.StatusInternalServerError)
InvalidArgument ErrorCodeName = E("INVALID_ARGUMENT", http.StatusBadRequest)
DeadlineExceeded ErrorCodeName = E("DEADLINE_EXCEEDED", http.StatusGatewayTimeout)
NotFound ErrorCodeName = E("NOT_FOUND", http.StatusNotFound)
AlreadyExists ErrorCodeName = E("ALREADY_EXISTS", http.StatusConflict)
PermissionDenied ErrorCodeName = E("PERMISSION_DENIED", http.StatusForbidden)
Unauthenticated ErrorCodeName = E("UNAUTHENTICATED", http.StatusUnauthorized)
ResourceExhausted ErrorCodeName = E("RESOURCE_EXHAUSTED", http.StatusTooManyRequests)
FailedPrecondition ErrorCodeName = E("FAILED_PRECONDITION", http.StatusBadRequest)
Aborted ErrorCodeName = E("ABORTED", http.StatusConflict)
OutOfRange ErrorCodeName = E("OUT_OF_RANGE", http.StatusBadRequest)
Unimplemented ErrorCodeName = E("UNIMPLEMENTED", http.StatusNotImplemented)
Internal ErrorCodeName = E("INTERNAL", http.StatusInternalServerError)
Unavailable ErrorCodeName = E("UNAVAILABLE", http.StatusServiceUnavailable)
DataLoss ErrorCodeName = E("DATA_LOSS", http.StatusInternalServerError)
)
// Message sends an error with a simple message to the client.
func (e ErrorCodeName) Message(ctx *context.Context, format string, args ...interface{}) {
fail(ctx, e, sprintf(format, args...), "", nil, nil)
}
// Details sends an error with a message and details to the client.
func (e ErrorCodeName) Details(ctx *context.Context, msg, details string, detailsArgs ...interface{}) {
fail(ctx, e, msg, sprintf(details, detailsArgs...), nil, nil)
}
// Data sends an error with a message and json data to the client.
func (e ErrorCodeName) Data(ctx *context.Context, msg string, data interface{}) {
fail(ctx, e, msg, "", nil, data)
}
// DataWithDetails sends an error with a message, details and json data to the client.
func (e ErrorCodeName) DataWithDetails(ctx *context.Context, msg, details string, data interface{}) {
fail(ctx, e, msg, details, nil, data)
}
// Validation sends an error which renders the invalid fields to the client.
func (e ErrorCodeName) Validation(ctx *context.Context, validationErrors ...ValidationError) {
e.validation(ctx, validationErrors)
}
func (e ErrorCodeName) validation(ctx *context.Context, validationErrors interface{}) {
fail(ctx, e, "validation failure", "fields were invalid", validationErrors, nil)
}
// Err sends the error's text as a message to the client.
// In exception, if the given "err" is a type of validation error
// then the Validation method is called instead.
func (e ErrorCodeName) Err(ctx *context.Context, err error) {
if err == nil {
return
}
if validationErrors, ok := AsValidationErrors(err); ok {
e.validation(ctx, validationErrors)
return
}
e.Message(ctx, err.Error())
}
// Log sends an error of "format" and optional "args" to the client and prints that
// error using the "LogError" package-level function, which can be customized.
//
// See "LogErr" too.
func (e ErrorCodeName) Log(ctx *context.Context, format string, args ...interface{}) {
if SkipCanceled {
if ctx.IsCanceled() {
return
}
for _, arg := range args {
if err, ok := arg.(error); ok {
if context.IsErrCanceled(err) {
return
}
}
}
}
err := fmt.Errorf(format, args...)
e.LogErr(ctx, err)
}
// LogErr sends the given "err" as message to the client and prints that
// error to using the "LogError" package-level function, which can be customized.
func (e ErrorCodeName) LogErr(ctx *context.Context, err error) {
if SkipCanceled && (ctx.IsCanceled() || context.IsErrCanceled(err)) {
return
}
LogError(ctx, err)
e.Message(ctx, "server error")
}
// HandleAPIError handles remote server errors.
// Optionally, use it when you write your server's HTTP clients using the the /x/client package.
// When the HTTP Client sends data to a remote server but that remote server
// failed to accept the request as expected, then the error will be proxied
// to this server's end-client.
//
// When the given "err" is not a type of client.APIError then
// the error will be sent using the "Internal.LogErr" method which sends
// HTTP internal server error to the end-client and
// prints the "err" using the "LogError" package-level function.
func HandleAPIError(ctx *context.Context, err error) {
// Error expected and came from the external server,
// save its body so we can forward it to the end-client.
if apiErr, ok := client.GetError(err); ok {
statusCode := apiErr.Response.StatusCode
if statusCode >= 400 && statusCode < 500 {
InvalidArgument.DataWithDetails(ctx, "remote server error", "invalid client request", apiErr.Body)
} else {
Internal.Data(ctx, "remote server error", apiErr.Body)
}
// Unavailable.DataWithDetails(ctx, "remote server error", "unavailable", apiErr.Body)
return
}
Internal.LogErr(ctx, err)
}
var (
// ErrUnexpected is the HTTP error which sent to the client
// when server fails to send an error, it's a fallback error.
// The server fails to send an error on two cases:
// 1. when the provided error code name is not registered (the error value is the ErrUnexpectedErrorCode)
// 2. when the error contains data but cannot be encoded to json (the value of the error is the result error of json.Marshal).
ErrUnexpected = E("UNEXPECTED_ERROR", http.StatusInternalServerError)
// ErrUnexpectedErrorCode is the error which logged
// when the given error code name is not registered.
ErrUnexpectedErrorCode = New("unexpected error code name")
)
// Error represents the JSON form of "http wire errors".
//
// Examples can be found at:
//
// https://github.com/kataras/iris/tree/master/_examples/routing/http-wire-errors.
type Error struct {
ErrorCode ErrorCode `json:"http_error_code" yaml:"HTTPErrorCode"`
Message string `json:"message,omitempty" yaml:"Message"`
Details string `json:"details,omitempty" yaml:"Details"`
Validation interface{} `json:"validation,omitempty" yaml:"Validation,omitempty"`
Data json.RawMessage `json:"data,omitempty" yaml:"Data,omitempty"` // any other custom json data.
}
// Error method completes the error interface. It just returns the canonical name, status code, message and details.
func (err *Error) Error() string {
if err.Message == "" {
err.Message = "<empty>"
}
if err.Details == "" {
err.Details = "<empty>"
}
if err.ErrorCode.CanonicalName == "" {
err.ErrorCode.CanonicalName = ErrUnexpected
}
if err.ErrorCode.Status <= 0 {
err.ErrorCode.Status = http.StatusInternalServerError
}
return sprintf("iris http wire error: canonical name: %s, http status code: %d, message: %s, details: %s", err.ErrorCode.CanonicalName, err.ErrorCode.Status, err.Message, err.Details)
}
func fail(ctx *context.Context, codeName ErrorCodeName, msg, details string, validationErrors interface{}, dataValue interface{}) {
errorCode, ok := errorCodeMap[codeName]
if !ok {
// This SHOULD NEVER happen, all ErrorCodeNames MUST be registered.
LogError(ctx, ErrUnexpectedErrorCode)
fail(ctx, ErrUnexpected, msg, details, validationErrors, dataValue)
return
}
var data json.RawMessage
if dataValue != nil {
switch v := dataValue.(type) {
case json.RawMessage:
data = v
case []byte:
data = v
case error:
if msg == "" {
msg = v.Error()
} else if details == "" {
details = v.Error()
} else {
data = json.RawMessage(v.Error())
}
default:
b, err := json.Marshal(v)
if err != nil {
LogError(ctx, err)
fail(ctx, ErrUnexpected, err.Error(), "", nil, nil)
return
}
data = b
}
}
err := Error{
ErrorCode: errorCode,
Message: msg,
Details: details,
Data: data,
Validation: validationErrors,
}
// ctx.SetErr(&err)
ctx.StopWithJSON(errorCode.Status, err)
}