-
Notifications
You must be signed in to change notification settings - Fork 66
/
ext_grpc.go
188 lines (168 loc) · 7 KB
/
ext_grpc.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
// Copyright 2019 The Cockroach Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License 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.
// This package provides support for gRPC error handling. It has
// two main features:
//
// 1. Automatic en/decoding of gRPC Status errors via EncodeError and
// DecodeError, enabled by importing the package. Supports both the
// standard google.golang.org/grpc/status and the gogoproto-compatible
// github.com/gogo/status packages.
//
// 2. Wrapping arbitrary errors with a gRPC status code via WrapWithGrpcCode()
// and GetGrpcCode(). There is also a gRPC middleware in middleware/grpc
// that will automatically do this (un)wrapping.
//
// Note that it is not possible to mix standard Protobuf and gogoproto
// status details via Status.WithDetails(). All details must be standard
// Protobufs with the standard grpc/status package, and all details must
// be gogoproto Protobufs with the gogo/status package. This is caused by
// WithDetails() and Details() immediately (un)marshalling the detail as
// an Any field, and since gogoproto types are not registered with the standard
// Protobuf registry (and vice versa) they cannot be unmarshalled.
//
// Furthermore, since we have to encode all errors as gogoproto Status
// messages to place them in EncodedError (again because of an Any field),
// only gogoproto status details can be preserved across EncodeError().
// See also: https://github.com/cockroachdb/errors/issues/63
package extgrpc
import (
"context"
"fmt"
"github.com/cockroachdb/errors"
"github.com/cockroachdb/errors/errbase"
"github.com/cockroachdb/errors/markers"
"github.com/cockroachdb/redact"
gogorpc "github.com/gogo/googleapis/google/rpc"
"github.com/gogo/protobuf/proto"
gogostatus "github.com/gogo/status"
"google.golang.org/grpc/codes"
grpcstatus "google.golang.org/grpc/status"
)
// withGrpcCode wraps an error with a gRPC status code.
type withGrpcCode struct {
cause error
code codes.Code
}
// WrapWithGrpcCode wraps an error with a gRPC status code.
func WrapWithGrpcCode(err error, code codes.Code) error {
if err == nil {
return nil
}
return &withGrpcCode{cause: err, code: code}
}
// GetGrpcCode retrieves the gRPC code from a stack of causes.
func GetGrpcCode(err error) codes.Code {
if err == nil {
return codes.OK
}
if v, ok := markers.If(err, func(err error) (interface{}, bool) {
if w, ok := err.(*withGrpcCode); ok {
return w.code, true
}
return nil, false
}); ok {
return v.(codes.Code)
}
return codes.Unknown
}
// it's an error.
func (w *withGrpcCode) Error() string { return w.cause.Error() }
// it's also a wrapper.
func (w *withGrpcCode) Cause() error { return w.cause }
func (w *withGrpcCode) Unwrap() error { return w.cause }
// it knows how to format itself.
func (w *withGrpcCode) Format(s fmt.State, verb rune) { errors.FormatError(w, s, verb) }
// SafeFormatter implements errors.SafeFormatter.
// Note: see the documentation of errbase.SafeFormatter for details
// on how to implement this. In particular beware of not emitting
// unsafe strings.
func (w *withGrpcCode) SafeFormatError(p errors.Printer) (next error) {
if p.Detail() {
p.Printf("gRPC code: %s", redact.Safe(w.code))
}
return w.cause
}
// it's an encodable error.
func encodeWithGrpcCode(_ context.Context, err error) (string, []string, proto.Message) {
w := err.(*withGrpcCode)
details := []string{fmt.Sprintf("gRPC %d", w.code)}
payload := &EncodedGrpcCode{Code: uint32(w.code)}
return "", details, payload
}
// it's a decodable error.
func decodeWithGrpcCode(
_ context.Context, cause error, _ string, _ []string, payload proto.Message,
) error {
wp := payload.(*EncodedGrpcCode)
return &withGrpcCode{cause: cause, code: codes.Code(wp.Code)}
}
// encodeGrpcStatus takes an error generated by a standard gRPC Status and
// converts it into a GoGo Protobuf representation from
// github.com/gogo/googleapis/google/rpc.
//
// This is necessary since EncodedError uses an Any field for structured errors,
// and thus can only contain Protobufs that have been registered with the GoGo
// Protobuf type registry -- the standard gRPC Status type is not a GoGo
// Protobuf, and is therefore not registered with it and cannot be decoded by
// DecodeError().
//
// Also note that in order to use error details, the input type must be a
// gogoproto Status from github.com/gogo/status, not from the standard gRPC
// Status, and all details must be gogoproto types. The reasons for this
// are the same as for the Any field issue mentioned above.
func encodeGrpcStatus(_ context.Context, err error) (string, []string, proto.Message) {
s := gogostatus.Convert(err)
// If there are known safe details, return them.
details := []string{}
for _, detail := range s.Details() {
if safe, ok := detail.(errbase.SafeDetailer); ok {
details = append(details, safe.SafeDetails()...)
}
}
return s.Message(), details, s.Proto()
}
// decodeGrpcStatus is the inverse of encodeGrpcStatus. It takes a gogoproto
// Status as input, and converts it into a standard gRPC Status error.
func decodeGrpcStatus(
ctx context.Context, msg string, details []string, payload proto.Message,
) error {
return grpcstatus.Convert(decodeGoGoStatus(ctx, msg, details, payload)).Err()
}
// encodeGoGoStatus encodes a GoGo Status error. It calls encodeGrpcStatus, since
// it can handle both kinds.
func encodeGoGoStatus(ctx context.Context, err error) (string, []string, proto.Message) {
return encodeGrpcStatus(ctx, err)
}
// decodeGoGoStatus is similar to decodeGrpcStatus, but decodes into a gogo
// Status error instead of a gRPC Status error. It is used when the original
// error was a gogo Status error rather than a gRPC Status error.
func decodeGoGoStatus(_ context.Context, _ string, _ []string, payload proto.Message) error {
s, ok := payload.(*gogorpc.Status)
if !ok {
// If input type was unexpected (shouldn't happen), we just return nil
// which will cause DecodeError() to return an opaqueLeaf.
return nil
}
return gogostatus.ErrorProto(s)
}
func init() {
grpcError := grpcstatus.Error(codes.Unknown, "")
errbase.RegisterLeafEncoder(errbase.GetTypeKey(grpcError), encodeGrpcStatus)
errbase.RegisterLeafDecoder(errbase.GetTypeKey(grpcError), decodeGrpcStatus)
gogoError := gogostatus.Error(codes.Unknown, "")
errbase.RegisterLeafEncoder(errbase.GetTypeKey(gogoError), encodeGoGoStatus)
errbase.RegisterLeafDecoder(errbase.GetTypeKey(gogoError), decodeGoGoStatus)
errbase.RegisterWrapperEncoder(errbase.GetTypeKey((*withGrpcCode)(nil)), encodeWithGrpcCode)
errbase.RegisterWrapperDecoder(errbase.GetTypeKey((*withGrpcCode)(nil)), decodeWithGrpcCode)
}