| // Copyright 2021-2023 Buf Technologies, Inc. |
| // |
| // 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. |
| |
| package triple_protocol |
| |
| import ( |
| "context" |
| "errors" |
| "fmt" |
| "net/http" |
| "net/url" |
| "strings" |
| ) |
| |
| import ( |
| "google.golang.org/protobuf/proto" |
| |
| "google.golang.org/protobuf/types/known/anypb" |
| ) |
| |
| const ( |
| commonErrorsURL = "https://connect.build/docs/go/common-errors" |
| defaultAnyResolverPrefix = "type.googleapis.com/" |
| ) |
| |
| // An ErrorDetail is a self-describing Protobuf message attached to an [*Error]. |
| // Error details are sent over the network to clients, which can then work with |
| // strongly-typed data rather than trying to parse a complex error message. For |
| // example, you might use details to send a localized error message or retry |
| // parameters to the client. |
| // |
| // The [google.golang.org/genproto/googleapis/rpc/errdetails] package contains a |
| // variety of Protobuf messages commonly used as error details. |
| type ErrorDetail struct { |
| pb *anypb.Any |
| wireJSON string // preserve human-readable JSON |
| } |
| |
| // NewErrorDetail constructs a new error detail. If msg is an *[anypb.Any] then |
| // it is used as is. Otherwise, it is first marshalled into an *[anypb.Any] |
| // value. This returns an error if msg cannot be marshalled. |
| func NewErrorDetail(msg proto.Message) (*ErrorDetail, error) { |
| // If it's already an Any, don't wrap it inside another. |
| if pb, ok := msg.(*anypb.Any); ok { |
| return &ErrorDetail{pb: pb}, nil |
| } |
| pb, err := anypb.New(msg) |
| if err != nil { |
| return nil, err |
| } |
| return &ErrorDetail{pb: pb}, nil |
| } |
| |
| // Type is the fully-qualified name of the detail's Protobuf message (for |
| // example, acme.foo.v1.FooDetail). |
| func (d *ErrorDetail) Type() string { |
| // proto.Any tries to make messages self-describing by using type URLs rather |
| // than plain type names, but there aren't any descriptor registries |
| // deployed. With the current state of the `Any` code, it's not possible to |
| // build a useful type registry either. To hide this from users, we should |
| // trim the static hostname that `Any` adds to the type name. |
| // |
| // If we ever want to support remote registries, we can add an explicit |
| // `TypeURL` method. |
| return strings.TrimPrefix(d.pb.TypeUrl, defaultAnyResolverPrefix) |
| } |
| |
| // Bytes returns a copy of the Protobuf-serialized detail. |
| func (d *ErrorDetail) Bytes() []byte { |
| out := make([]byte, len(d.pb.Value)) |
| copy(out, d.pb.Value) |
| return out |
| } |
| |
| // Value uses the Protobuf runtime's package-global registry to unmarshal the |
| // Detail into a strongly-typed message. Typically, clients use Go type |
| // assertions to cast from the proto.Message interface to concrete types. |
| func (d *ErrorDetail) Value() (proto.Message, error) { |
| return d.pb.UnmarshalNew() |
| } |
| |
| // An Error captures four key pieces of information: a [Code], an underlying Go |
| // error, a map of metadata, and an optional collection of arbitrary Protobuf |
| // messages called "details" (more on those below). Servers send the code, the |
| // underlying error's Error() output, the metadata, and details over the wire |
| // to clients. Remember that the underlying error's message will be sent to |
| // clients - take care not to leak sensitive information from public APIs! |
| // |
| // Service implementations and interceptors should return errors that can be |
| // cast to an [*Error] (using the standard library's [errors.As]). If the returned |
| // error can't be cast to an [*Error], triple will use [CodeUnknown] and the |
| // returned error's message. |
| // |
| // Error details are an optional mechanism for servers, interceptors, and |
| // proxies to attach arbitrary Protobuf messages to the error code and message. |
| // They're a clearer and more performant alternative to HTTP header |
| // microformats. See [the documentation on errors] for more details. |
| // |
| // [the documentation on errors]: https://connect.build/docs/go/errors |
| type Error struct { |
| code Code |
| err error |
| details []*ErrorDetail |
| meta http.Header |
| wireErr bool |
| } |
| |
| // NewError annotates any Go error with a status code. |
| func NewError(c Code, underlying error) *Error { |
| return &Error{code: c, err: underlying} |
| } |
| |
| // NewWireError is similar to [NewError], but the resulting *Error returns true |
| // when tested with [IsWireError]. |
| // |
| // This is useful for clients trying to propagate partial failures from |
| // streaming RPCs. Often, these RPCs include error information in their |
| // response messages (for example, [gRPC server reflection] and |
| // OpenTelemtetry's [OTLP]). Clients propagating these errors up the stack |
| // should use NewWireError to clarify that the error code, message, and details |
| // (if any) were explicitly sent by the server rather than inferred from a |
| // lower-level networking error or timeout. |
| // |
| // [gRPC server reflection]: https://github.com/grpc/grpc/blob/v1.49.2/src/proto/grpc/reflection/v1alpha/reflection.proto#L132-L136 |
| // [OTLP]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/otlp.md#partial-success |
| func NewWireError(c Code, underlying error) *Error { |
| err := NewError(c, underlying) |
| err.wireErr = true |
| return err |
| } |
| |
| // IsWireError checks whether the error was returned by the server, as opposed |
| // to being synthesized by the client. |
| // |
| // Clients may find this useful when deciding how to propagate errors. For |
| // example, an RPC-to-HTTP proxy might expose a server-sent CodeUnknown as an |
| // HTTP 500 but a client-synthesized CodeUnknown as a 503. |
| func IsWireError(err error) bool { |
| se := new(Error) |
| if !errors.As(err, &se) { |
| return false |
| } |
| return se.wireErr |
| } |
| |
| func (e *Error) Error() string { |
| message := e.Message() |
| if message == "" { |
| return e.code.String() |
| } |
| return e.code.String() + ": " + message |
| } |
| |
| // Message returns the underlying error message. It may be empty if the |
| // original error was created with a status code and a nil error. |
| func (e *Error) Message() string { |
| if e.err != nil { |
| return e.err.Error() |
| } |
| return "" |
| } |
| |
| // Unwrap allows [errors.Is] and [errors.As] access to the underlying error. |
| func (e *Error) Unwrap() error { |
| return e.err |
| } |
| |
| // Code returns the error's status code. |
| func (e *Error) Code() Code { |
| return e.code |
| } |
| |
| // Details returns the error's details. |
| func (e *Error) Details() []*ErrorDetail { |
| return e.details |
| } |
| |
| // AddDetail appends to the error's details. |
| func (e *Error) AddDetail(d *ErrorDetail) { |
| e.details = append(e.details, d) |
| } |
| |
| // Meta allows the error to carry additional information as key-value pairs. |
| // |
| // Metadata attached to errors returned by unary handlers is always sent as |
| // HTTP headers, regardless of the protocol. Metadata attached to errors |
| // returned by streaming handlers may be sent as HTTP headers, HTTP trailers, |
| // or a block of in-body metadata, depending on the protocol in use and whether |
| // or not the handler has already written messages to the stream. |
| // |
| // When clients receive errors, the metadata contains the union of the HTTP |
| // headers and the protocol-specific trailers (either HTTP trailers or in-body |
| // metadata). |
| func (e *Error) Meta() http.Header { |
| if e.meta == nil { |
| e.meta = make(http.Header) |
| } |
| return e.meta |
| } |
| |
| func (e *Error) detailsAsAny() []*anypb.Any { |
| anys := make([]*anypb.Any, 0, len(e.details)) |
| for _, detail := range e.details { |
| anys = append(anys, detail.pb) |
| } |
| return anys |
| } |
| |
| // errorf calls fmt.Errorf with the supplied template and arguments, then wraps |
| // the resulting error. |
| func errorf(c Code, template string, args ...any) *Error { |
| return NewError(c, fmt.Errorf(template, args...)) |
| } |
| |
| // asError uses errors.As to unwrap any error and look for a triple *Error. |
| func asError(err error) (*Error, bool) { |
| var connectErr *Error |
| ok := errors.As(err, &connectErr) |
| return connectErr, ok |
| } |
| |
| // wrapIfUncoded ensures that all errors are wrapped. It leaves already-wrapped |
| // errors unchanged, uses wrapIfContextError to apply codes to context.Canceled |
| // and context.DeadlineExceeded, and falls back to wrapping other errors with |
| // CodeUnknown. |
| func wrapIfUncoded(err error) error { |
| if err == nil { |
| return nil |
| } |
| maybeCodedErr := wrapIfContextError(err) |
| if _, ok := asError(maybeCodedErr); ok { |
| return maybeCodedErr |
| } |
| return NewError(CodeUnknown, maybeCodedErr) |
| } |
| |
| // wrapIfContextError applies CodeCanceled or CodeDeadlineExceeded to Go's |
| // context.Canceled and context.DeadlineExceeded errors, but only if they |
| // haven't already been wrapped. |
| func wrapIfContextError(err error) error { |
| if err == nil { |
| return nil |
| } |
| if _, ok := asError(err); ok { |
| return err |
| } |
| if errors.Is(err, context.Canceled) { |
| return NewError(CodeCanceled, err) |
| } |
| if errors.Is(err, context.DeadlineExceeded) { |
| return NewError(CodeDeadlineExceeded, err) |
| } |
| return err |
| } |
| |
| // wrapIfLikelyWithGRPCNotUsedError adds a wrapping error that has a message |
| // telling the caller that they likely need to use h2c but are using a raw http.Client{}. |
| // |
| // This happens when running a gRPC-only server. |
| // This is fragile and may break over time, and this should be considered a best-effort. |
| func wrapIfLikelyH2CNotConfiguredError(request *http.Request, err error) error { |
| if err == nil { |
| return nil |
| } |
| if _, ok := asError(err); ok { |
| return err |
| } |
| if url := request.URL; url != nil && url.Scheme != "http" { |
| // If the scheme is not http, we definitely do not have an h2c error, so just return. |
| return err |
| } |
| // net/http code has been investigated and there is no typing of any of these errors |
| // they are all created with fmt.Errorf |
| // grpc-go returns the first error 2/3-3/4 of the time, and the second error 1/4-1/3 of the time |
| if errString := err.Error(); strings.HasPrefix(errString, `Post "`) && |
| (strings.Contains(errString, `net/http: HTTP/1.x transport connection broken: malformed HTTP response`) || |
| strings.HasSuffix(errString, `write: broken pipe`)) { |
| return fmt.Errorf("possible h2c configuration issue when talking to gRPC server, see %s: %w", commonErrorsURL, err) |
| } |
| return err |
| } |
| |
| // wrapIfLikelyWithGRPCNotUsedError adds a wrapping error that has a message |
| // telling the caller that they likely forgot to use triple.WithGRPC(). |
| // |
| // This happens when running a gRPC-only server. |
| // This is fragile and may break over time, and this should be considered a best-effort. |
| func wrapIfLikelyWithGRPCNotUsedError(err error) error { |
| if err == nil { |
| return nil |
| } |
| if _, ok := asError(err); ok { |
| return err |
| } |
| // golang.org/x/net code has been investigated and there is no typing of this error |
| // it is created with fmt.Errorf |
| // http2/transport.go:573: return nil, fmt.Errorf("http2: Transport: cannot retry err [%v] after Request.Body was written; define Request.GetBody to avoid this error", err) |
| if errString := err.Error(); strings.HasPrefix(errString, `Post "`) && |
| strings.Contains(errString, `http2: Transport: cannot retry err`) && |
| strings.HasSuffix(errString, `after Request.Body was written; define Request.GetBody to avoid this error`) { |
| return fmt.Errorf("possible missing triple.WithGPRC() client option when talking to gRPC server, see %s: %w", commonErrorsURL, err) |
| } |
| return err |
| } |
| |
| // HTTP/2 has its own set of error codes, which it sends in RST_STREAM frames. |
| // When the server sends one of these errors, we should map it back into our |
| // RPC error codes following |
| // https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#http2-transport-mapping. |
| // |
| // This would be vastly simpler if we were using x/net/http2 directly, since |
| // the StreamError type is exported. When x/net/http2 gets vendored into |
| // net/http, though, all these types become unexported...so we're left with |
| // string munging. |
| func wrapIfRSTError(err error) error { |
| const ( |
| streamErrPrefix = "stream error: " |
| fromPeerSuffix = "; received from peer" |
| ) |
| if err == nil { |
| return nil |
| } |
| if _, ok := asError(err); ok { |
| return err |
| } |
| if urlErr := new(url.Error); errors.As(err, &urlErr) { |
| // If we get an RST_STREAM error from http.Client.Do, it's wrapped in a |
| // *url.Error. |
| err = urlErr.Unwrap() |
| } |
| msg := err.Error() |
| if !strings.HasPrefix(msg, streamErrPrefix) { |
| return err |
| } |
| if !strings.HasSuffix(msg, fromPeerSuffix) { |
| return err |
| } |
| msg = strings.TrimSuffix(msg, fromPeerSuffix) |
| i := strings.LastIndex(msg, ";") |
| if i < 0 || i >= len(msg)-1 { |
| return err |
| } |
| msg = msg[i+1:] |
| msg = strings.TrimSpace(msg) |
| switch msg { |
| case "NO_ERROR", "PROTOCOL_ERROR", "INTERNAL_ERROR", "FLOW_CONTROL_ERROR", |
| "SETTINGS_TIMEOUT", "FRAME_SIZE_ERROR", "COMPRESSION_ERROR", "CONNECT_ERROR": |
| return NewError(CodeInternal, err) |
| case "REFUSED_STREAM": |
| return NewError(CodeUnavailable, err) |
| case "CANCEL": |
| return NewError(CodeCanceled, err) |
| case "ENHANCE_YOUR_CALM": |
| return NewError(CodeResourceExhausted, fmt.Errorf("bandwidth exhausted: %w", err)) |
| case "INADEQUATE_SECURITY": |
| return NewError(CodePermissionDenied, fmt.Errorf("transport protocol insecure: %w", err)) |
| default: |
| return err |
| } |
| } |