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
|
package structerr
import (
"errors"
"fmt"
"sort"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/anypb"
)
// MetadataItem is an item that associated a metadata key with an arbitrary value.
type MetadataItem struct {
// Key is the key of the metadata item that will be used as the logging key.
Key string
// Value is the value of the metadata item that will be formatted as the logging value.
Value any
}
// Error is a structured error that contains additional details.
type Error struct {
err error
code codes.Code
details []proto.Message
// metadata is the array of metadata items added to this error. Note that we explicitly
// don't use a map here so that we don't have any allocation overhead here in the general
// case where there is no metadata.
metadata []MetadataItem
}
type grpcStatuser interface {
GRPCStatus() *status.Status
}
func newError(code codes.Code, format string, a ...any) Error {
for i, arg := range a {
err, ok := arg.(error)
if !ok {
continue
}
if errors.As(err, &(Error{})) {
// We need to explicitly handle this, otherwise `status.FromError()` would
// return these because we implement `GRPCStatus()`.
continue
}
// If we see any wrapped gRPC error, then we retain its error code and details.
// Note that we cannot use `status.FromError()` here, as that would only return an
// error in case the immediate error is a gRPC status error.
var wrappedGRPCStatus grpcStatuser
if errors.As(err, &wrappedGRPCStatus) {
grpcStatus := wrappedGRPCStatus.GRPCStatus()
// The error message from gRPC errors is awkward because they include
// RPC-specific constructs. This is awkward especially in the case where
// these are embedded in the middle of an error message.
//
// So if we see that the top-level error is a gRPC error, then we only use
// the status message as error message. But otherwise, we use the top-level
// error message.
message := err.Error()
if st, ok := status.FromError(err); ok {
message = st.Message()
}
var details []proto.Message
for _, detail := range grpcStatus.Details() {
if detailProto, ok := detail.(proto.Message); ok {
details = append(details, detailProto)
}
}
a[i] = Error{
err: errors.New(message),
code: grpcStatus.Code(),
details: details,
}
}
}
formattedErr := fmt.Errorf(format, a...)
// When we wrap an Error, we retain its error code. The intent of this is to retain the most
// specific error code we have in the general case. As `Unknown` does not really count as a
// specific error code, we will ignore these errors.
//
// Note that this impacts our middleware status handler, where we wrap non-context-errors
// via `structerr.NewInternal()`. The result is that the caller should never see any
// `Unknown` errors.
var wrappedErr Error
if errors.As(formattedErr, &wrappedErr) {
if wrappedErr.code != codes.Unknown {
code = wrappedErr.code
}
}
return Error{
err: formattedErr,
code: code,
}
}
// New returns a new Error with an Unknown error code. This constructor should be used in the
// general case where it is not clear what the specific error category is. As Unknown errors get
// treated specially, they will be overridden when wrapped with an error that has a more specific
// error code.
func New(format string, a ...any) Error {
return newError(codes.Unknown, format, a...)
}
// NewAborted constructs a new error code with the Aborted error code. Please refer to New for
// further details.
func NewAborted(format string, a ...any) Error {
return newError(codes.Aborted, format, a...)
}
// NewAlreadyExists constructs a new error code with the AlreadyExists error code. Please refer to
// New for further details.
func NewAlreadyExists(format string, a ...any) Error {
return newError(codes.AlreadyExists, format, a...)
}
// NewCanceled constructs a new error code with the Canceled error code. Please refer to New for
// further details.
func NewCanceled(format string, a ...any) Error {
return newError(codes.Canceled, format, a...)
}
// NewDataLoss constructs a new error code with the DataLoss error code. Please refer to New for
// further details.
func NewDataLoss(format string, a ...any) Error {
return newError(codes.DataLoss, format, a...)
}
// NewDeadlineExceeded constructs a new error code with the DeadlineExceeded error code. Please
// refer to New for further details.
func NewDeadlineExceeded(format string, a ...any) Error {
return newError(codes.DeadlineExceeded, format, a...)
}
// NewFailedPrecondition constructs a new error code with the FailedPrecondition error code. Please
// refer to New for further details.
func NewFailedPrecondition(format string, a ...any) Error {
return newError(codes.FailedPrecondition, format, a...)
}
// NewInternal constructs a new error code with the Internal error code. Please refer to New for
// further details.
func NewInternal(format string, a ...any) Error {
return newError(codes.Internal, format, a...)
}
// NewInvalidArgument constructs a new error code with the InvalidArgument error code. Please refer
// to New for further details.
func NewInvalidArgument(format string, a ...any) Error {
return newError(codes.InvalidArgument, format, a...)
}
// NewNotFound constructs a new error code with the NotFound error code. Please refer to New for
// further details.
func NewNotFound(format string, a ...any) Error {
return newError(codes.NotFound, format, a...)
}
// NewPermissionDenied constructs a new error code with the PermissionDenied error code. Please
// refer to New for further details.
func NewPermissionDenied(format string, a ...any) Error {
return newError(codes.PermissionDenied, format, a...)
}
// NewResourceExhausted constructs a new error code with the ResourceExhausted error code. Please
// refer to New for further details.
func NewResourceExhausted(format string, a ...any) Error {
return newError(codes.ResourceExhausted, format, a...)
}
// NewUnavailable constructs a new error code with the Unavailable error code. Please refer to New
// for further details.
func NewUnavailable(format string, a ...any) Error {
return newError(codes.Unavailable, format, a...)
}
// NewUnauthenticated constructs a new error code with the Unauthenticated error code. Please refer
// to New for further details.
func NewUnauthenticated(format string, a ...any) Error {
return newError(codes.Unauthenticated, format, a...)
}
// NewUnimplemented constructs a new error code with the Unimplemented error code. Please refer to
// New for further details.
func NewUnimplemented(format string, a ...any) Error {
return newError(codes.Unimplemented, format, a...)
}
// Error returns the error message of the Error.
func (e Error) Error() string {
return e.err.Error()
}
// Unwrap returns the wrapped error if any, otherwise it returns nil.
func (e Error) Unwrap() error {
return errors.Unwrap(e.err)
}
// Is checks whether the error is equivalent to the target error. Errors are only considered
// equivalent if the GRPC representation of this error is the same.
func (e Error) Is(targetErr error) bool {
target, ok := targetErr.(Error)
if !ok {
return false
}
return errors.Is(e.GRPCStatus().Err(), target.GRPCStatus().Err())
}
// Code returns the error code of the Error.
func (e Error) Code() codes.Code {
return e.code
}
// GRPCStatus returns the gRPC status of this error.
func (e Error) GRPCStatus() *status.Status {
st := status.New(e.Code(), e.Error())
if details := e.Details(); len(details) > 0 {
proto := st.Proto()
for _, detail := range details {
marshaled, err := anypb.New(detail)
if err != nil {
return status.New(codes.Internal, fmt.Sprintf("marshaling error details: %v", err))
}
proto.Details = append(proto.Details, marshaled)
}
st = status.FromProto(proto)
}
return st
}
// errorChain returns the complete chain of `structerr.Error`s wrapped by this error, including the
// error itself.
func (e Error) errorChain() []Error {
var result []Error
for err := error(e); err != nil; err = errors.Unwrap(err) {
if structErr, ok := err.(Error); ok {
result = append(result, structErr)
}
}
return result
}
// Metadata returns the Error's metadata. The metadata will contain the combination of all added
// metadata of this error as well as any wrapped Errors.
//
// When the same metada key exists multiple times in the error chain, then the value that is
// highest up the callchain will be returned. This is done because in general, the higher up the
// callchain one is the more context is available.
func (e Error) Metadata() map[string]any {
result := map[string]any{}
for _, err := range e.errorChain() {
for _, m := range err.metadata {
if _, exists := result[m.Key]; !exists {
result[m.Key] = m.Value
}
}
}
return result
}
// MetadataItems returns a copy of all metadata items added to this error. This function has the
// same semantics as `Metadata()`. The results are sorted by their metadata key.
func (e Error) MetadataItems() []MetadataItem {
metadata := e.Metadata()
items := make([]MetadataItem, 0, len(metadata))
for key, value := range metadata {
items = append(items, MetadataItem{Key: key, Value: value})
}
sort.Slice(items, func(i, j int) bool {
return items[i].Key < items[j].Key
})
return items
}
// WithMetadata adds an additional metadata item to the Error. The metadata has the intent to
// provide more context around errors to the consumer of the Error. Calling this function multiple
// times with the same key will override any previous values.
func (e Error) WithMetadata(key string, value any) Error {
for i, metadataItem := range e.metadata {
// In case the key already exists we override it.
if metadataItem.Key == key {
e.metadata[i].Value = value
return e
}
}
// Otherwise we append a new metadata item.
e.metadata = append(e.metadata, MetadataItem{
Key: key, Value: value,
})
return e
}
// WithMetadataItems is a convenience function to append multiple metadata items to an error. It
// behaves the same as if `WithMetadata()` was called for each of the items separately.
func (e Error) WithMetadataItems(items ...MetadataItem) Error {
for _, item := range items {
e = e.WithMetadata(item.Key, item.Value)
}
return e
}
// Details returns the chain error details set by this and any wrapped Error. The returned array
// contains error details ordered from top-level error details to bottom-level error details.
func (e Error) Details() []proto.Message {
var details []proto.Message
for _, err := range e.errorChain() {
details = append(details, err.details...)
}
return details
}
// WithDetail sets the Error detail that provides additional structured information about the error
// via gRPC so that callers can programmatically determine the exact circumstances of an error.
func (e Error) WithDetail(detail proto.Message) Error {
e.details = append(e.details, detail)
return e
}
// WithGRPCCode overrides the gRPC code embedded into the error.
func (e Error) WithGRPCCode(code codes.Code) Error {
e.code = code
return e
}
|