// Copyright 2021-2024 The Connect 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. package connect import ( "bufio" "context" "errors" "fmt" "io" "math" "net/http" "net/textproto" "runtime" "strconv" "strings" "time" statusv1 "connectrpc.com/connect/internal/gen/connectext/grpc/status/v1" ) const ( grpcHeaderCompression = "Grpc-Encoding" grpcHeaderAcceptCompression = "Grpc-Accept-Encoding" grpcHeaderTimeout = "Grpc-Timeout" grpcHeaderStatus = "Grpc-Status" grpcHeaderMessage = "Grpc-Message" grpcHeaderDetails = "Grpc-Status-Details-Bin" grpcFlagEnvelopeTrailer = 0b10000000 grpcContentTypeDefault = "application/grpc" grpcWebContentTypeDefault = "application/grpc-web" grpcContentTypePrefix = grpcContentTypeDefault + "+" grpcWebContentTypePrefix = grpcWebContentTypeDefault + "+" headerXUserAgent = "X-User-Agent" upperhex = "0123456789ABCDEF" ) var ( errTrailersWithoutGRPCStatus = fmt.Errorf("protocol error: no %s trailer: %w", grpcHeaderStatus, io.ErrUnexpectedEOF) // defaultGrpcUserAgent follows // https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#user-agents: // // While the protocol does not require a user-agent to function it is recommended // that clients provide a structured user-agent string that provides a basic // description of the calling library, version & platform to facilitate issue diagnosis // in heterogeneous environments. The following structure is recommended to library developers: // // User-Agent → "grpc-" Language ?("-" Variant) "/" Version ?( " (" *(AdditionalProperty ";") ")" ) defaultGrpcUserAgent = fmt.Sprintf("grpc-go-connect/%s (%s)", Version, runtime.Version()) grpcAllowedMethods = map[string]struct{}{ http.MethodPost: {}, } ) type protocolGRPC struct { web bool } // NewHandler implements protocol, so it must return an interface. func (g *protocolGRPC) NewHandler(params *protocolHandlerParams) protocolHandler { bare, prefix := grpcContentTypeDefault, grpcContentTypePrefix if g.web { bare, prefix = grpcWebContentTypeDefault, grpcWebContentTypePrefix } contentTypes := make(map[string]struct{}) for _, name := range params.Codecs.Names() { contentTypes[canonicalizeContentType(prefix+name)] = struct{}{} } if params.Codecs.Get(codecNameProto) != nil { contentTypes[bare] = struct{}{} } return &grpcHandler{ protocolHandlerParams: *params, web: g.web, accept: contentTypes, } } // NewClient implements protocol, so it must return an interface. func (g *protocolGRPC) NewClient(params *protocolClientParams) (protocolClient, error) { peer := newPeerFromURL(params.URL, ProtocolGRPC) if g.web { peer = newPeerFromURL(params.URL, ProtocolGRPCWeb) } return &grpcClient{ protocolClientParams: *params, web: g.web, peer: peer, }, nil } type grpcHandler struct { protocolHandlerParams web bool accept map[string]struct{} } func (g *grpcHandler) Methods() map[string]struct{} { return grpcAllowedMethods } func (g *grpcHandler) ContentTypes() map[string]struct{} { return g.accept } func (*grpcHandler) SetTimeout(request *http.Request) (context.Context, context.CancelFunc, error) { timeout, err := grpcParseTimeout(getHeaderCanonical(request.Header, grpcHeaderTimeout)) if err != nil && !errors.Is(err, errNoTimeout) { // Errors here indicate that the client sent an invalid timeout header, so // the error text is safe to send back. return nil, nil, NewError(CodeInvalidArgument, err) } else if err != nil { // err wraps errNoTimeout, nothing to do. return request.Context(), nil, nil //nolint:nilerr } ctx, cancel := context.WithTimeout(request.Context(), timeout) return ctx, cancel, nil } func (g *grpcHandler) CanHandlePayload(_ *http.Request, contentType string) bool { _, ok := g.accept[contentType] return ok } func (g *grpcHandler) NewConn( responseWriter http.ResponseWriter, request *http.Request, ) (handlerConnCloser, bool) { ctx := request.Context() // We need to parse metadata before entering the interceptor stack; we'll // send the error to the client later on. requestCompression, responseCompression, failed := negotiateCompression( g.CompressionPools, getHeaderCanonical(request.Header, grpcHeaderCompression), getHeaderCanonical(request.Header, grpcHeaderAcceptCompression), ) if failed == nil { failed = checkServerStreamsCanFlush(g.Spec, responseWriter) } // Write any remaining headers here: // (1) any writes to the stream will implicitly send the headers, so we // should get all of gRPC's required response headers ready. // (2) interceptors should be able to see these headers. // // Since we know that these header keys are already in canonical form, we can // skip the normalization in Header.Set. header := responseWriter.Header() header[headerContentType] = []string{getHeaderCanonical(request.Header, headerContentType)} header[grpcHeaderAcceptCompression] = []string{g.CompressionPools.CommaSeparatedNames()} if responseCompression != compressionIdentity { header[grpcHeaderCompression] = []string{responseCompression} } codecName := grpcCodecFromContentType(g.web, getHeaderCanonical(request.Header, headerContentType)) codec := g.Codecs.Get(codecName) // handler.go guarantees this is not nil protocolName := ProtocolGRPC if g.web { protocolName = ProtocolGRPCWeb } conn := wrapHandlerConnWithCodedErrors(&grpcHandlerConn{ spec: g.Spec, peer: Peer{ Addr: request.RemoteAddr, Protocol: protocolName, }, web: g.web, bufferPool: g.BufferPool, protobuf: g.Codecs.Protobuf(), // for errors marshaler: grpcMarshaler{ envelopeWriter: envelopeWriter{ ctx: ctx, sender: writeSender{writer: responseWriter}, compressionPool: g.CompressionPools.Get(responseCompression), codec: codec, compressMinBytes: g.CompressMinBytes, bufferPool: g.BufferPool, sendMaxBytes: g.SendMaxBytes, }, }, responseWriter: responseWriter, responseHeader: make(http.Header), responseTrailer: make(http.Header), request: request, unmarshaler: grpcUnmarshaler{ envelopeReader: envelopeReader{ ctx: ctx, reader: request.Body, codec: codec, compressionPool: g.CompressionPools.Get(requestCompression), bufferPool: g.BufferPool, readMaxBytes: g.ReadMaxBytes, }, web: g.web, }, }) if failed != nil { // Negotiation failed, so we can't establish a stream. _ = conn.Close(failed) return nil, false } return conn, true } type grpcClient struct { protocolClientParams web bool peer Peer } func (g *grpcClient) Peer() Peer { return g.peer } func (g *grpcClient) WriteRequestHeader(_ StreamType, header http.Header) { // We know these header keys are in canonical form, so we can bypass all the // checks in Header.Set. if getHeaderCanonical(header, headerUserAgent) == "" { header[headerUserAgent] = []string{defaultGrpcUserAgent} } if g.web && getHeaderCanonical(header, headerXUserAgent) == "" { // The gRPC-Web pseudo-specification seems to require X-User-Agent rather // than User-Agent for all clients, even if they're not browser-based. This // is very odd for a backend client, so we'll split the difference and set // both. header[headerXUserAgent] = []string{defaultGrpcUserAgent} } header[headerContentType] = []string{grpcContentTypeFromCodecName(g.web, g.Codec.Name())} // gRPC handles compression on a per-message basis, so we don't want to // compress the whole stream. By default, http.Client will ask the server // to gzip the stream if we don't set Accept-Encoding. header["Accept-Encoding"] = []string{compressionIdentity} if g.CompressionName != "" && g.CompressionName != compressionIdentity { header[grpcHeaderCompression] = []string{g.CompressionName} } if acceptCompression := g.CompressionPools.CommaSeparatedNames(); acceptCompression != "" { header[grpcHeaderAcceptCompression] = []string{acceptCompression} } if !g.web { // The gRPC-HTTP2 specification requires this - it flushes out proxies that // don't support HTTP trailers. header["Te"] = []string{"trailers"} } } func (g *grpcClient) NewConn( ctx context.Context, spec Spec, header http.Header, ) streamingClientConn { if deadline, ok := ctx.Deadline(); ok { encodedDeadline := grpcEncodeTimeout(time.Until(deadline)) header[grpcHeaderTimeout] = []string{encodedDeadline} } duplexCall := newDuplexHTTPCall( ctx, g.HTTPClient, g.URL, spec, header, ) conn := &grpcClientConn{ spec: spec, peer: g.Peer(), duplexCall: duplexCall, compressionPools: g.CompressionPools, bufferPool: g.BufferPool, protobuf: g.Protobuf, marshaler: grpcMarshaler{ envelopeWriter: envelopeWriter{ ctx: ctx, sender: duplexCall, compressionPool: g.CompressionPools.Get(g.CompressionName), codec: g.Codec, compressMinBytes: g.CompressMinBytes, bufferPool: g.BufferPool, sendMaxBytes: g.SendMaxBytes, }, }, unmarshaler: grpcUnmarshaler{ envelopeReader: envelopeReader{ ctx: ctx, reader: duplexCall, codec: g.Codec, bufferPool: g.BufferPool, readMaxBytes: g.ReadMaxBytes, }, }, responseHeader: make(http.Header), responseTrailer: make(http.Header), } duplexCall.SetValidateResponse(conn.validateResponse) if g.web { conn.unmarshaler.web = true conn.readTrailers = func(unmarshaler *grpcUnmarshaler, _ *duplexHTTPCall) http.Header { return unmarshaler.WebTrailer() } } else { conn.readTrailers = func(_ *grpcUnmarshaler, call *duplexHTTPCall) http.Header { // To access HTTP trailers, we need to read the body to EOF. _, _ = discard(call) return call.ResponseTrailer() } } return wrapClientConnWithCodedErrors(conn) } // grpcClientConn works for both gRPC and gRPC-Web. type grpcClientConn struct { spec Spec peer Peer duplexCall *duplexHTTPCall compressionPools readOnlyCompressionPools bufferPool *bufferPool protobuf Codec // for errors marshaler grpcMarshaler unmarshaler grpcUnmarshaler responseHeader http.Header responseTrailer http.Header readTrailers func(*grpcUnmarshaler, *duplexHTTPCall) http.Header } func (cc *grpcClientConn) Spec() Spec { return cc.spec } func (cc *grpcClientConn) Peer() Peer { return cc.peer } func (cc *grpcClientConn) Send(msg any) error { if err := cc.marshaler.Marshal(msg); err != nil { return err } return nil // must be a literal nil: nil *Error is a non-nil error } func (cc *grpcClientConn) RequestHeader() http.Header { return cc.duplexCall.Header() } func (cc *grpcClientConn) CloseRequest() error { return cc.duplexCall.CloseWrite() } func (cc *grpcClientConn) Receive(msg any) error { if err := cc.duplexCall.BlockUntilResponseReady(); err != nil { return err } err := cc.unmarshaler.Unmarshal(msg) if err == nil { return nil } mergeHeaders( cc.responseTrailer, cc.readTrailers(&cc.unmarshaler, cc.duplexCall), ) if errors.Is(err, io.EOF) && cc.unmarshaler.bytesRead == 0 && len(cc.responseTrailer) == 0 { // No body and no trailers means a trailers-only response. // Note: per the specification, only the HTTP status code and Content-Type // should be treated as headers. The rest should be treated as trailing // metadata. But it would be unsafe to mutate cc.responseHeader at this // point. So we'll leave cc.responseHeader alone but copy the relevant // metadata into cc.responseTrailer. mergeHeaders(cc.responseTrailer, cc.responseHeader) delHeaderCanonical(cc.responseTrailer, headerContentType) // Try to read the status out of the headers. serverErr := grpcErrorFromTrailer(cc.protobuf, cc.responseHeader) if serverErr == nil { // Status says "OK". So return original error (io.EOF). return err } serverErr.meta = cc.responseHeader.Clone() return serverErr } // See if the server sent an explicit error in the HTTP or gRPC-Web trailers. serverErr := grpcErrorFromTrailer(cc.protobuf, cc.responseTrailer) if serverErr != nil && (errors.Is(err, io.EOF) || !errors.Is(serverErr, errTrailersWithoutGRPCStatus)) { // We've either: // - Cleanly read until the end of the response body and *not* received // gRPC status trailers, which is a protocol error, or // - Received an explicit error from the server. // // This is expected from a protocol perspective, but receiving trailers // means that we're _not_ getting a message. For users to realize that // the stream has ended, Receive must return an error. serverErr.meta = cc.responseHeader.Clone() mergeHeaders(serverErr.meta, cc.responseTrailer) _ = cc.duplexCall.CloseWrite() return serverErr } // This was probably an error converting the bytes to a message or an error // reading from the network. We're going to return it to the // user, but we also want to close writes so Send errors out. _ = cc.duplexCall.CloseWrite() return err } func (cc *grpcClientConn) ResponseHeader() http.Header { _ = cc.duplexCall.BlockUntilResponseReady() return cc.responseHeader } func (cc *grpcClientConn) ResponseTrailer() http.Header { _ = cc.duplexCall.BlockUntilResponseReady() return cc.responseTrailer } func (cc *grpcClientConn) CloseResponse() error { return cc.duplexCall.CloseRead() } func (cc *grpcClientConn) onRequestSend(fn func(*http.Request)) { cc.duplexCall.onRequestSend = fn } func (cc *grpcClientConn) validateResponse(response *http.Response) *Error { if err := grpcValidateResponse( response, cc.responseHeader, cc.compressionPools, cc.unmarshaler.web, cc.marshaler.codec.Name(), ); err != nil { return err } compression := getHeaderCanonical(response.Header, grpcHeaderCompression) cc.unmarshaler.compressionPool = cc.compressionPools.Get(compression) return nil } type grpcHandlerConn struct { spec Spec peer Peer web bool bufferPool *bufferPool protobuf Codec // for errors marshaler grpcMarshaler responseWriter http.ResponseWriter responseHeader http.Header responseTrailer http.Header wroteToBody bool request *http.Request unmarshaler grpcUnmarshaler } func (hc *grpcHandlerConn) Spec() Spec { return hc.spec } func (hc *grpcHandlerConn) Peer() Peer { return hc.peer } func (hc *grpcHandlerConn) Receive(msg any) error { if err := hc.unmarshaler.Unmarshal(msg); err != nil { return err // already coded } return nil // must be a literal nil: nil *Error is a non-nil error } func (hc *grpcHandlerConn) RequestHeader() http.Header { return hc.request.Header } func (hc *grpcHandlerConn) Send(msg any) error { defer flushResponseWriter(hc.responseWriter) if !hc.wroteToBody { mergeHeaders(hc.responseWriter.Header(), hc.responseHeader) hc.wroteToBody = true } if err := hc.marshaler.Marshal(msg); err != nil { return err } return nil // must be a literal nil: nil *Error is a non-nil error } func (hc *grpcHandlerConn) ResponseHeader() http.Header { return hc.responseHeader } func (hc *grpcHandlerConn) ResponseTrailer() http.Header { return hc.responseTrailer } func (hc *grpcHandlerConn) Close(err error) (retErr error) { defer func() { // We don't want to copy unread portions of the body to /dev/null here: if // the client hasn't closed the request body, we'll block until the server // timeout kicks in. This could happen because the client is malicious, but // a well-intentioned client may just not expect the server to be returning // an error for a streaming RPC. Better to accept that we can't always reuse // TCP connections. closeErr := hc.request.Body.Close() if retErr == nil { retErr = closeErr } }() defer flushResponseWriter(hc.responseWriter) // If we haven't written the headers yet, do so. if !hc.wroteToBody { mergeHeaders(hc.responseWriter.Header(), hc.responseHeader) } // gRPC always sends the error's code, message, details, and metadata as // trailing metadata. The Connect protocol doesn't do this, so we don't want // to mutate the trailers map that the user sees. mergedTrailers := make( http.Header, len(hc.responseTrailer)+2, // always make space for status & message ) mergeHeaders(mergedTrailers, hc.responseTrailer) grpcErrorToTrailer(mergedTrailers, hc.protobuf, err) if hc.web && !hc.wroteToBody && len(hc.responseHeader) == 0 { // We're using gRPC-Web, we haven't yet written to the body, and there are no // custom headers. That means we can send a "trailers-only" response and send // trailing metadata as HTTP headers (instead of as trailers). mergeHeaders(hc.responseWriter.Header(), mergedTrailers) return nil } if hc.web { // We're using gRPC-Web and we've already sent the headers, so we write // trailing metadata to the HTTP body. if err := hc.marshaler.MarshalWebTrailers(mergedTrailers); err != nil { return err } return nil // must be a literal nil: nil *Error is a non-nil error } // We're using standard gRPC. Even if we haven't written to the body and // we're sending a "trailers-only" response, we must send trailing metadata // as HTTP trailers. (If we had frame-level control of the HTTP/2 layer, we // could send trailers-only responses as a single HEADER frame and no DATA // frames, but net/http doesn't expose APIs that low-level.) // // In net/http's ResponseWriter API, we send HTTP trailers by writing to the // headers map with a special prefix. This prefixing is an implementation // detail, so we should hide it and _not_ mutate the user-visible headers. // // Note that this is _very_ finicky and difficult to test with net/http, // since correctness depends on low-level framing details. Breaking this // logic breaks Envoy's gRPC-Web translation. for key, values := range mergedTrailers { for _, value := range values { // These are potentially user-supplied, so we can't assume they're in // canonical form. hc.responseWriter.Header().Add(http.TrailerPrefix+key, value) } } return nil } type grpcMarshaler struct { envelopeWriter } func (m *grpcMarshaler) MarshalWebTrailers(trailer http.Header) *Error { raw := m.envelopeWriter.bufferPool.Get() defer m.envelopeWriter.bufferPool.Put(raw) for key, values := range trailer { // Per the Go specification, keys inserted during iteration may be produced // later in the iteration or may be skipped. For safety, avoid mutating the // map if the key is already lower-cased. lower := strings.ToLower(key) if key == lower { continue } delete(trailer, key) trailer[lower] = values } if err := trailer.Write(raw); err != nil { return errorf(CodeInternal, "format trailers: %w", err) } return m.Write(&envelope{ Data: raw, Flags: grpcFlagEnvelopeTrailer, }) } type grpcUnmarshaler struct { envelopeReader web bool webTrailer http.Header } func (u *grpcUnmarshaler) Unmarshal(message any) *Error { err := u.envelopeReader.Unmarshal(message) if err == nil { return nil } if !errors.Is(err, errSpecialEnvelope) { return err } env := u.last data := env.Data u.last.Data = nil // don't keep a reference to it defer u.bufferPool.Put(data) if !u.web || !env.IsSet(grpcFlagEnvelopeTrailer) { return errorf(CodeInternal, "protocol error: invalid envelope flags %d", env.Flags) } // Per the gRPC-Web specification, trailers should be encoded as an HTTP/1 // headers block _without_ the terminating newline. To make the headers // parseable by net/textproto, we need to add the newline. if err := data.WriteByte('\n'); err != nil { return errorf(CodeInternal, "unmarshal web trailers: %w", err) } bufferedReader := bufio.NewReader(data) mimeReader := textproto.NewReader(bufferedReader) mimeHeader, mimeErr := mimeReader.ReadMIMEHeader() if mimeErr != nil { return errorf( CodeInternal, "gRPC-Web protocol error: trailers invalid: %w", mimeErr, ) } u.webTrailer = http.Header(mimeHeader) return errSpecialEnvelope } func (u *grpcUnmarshaler) WebTrailer() http.Header { return u.webTrailer } func grpcValidateResponse( response *http.Response, header http.Header, availableCompressors readOnlyCompressionPools, web bool, codecName string, ) *Error { if response.StatusCode != http.StatusOK { return errorf(httpToCode(response.StatusCode), "HTTP status %v", response.Status) } if err := grpcValidateResponseContentType( web, codecName, getHeaderCanonical(response.Header, headerContentType), ); err != nil { return err } if compression := getHeaderCanonical(response.Header, grpcHeaderCompression); compression != "" && compression != compressionIdentity && !availableCompressors.Contains(compression) { // Per https://github.com/grpc/grpc/blob/master/doc/compression.md, we // should return CodeInternal and specify acceptable compression(s) (in // addition to setting the Grpc-Accept-Encoding header). return errorf( CodeInternal, "unknown encoding %q: accepted encodings are %v", compression, availableCompressors.CommaSeparatedNames(), ) } // The response is valid, so we should expose the headers. mergeHeaders(header, response.Header) return nil } // The gRPC wire protocol specifies that errors should be serialized using the // binary Protobuf format, even if the messages in the request/response stream // use a different codec. Consequently, this function needs a Protobuf codec to // unmarshal error information in the headers. // // A nil error is only returned when a grpc-status key IS present, but it // indicates a code of zero (no error). If no grpc-status key is present, this // returns a non-nil *Error that wraps errTrailersWithoutGRPCStatus. func grpcErrorFromTrailer(protobuf Codec, trailer http.Header) *Error { codeHeader := getHeaderCanonical(trailer, grpcHeaderStatus) if codeHeader == "" { // If there are no trailers at all, that's an internal error. // But if it's an error determining the status code from the // trailers, it's unknown. code := CodeUnknown if len(trailer) == 0 { code = CodeInternal } return NewError(code, errTrailersWithoutGRPCStatus) } if codeHeader == "0" { return nil } code, err := strconv.ParseUint(codeHeader, 10 /* base */, 32 /* bitsize */) if err != nil { return errorf(CodeUnknown, "protocol error: invalid error code %q", codeHeader) } message, err := grpcPercentDecode(getHeaderCanonical(trailer, grpcHeaderMessage)) if err != nil { return errorf(CodeInternal, "protocol error: invalid error message %q", message) } retErr := NewWireError(Code(code), errors.New(message)) detailsBinaryEncoded := getHeaderCanonical(trailer, grpcHeaderDetails) if len(detailsBinaryEncoded) > 0 { detailsBinary, err := DecodeBinaryHeader(detailsBinaryEncoded) if err != nil { return errorf(CodeInternal, "server returned invalid grpc-status-details-bin trailer: %w", err) } var status statusv1.Status if err := protobuf.Unmarshal(detailsBinary, &status); err != nil { return errorf(CodeInternal, "server returned invalid protobuf for error details: %w", err) } for _, d := range status.GetDetails() { retErr.details = append(retErr.details, &ErrorDetail{pbAny: d}) } // Prefer the Protobuf-encoded data to the headers (grpc-go does this too). retErr.code = Code(status.GetCode()) retErr.err = errors.New(status.GetMessage()) } return retErr } func grpcParseTimeout(timeout string) (time.Duration, error) { if timeout == "" { return 0, errNoTimeout } unit, err := grpcTimeoutUnitLookup(timeout[len(timeout)-1]) if err != nil { return 0, err } num, err := strconv.ParseInt(timeout[:len(timeout)-1], 10 /* base */, 64 /* bitsize */) if err != nil || num < 0 { return 0, fmt.Errorf("protocol error: invalid timeout %q", timeout) } if num > 99999999 { // timeout must be ASCII string of at most 8 digits return 0, fmt.Errorf("protocol error: timeout %q is too long", timeout) } const grpcTimeoutMaxHours = math.MaxInt64 / int64(time.Hour) // how many hours fit into a time.Duration? if unit == time.Hour && num > grpcTimeoutMaxHours { // Timeout is effectively unbounded, so ignore it. The grpc-go // implementation does the same thing. return 0, errNoTimeout } return time.Duration(num) * unit, nil } func grpcEncodeTimeout(timeout time.Duration) string { if timeout <= 0 { return "0n" } // The gRPC protocol limits timeouts to 8 characters (not counting the unit), // so timeouts must be strictly less than 1e8 of the appropriate unit. const grpcTimeoutMaxValue = 1e8 var ( size time.Duration unit byte ) switch { case timeout < time.Nanosecond*grpcTimeoutMaxValue: size, unit = time.Nanosecond, 'n' case timeout < time.Microsecond*grpcTimeoutMaxValue: size, unit = time.Microsecond, 'u' case timeout < time.Millisecond*grpcTimeoutMaxValue: size, unit = time.Millisecond, 'm' case timeout < time.Second*grpcTimeoutMaxValue: size, unit = time.Second, 'S' case timeout < time.Minute*grpcTimeoutMaxValue: size, unit = time.Minute, 'M' default: // time.Duration is an int64 number of nanoseconds, so the largest // expressible duration is less than 1e8 hours. size, unit = time.Hour, 'H' } buf := make([]byte, 0, 9) buf = strconv.AppendInt(buf, int64(timeout/size), 10 /* base */) buf = append(buf, unit) return string(buf) } func grpcTimeoutUnitLookup(unit byte) (time.Duration, error) { switch unit { case 'n': return time.Nanosecond, nil case 'u': return time.Microsecond, nil case 'm': return time.Millisecond, nil case 'S': return time.Second, nil case 'M': return time.Minute, nil case 'H': return time.Hour, nil default: return 0, fmt.Errorf("protocol error: timeout has invalid unit %q", unit) } } func grpcCodecFromContentType(web bool, contentType string) string { if (!web && contentType == grpcContentTypeDefault) || (web && contentType == grpcWebContentTypeDefault) { // implicitly protobuf return codecNameProto } prefix := grpcContentTypePrefix if web { prefix = grpcWebContentTypePrefix } return strings.TrimPrefix(contentType, prefix) } func grpcContentTypeFromCodecName(web bool, name string) string { if web { return grpcWebContentTypePrefix + name } if name == codecNameProto { // For compatibility with Google Cloud Platform's frontends, prefer an // implicit default codec. See // https://github.com/connectrpc/connect-go/pull/655#issuecomment-1915754523 // for details. return grpcContentTypeDefault } return grpcContentTypePrefix + name } func grpcErrorToTrailer(trailer http.Header, protobuf Codec, err error) { if err == nil { setHeaderCanonical(trailer, grpcHeaderStatus, "0") // zero is the gRPC OK status return } if connectErr, ok := asError(err); ok && !connectErr.wireErr { mergeMetadataHeaders(trailer, connectErr.meta) } var ( status = grpcStatusFromError(err) code = status.GetCode() message = status.GetMessage() bin []byte ) if len(status.Details) > 0 { var binErr error bin, binErr = protobuf.Marshal(status) if binErr != nil { code = int32(CodeInternal) message = fmt.Sprintf("marshal protobuf status: %v", binErr) } } setHeaderCanonical(trailer, grpcHeaderStatus, strconv.Itoa(int(code))) setHeaderCanonical(trailer, grpcHeaderMessage, grpcPercentEncode(message)) if len(bin) > 0 { setHeaderCanonical(trailer, grpcHeaderDetails, EncodeBinaryHeader(bin)) } } func grpcStatusFromError(err error) *statusv1.Status { status := &statusv1.Status{ Code: int32(CodeUnknown), Message: err.Error(), } if connectErr, ok := asError(err); ok { status.Code = int32(connectErr.Code()) status.Message = connectErr.Message() status.Details = connectErr.detailsAsAny() } return status } // grpcPercentEncode follows RFC 3986 Section 2.1 and the gRPC HTTP/2 spec. // It's a variant of URL-encoding with fewer reserved characters. It's intended // to take UTF-8 encoded text and escape non-ASCII bytes so that they're valid // HTTP/1 headers, while still maximizing readability of the data on the wire. // // The grpc-message trailer (used for human-readable error messages) should be // percent-encoded. // // References: // // https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#responses // https://datatracker.ietf.org/doc/html/rfc3986#section-2.1 func grpcPercentEncode(msg string) string { var hexCount int for i := 0; i < len(msg); i++ { if grpcShouldEscape(msg[i]) { hexCount++ } } if hexCount == 0 { return msg } // We need to escape some characters, so we'll need to allocate a new string. var out strings.Builder out.Grow(len(msg) + 2*hexCount) for i := 0; i < len(msg); i++ { switch char := msg[i]; { case grpcShouldEscape(char): out.WriteByte('%') out.WriteByte(upperhex[char>>4]) out.WriteByte(upperhex[char&15]) default: out.WriteByte(char) } } return out.String() } func grpcPercentDecode(input string) (string, error) { percentCount := 0 for i := 0; i < len(input); { switch input[i] { case '%': percentCount++ if err := validateHex(input[i:]); err != nil { return "", err } i += 3 default: i++ } } if percentCount == 0 { return input, nil } // We need to unescape some characters, so we'll need to allocate a new string. var out strings.Builder out.Grow(len(input) - 2*percentCount) for i := 0; i < len(input); i++ { switch input[i] { case '%': out.WriteByte(unhex(input[i+1])<<4 | unhex(input[i+2])) i += 2 default: out.WriteByte(input[i]) } } return out.String(), nil } // Characters that need to be escaped are defined in gRPC's HTTP/2 spec. // They're different from the generic set defined in RFC 3986. func grpcShouldEscape(char byte) bool { return char < ' ' || char > '~' || char == '%' } func unhex(char byte) byte { switch { case '0' <= char && char <= '9': return char - '0' case 'a' <= char && char <= 'f': return char - 'a' + 10 case 'A' <= char && char <= 'F': return char - 'A' + 10 } return 0 } func isHex(char byte) bool { return ('0' <= char && char <= '9') || ('a' <= char && char <= 'f') || ('A' <= char && char <= 'F') } func validateHex(input string) error { if len(input) < 3 || input[0] != '%' || !isHex(input[1]) || !isHex(input[2]) { if len(input) > 3 { input = input[:3] } return fmt.Errorf("invalid percent-encoded string %q", input) } return nil } func grpcValidateResponseContentType(web bool, requestCodecName string, responseContentType string) *Error { // Responses must have valid content-type that indicates same codec as the request. bare, prefix := grpcContentTypeDefault, grpcContentTypePrefix if web { bare, prefix = grpcWebContentTypeDefault, grpcWebContentTypePrefix } if responseContentType == prefix+requestCodecName || (requestCodecName == codecNameProto && responseContentType == bare) { return nil } expectedContentType := bare if requestCodecName != codecNameProto { expectedContentType = prefix + requestCodecName } code := CodeInternal if responseContentType != bare && !strings.HasPrefix(responseContentType, prefix) { // Doesn't even look like a gRPC response? Use code "unknown". code = CodeUnknown } return errorf( code, "invalid content-type: %q; expecting %q", responseContentType, expectedContentType, ) }