forked from twitchtv/twirp
-
Notifications
You must be signed in to change notification settings - Fork 0
/
errors.go
428 lines (370 loc) · 15.2 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
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
// Copyright 2018 Twitch Interactive, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"). You may not
// use this file except in compliance with the License. A copy of the License is
// located at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// or in the "license" file accompanying this file. This file is distributed on
// an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
// express or implied. See the License for the specific language governing
// permissions and limitations under the License.
// Package twirp provides core types used in generated Twirp servers and client.
//
// Twirp services handle errors using the `twirp.Error` interface.
//
// For example, a server method may return an InvalidArgumentError:
//
// if req.Order != "DESC" && req.Order != "ASC" {
// return nil, twirp.InvalidArgumentError("Order", "must be DESC or ASC")
// }
//
// And the same twirp.Error is returned by the client, for example:
//
// resp, err := twirpClient.RPCMethod(ctx, req)
// if err != nil {
// if twerr, ok := err.(twirp.Error); ok {
// switch twerr.Code() {
// case twirp.InvalidArgument:
// log.Error("invalid argument "+twirp.Meta("argument"))
// default:
// log.Error(twerr.Error())
// }
// }
// }
//
// Clients may also return Internal errors if something failed on the system:
// the server, the network, or the client itself (i.e. failure parsing
// response).
//
package twirp
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"strconv"
)
// Error represents an error in a Twirp service call.
type Error interface {
// Code is of the valid error codes.
Code() ErrorCode
// Msg returns a human-readable, unstructured messages describing the error.
Msg() string
// WithMeta returns a copy of the Error with the given key-value pair attached
// as metadata. If the key is already set, it is overwritten.
WithMeta(key string, val string) Error
// Meta returns the stored value for the given key. If the key has no set
// value, Meta returns an empty string. There is no way to distinguish between
// an unset value and an explicit empty string.
Meta(key string) string
// MetaMap returns the complete key-value metadata map stored on the error.
MetaMap() map[string]string
// Error returns a string of the form "twirp error <Code>: <Msg>"
Error() string
}
// code.Error(msg) builds a new Twirp error with code and msg. Example:
// twirp.NotFound.Error("Resource not found")
// twirp.Internal.Error("Oops")
func (code ErrorCode) Error(msg string) Error {
return NewError(code, msg)
}
// code.Errorf(msg, args...) builds a new Twirp error with code and formatted msg.
// The format may include "%w" to wrap other errors. Examples:
// twirp.Internal.Error("Oops: %w", originalErr)
// twirp.NotFound.Error("Resource not found with id: %q", resourceID)
func (code ErrorCode) Errorf(msgFmt string, a ...interface{}) Error {
return NewErrorf(code, msgFmt, a...)
}
// WrapError allows Twirp errors to wrap other errors.
// The wrapped error can be extracted later with (github.com/pkg/errors).Unwrap
// or errors.Is from the standard errors package on Go 1.13+.
func WrapError(twerr Error, err error) Error {
return &wrappedErr{
wrapper: twerr,
cause: err,
}
}
// NewError builds a twirp.Error. The code must be one of the valid predefined constants.
// To add metadata, use .WithMeta(key, value) method after building the error.
func NewError(code ErrorCode, msg string) Error {
if !IsValidErrorCode(code) {
return &twerr{code: Internal, msg: "invalid error type " + string(code)}
}
return &twerr{code: code, msg: msg}
}
// NewErrorf builds a twirp.Error with a formatted msg.
// The format may include "%w" to wrap other errors. Examples:
// twirp.NewErrorf(twirp.Internal, "Oops: %w", originalErr)
// twirp.NewErrorf(twirp.NotFound, "resource with id: %q", resourceID)
func NewErrorf(code ErrorCode, msgFmt string, a ...interface{}) Error {
err := fmt.Errorf(msgFmt, a...) // format error message, may include "%w" with an original error
twerr := NewError(code, err.Error()) // use the error as msg
return WrapError(twerr, err) // wrap so the original error can be identified with errors.Is
}
// NotFoundError is a convenience constructor for NotFound errors.
func NotFoundError(msg string) Error {
return NewError(NotFound, msg)
}
// InvalidArgumentError is a convenience constructor for InvalidArgument errors.
// The argument name is included on the "argument" metadata for convenience.
func InvalidArgumentError(argument string, validationMsg string) Error {
err := NewError(InvalidArgument, argument+" "+validationMsg)
err = err.WithMeta("argument", argument)
return err
}
// RequiredArgumentError builds an InvalidArgument error.
// Useful when a request argument is expected to have a non-zero value.
func RequiredArgumentError(argument string) Error {
return InvalidArgumentError(argument, "is required")
}
// InternalError is a convenience constructor for Internal errors.
func InternalError(msg string) Error {
return NewError(Internal, msg)
}
// InternalErrorf uses the formatted message as the internal error msg.
// The format may include "%w" to wrap other errors. Examples:
// twirp.InternalErrorf("database error: %w", err)
// twirp.InternalErrorf("failed to load resource %q: %w", resourceID, originalErr)
func InternalErrorf(msgFmt string, a ...interface{}) Error {
return NewErrorf(Internal, msgFmt, a...)
}
// InternalErrorWith makes an internal error, wrapping the original error and using it
// for the error message, and with metadata "cause" with the original error type.
// This function is used by Twirp services to wrap non-Twirp errors as internal errors.
// The wrapped error can be extracted later with (github.com/pkg/errors).Unwrap
// or errors.Is from the standard errors package on Go 1.13+.
func InternalErrorWith(err error) Error {
twerr := NewError(Internal, err.Error())
twerr = twerr.WithMeta("cause", fmt.Sprintf("%T", err)) // to easily tell apart wrapped internal errors from explicit ones
return WrapError(twerr, err)
}
// ErrorCode represents a Twirp error type.
type ErrorCode string
// Valid Twirp error types. Most error types are equivalent to gRPC status codes
// and follow the same semantics.
const (
// Canceled indicates the operation was cancelled (typically by the caller).
Canceled ErrorCode = "canceled"
// Unknown error. For example when handling errors raised by APIs that do not
// return enough error information.
Unknown ErrorCode = "unknown"
// InvalidArgument indicates client specified an invalid argument. It
// indicates arguments that are problematic regardless of the state of the
// system (i.e. a malformed file name, required argument, number out of range,
// etc.).
InvalidArgument ErrorCode = "invalid_argument"
// Malformed indicates an error occurred while decoding the client's request.
// This may mean that the message was encoded improperly, or that there is a
// disagreement in message format between the client and server.
Malformed ErrorCode = "malformed"
// DeadlineExceeded means operation expired before completion. For operations
// that change the state of the system, this error may be returned even if the
// operation has completed successfully (timeout).
DeadlineExceeded ErrorCode = "deadline_exceeded"
// NotFound means some requested entity was not found.
NotFound ErrorCode = "not_found"
// BadRoute means that the requested URL path wasn't routable to a Twirp
// service and method. This is returned by the generated server, and usually
// shouldn't be returned by applications. Instead, applications should use
// NotFound or Unimplemented.
BadRoute ErrorCode = "bad_route"
// AlreadyExists means an attempt to create an entity failed because one
// already exists.
AlreadyExists ErrorCode = "already_exists"
// PermissionDenied indicates the caller does not have permission to execute
// the specified operation. It must not be used if the caller cannot be
// identified (Unauthenticated).
PermissionDenied ErrorCode = "permission_denied"
// Unauthenticated indicates the request does not have valid authentication
// credentials for the operation.
Unauthenticated ErrorCode = "unauthenticated"
// ResourceExhausted indicates some resource has been exhausted or rate-limited,
// perhaps a per-user quota, or perhaps the entire file system is out of space.
ResourceExhausted ErrorCode = "resource_exhausted"
// FailedPrecondition indicates operation was rejected because the system is
// not in a state required for the operation's execution. For example, doing
// an rmdir operation on a directory that is non-empty, or on a non-directory
// object, or when having conflicting read-modify-write on the same resource.
FailedPrecondition ErrorCode = "failed_precondition"
// Aborted indicates the operation was aborted, typically due to a concurrency
// issue like sequencer check failures, transaction aborts, etc.
Aborted ErrorCode = "aborted"
// OutOfRange means operation was attempted past the valid range. For example,
// seeking or reading past end of a paginated collection.
//
// Unlike InvalidArgument, this error indicates a problem that may be fixed if
// the system state changes (i.e. adding more items to the collection).
//
// There is a fair bit of overlap between FailedPrecondition and OutOfRange.
// We recommend using OutOfRange (the more specific error) when it applies so
// that callers who are iterating through a space can easily look for an
// OutOfRange error to detect when they are done.
OutOfRange ErrorCode = "out_of_range"
// Unimplemented indicates operation is not implemented or not
// supported/enabled in this service.
Unimplemented ErrorCode = "unimplemented"
// Internal errors. When some invariants expected by the underlying system
// have been broken. In other words, something bad happened in the library or
// backend service. Do not confuse with HTTP Internal Server Error; an
// Internal error could also happen on the client code, i.e. when parsing a
// server response.
Internal ErrorCode = "internal"
// Unavailable indicates the service is currently unavailable. This is a most
// likely a transient condition and may be corrected by retrying with a
// backoff.
Unavailable ErrorCode = "unavailable"
// DataLoss indicates unrecoverable data loss or corruption.
DataLoss ErrorCode = "data_loss"
// NoError is the zero-value, is considered an empty error and should not be
// used.
NoError ErrorCode = ""
)
// ServerHTTPStatusFromErrorCode maps a Twirp error type into a similar HTTP
// response status. It is used by the Twirp server handler to set the HTTP
// response status code. Returns 0 if the ErrorCode is invalid.
func ServerHTTPStatusFromErrorCode(code ErrorCode) int {
switch code {
case Canceled:
return 408 // RequestTimeout
case Unknown:
return 500 // Internal Server Error
case InvalidArgument:
return 400 // BadRequest
case Malformed:
return 400 // BadRequest
case DeadlineExceeded:
return 408 // RequestTimeout
case NotFound:
return 404 // Not Found
case BadRoute:
return 404 // Not Found
case AlreadyExists:
return 409 // Conflict
case PermissionDenied:
return 403 // Forbidden
case Unauthenticated:
return 401 // Unauthorized
case ResourceExhausted:
return 429 // Too Many Requests
case FailedPrecondition:
return 412 // Precondition Failed
case Aborted:
return 409 // Conflict
case OutOfRange:
return 400 // Bad Request
case Unimplemented:
return 501 // Not Implemented
case Internal:
return 500 // Internal Server Error
case Unavailable:
return 503 // Service Unavailable
case DataLoss:
return 500 // Internal Server Error
case NoError:
return 200 // OK
default:
return 0 // Invalid!
}
}
// IsValidErrorCode returns true if is one of the valid predefined constants.
func IsValidErrorCode(code ErrorCode) bool {
return ServerHTTPStatusFromErrorCode(code) != 0
}
// twirp.Error implementation
type twerr struct {
code ErrorCode
msg string
meta map[string]string
}
func (e *twerr) Code() ErrorCode { return e.code }
func (e *twerr) Msg() string { return e.msg }
func (e *twerr) Meta(key string) string {
if e.meta != nil {
return e.meta[key] // also returns "" if key is not in meta map
}
return ""
}
func (e *twerr) WithMeta(key string, value string) Error {
newErr := &twerr{
code: e.code,
msg: e.msg,
meta: make(map[string]string, len(e.meta)),
}
for k, v := range e.meta {
newErr.meta[k] = v
}
newErr.meta[key] = value
return newErr
}
func (e *twerr) MetaMap() map[string]string {
return e.meta
}
func (e *twerr) Error() string {
return fmt.Sprintf("twirp error %s: %s", e.code, e.msg)
}
// wrappedErr is the error returned by twirp.InternalErrorWith(err), which is used by clients.
// Implements Unwrap() to allow go 1.13+ errors.Is/As checks,
// and Cause() to allow (github.com/pkg/errors).Unwrap.
type wrappedErr struct {
wrapper Error
cause error
}
func (e *wrappedErr) Code() ErrorCode { return e.wrapper.Code() }
func (e *wrappedErr) Msg() string { return e.wrapper.Msg() }
func (e *wrappedErr) Meta(key string) string { return e.wrapper.Meta(key) }
func (e *wrappedErr) MetaMap() map[string]string { return e.wrapper.MetaMap() }
func (e *wrappedErr) Error() string { return e.wrapper.Error() }
func (e *wrappedErr) WithMeta(key string, val string) Error {
return &wrappedErr{
wrapper: e.wrapper.WithMeta(key, val),
cause: e.cause,
}
}
func (e *wrappedErr) Unwrap() error { return e.cause } // for go1.13 + errors.Is/As
func (e *wrappedErr) Cause() error { return e.cause } // for github.com/pkg/errors
// WriteError writes an HTTP response with a valid Twirp error format (code, msg, meta).
// Useful outside of the Twirp server (e.g. http middleware).
// If err is not a twirp.Error, it will get wrapped with twirp.InternalErrorWith(err)
func WriteError(resp http.ResponseWriter, err error) error {
var twerr Error
if !errors.As(err, &twerr) {
twerr = InternalErrorWith(err)
}
statusCode := ServerHTTPStatusFromErrorCode(twerr.Code())
respBody := marshalErrorToJSON(twerr)
resp.Header().Set("Content-Type", "application/json") // Error responses are always JSON
resp.Header().Set("Content-Length", strconv.Itoa(len(respBody)))
resp.WriteHeader(statusCode) // set HTTP status code and send response
_, writeErr := resp.Write(respBody)
if writeErr != nil {
return writeErr
}
return nil
}
// JSON serialization for errors
type twerrJSON struct {
Code string `json:"code"`
Msg string `json:"msg"`
Meta map[string]string `json:"meta,omitempty"`
}
// marshalErrorToJSON returns JSON from a twirp.Error, that can be used as HTTP error response body.
// If serialization fails, it will use a descriptive Internal error instead.
func marshalErrorToJSON(twerr Error) []byte {
// make sure that msg is not too large
msg := twerr.Msg()
if len(msg) > 1e6 {
msg = msg[:1e6]
}
tj := twerrJSON{
Code: string(twerr.Code()),
Msg: msg,
Meta: twerr.MetaMap(),
}
buf, err := json.Marshal(&tj)
if err != nil {
buf = []byte("{\"type\": \"" + Internal + "\", \"msg\": \"There was an error but it could not be serialized into JSON\"}") // fallback
}
return buf
}