protocol/triple/triple_protocol/error.go (210 lines of code) (raw):

// 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 ( // todo(DMwangnima): add common errors documentation 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 marshaled into an *[anypb.Any] // value. This returns an error if msg cannot be marshaled. 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. // // todo(DMwangnima): add error documentation to dubbo-go official website // [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 tripleErr *Error ok := errors.As(err, &tripleErr) return tripleErr, 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 } // wrapIfLikelyH2CNotConfiguredError 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 reqUrl := request.URL; reqUrl != nil && reqUrl.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 the server side does not use gRPC. // // 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 } }