Welcome to mirror list, hosted at ThFree Co, Russian Federation.

error.go « structerr « internal - gitlab.com/gitlab-org/gitaly.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: fdaf132dfe4f8b1a7a4da9dbbd5608ee9ed3f1e7 (plain)
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
package structerr

import (
	"errors"
	"fmt"

	"gitlab.com/gitlab-org/gitaly/v15/proto/go/gitalypb/testproto"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/types/known/anypb"
)

type metadataItem struct {
	key   string
	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.
	var wrappedErr Error
	if errors.As(formattedErr, &wrappedErr) {
		code = wrappedErr.code
	}

	return Error{
		err:  formattedErr,
		code: code,
	}
}

// New returns a new Error with the default error code, which is Internal. When this function is
// used to wrap another Error, then the error code of that wrapped Error will be retained. The
// intent of this is to always retain the most specific error code in the general case.
func New(format string, a ...any) Error {
	return newError(codes.Internal, 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...)
}

// NewUnknown constructs a new error code with the Unknown error code. Please refer to New for
// further details.
func NewUnknown(format string, a ...any) Error {
	return newError(codes.Unknown, 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
}

// 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
}

// WithInterceptedMetadata adds an additional metadata item to the Error in the form of an error
// detail. Note that this is only intended to be used in the context of tests where we convert error
// metadata into structured errors via the UnaryInterceptor and StreamInterceptor so that we can
// test that metadata has been set as expected on the client-side of a gRPC call.
func (e Error) WithInterceptedMetadata(key string, value any) Error {
	return e.WithDetail(&testproto.ErrorMetadata{
		Key:   []byte(key),
		Value: []byte(fmt.Sprintf("%v", value)),
	})
}

// 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
}