github.com/cloudwego/kitex@v0.9.0/pkg/remote/trans/nphttp2/grpc/http_util.go (about)

     1  /*
     2   *
     3   * Copyright 2014 gRPC authors.
     4   *
     5   * Licensed under the Apache License, Version 2.0 (the "License");
     6   * you may not use this file except in compliance with the License.
     7   * You may obtain a copy of the License at
     8   *
     9   *     http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the License for the specific language governing permissions and
    15   * limitations under the License.
    16   *
    17   * This file may have been modified by CloudWeGo authors. All CloudWeGo
    18   * Modifications are Copyright 2021 CloudWeGo Authors.
    19   */
    20  
    21  package grpc
    22  
    23  import (
    24  	"bytes"
    25  	"encoding/base64"
    26  	"fmt"
    27  	"math"
    28  	"net/http"
    29  	"strconv"
    30  	"strings"
    31  	"time"
    32  	"unicode/utf8"
    33  
    34  	"golang.org/x/net/http2"
    35  	"golang.org/x/net/http2/hpack"
    36  	spb "google.golang.org/genproto/googleapis/rpc/status"
    37  	"google.golang.org/protobuf/proto"
    38  
    39  	"github.com/cloudwego/kitex/pkg/kerrors"
    40  	"github.com/cloudwego/kitex/pkg/klog"
    41  	"github.com/cloudwego/kitex/pkg/remote/trans/nphttp2/codes"
    42  	"github.com/cloudwego/kitex/pkg/remote/trans/nphttp2/grpc/grpcframe"
    43  	"github.com/cloudwego/kitex/pkg/remote/trans/nphttp2/status"
    44  	"github.com/cloudwego/kitex/pkg/utils"
    45  )
    46  
    47  const (
    48  	// http2MaxFrameLen specifies the max length of a HTTP2 frame.
    49  	http2MaxFrameLen = 16384 // 16KB frame
    50  	// http://http2.github.io/http2-spec/#SettingValues
    51  	http2InitHeaderTableSize = 4096
    52  	// baseContentType is the base content-type for gRPC.  This is a valid
    53  	// content-type on it's own, but can also include a content-subtype such as
    54  	// "proto" as a suffix after "+" or ";".  See
    55  	// https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests
    56  	// for more details.
    57  	baseContentType = "application/grpc"
    58  )
    59  
    60  var (
    61  	// ClientPreface http2 preface message
    62  	ClientPreface = []byte(http2.ClientPreface)
    63  	// ClientPrefaceLen preface length
    64  	ClientPrefaceLen = len(ClientPreface)
    65  	http2ErrConvTab  = map[http2.ErrCode]codes.Code{
    66  		http2.ErrCodeNo:                 codes.Internal,
    67  		http2.ErrCodeProtocol:           codes.Internal,
    68  		http2.ErrCodeInternal:           codes.Internal,
    69  		http2.ErrCodeFlowControl:        codes.ResourceExhausted,
    70  		http2.ErrCodeSettingsTimeout:    codes.Internal,
    71  		http2.ErrCodeStreamClosed:       codes.Internal,
    72  		http2.ErrCodeFrameSize:          codes.Internal,
    73  		http2.ErrCodeRefusedStream:      codes.Unavailable,
    74  		http2.ErrCodeCancel:             codes.Canceled,
    75  		http2.ErrCodeCompression:        codes.Internal,
    76  		http2.ErrCodeConnect:            codes.Internal,
    77  		http2.ErrCodeEnhanceYourCalm:    codes.ResourceExhausted,
    78  		http2.ErrCodeInadequateSecurity: codes.PermissionDenied,
    79  		http2.ErrCodeHTTP11Required:     codes.Internal,
    80  	}
    81  	statusCodeConvTab = map[codes.Code]http2.ErrCode{
    82  		codes.Internal:          http2.ErrCodeInternal,
    83  		codes.Canceled:          http2.ErrCodeCancel,
    84  		codes.Unavailable:       http2.ErrCodeRefusedStream,
    85  		codes.ResourceExhausted: http2.ErrCodeEnhanceYourCalm,
    86  		codes.PermissionDenied:  http2.ErrCodeInadequateSecurity,
    87  	}
    88  	// HTTPStatusConvTab is the HTTP status code to gRPC error code conversion table.
    89  	HTTPStatusConvTab = map[int]codes.Code{
    90  		// 400 Bad Request - INTERNAL.
    91  		http.StatusBadRequest: codes.Internal,
    92  		// 401 Unauthorized  - UNAUTHENTICATED.
    93  		http.StatusUnauthorized: codes.Unauthenticated,
    94  		// 403 Forbidden - PERMISSION_DENIED.
    95  		http.StatusForbidden: codes.PermissionDenied,
    96  		// 404 Not Found - UNIMPLEMENTED.
    97  		http.StatusNotFound: codes.Unimplemented,
    98  		// 429 Too Many Requests - UNAVAILABLE.
    99  		http.StatusTooManyRequests: codes.Unavailable,
   100  		// 502 Bad Gateway - UNAVAILABLE.
   101  		http.StatusBadGateway: codes.Unavailable,
   102  		// 503 Service Unavailable - UNAVAILABLE.
   103  		http.StatusServiceUnavailable: codes.Unavailable,
   104  		// 504 Gateway timeout - UNAVAILABLE.
   105  		http.StatusGatewayTimeout: codes.Unavailable,
   106  	}
   107  )
   108  
   109  type parsedHeaderData struct {
   110  	encoding       string
   111  	acceptEncoding string
   112  	// statusGen caches the stream status received from the trailer the server
   113  	// sent.  Client side only.  Do not access directly.  After all trailers are
   114  	// parsed, use the status method to retrieve the status.
   115  	statusGen    *status.Status
   116  	bizStatusErr kerrors.BizStatusErrorIface
   117  	// rawStatusCode and rawStatusMsg are set from the raw trailer fields and are not
   118  	// intended for direct access outside of parsing.
   119  	rawStatusCode  *int
   120  	rawStatusMsg   string
   121  	bizStatusCode  *int
   122  	bizStatusExtra map[string]string
   123  	httpStatus     *int
   124  	// Server side only fields.
   125  	timeoutSet bool
   126  	timeout    time.Duration
   127  	method     string
   128  	// key-value metadata map from the peer.
   129  	mdata          map[string][]string
   130  	statsTags      []byte
   131  	statsTrace     []byte
   132  	contentSubtype string
   133  
   134  	// isGRPC field indicates whether the peer is speaking gRPC (otherwise HTTP).
   135  	//
   136  	// We are in gRPC mode (peer speaking gRPC) if:
   137  	// 	* We are client side and have already received a HEADER frame that indicates gRPC peer.
   138  	//  * The header contains valid  a content-type, i.e. a string starts with "application/grpc"
   139  	// And we should handle error specific to gRPC.
   140  	//
   141  	// Otherwise (i.e. a content-type string starts without "application/grpc", or does not exist), we
   142  	// are in HTTP fallback mode, and should handle error specific to HTTP.
   143  	isGRPC         bool
   144  	grpcErr        error
   145  	httpErr        error
   146  	contentTypeErr string
   147  }
   148  
   149  // decodeState configures decoding criteria and records the decoded data.
   150  type decodeState struct {
   151  	// whether decoding on server side or not
   152  	serverSide bool
   153  
   154  	// Records the states during HPACK decoding. It will be filled with info parsed from HTTP HEADERS
   155  	// frame once decodeHeader function has been invoked and returned.
   156  	data parsedHeaderData
   157  }
   158  
   159  // isReservedHeader checks whether hdr belongs to HTTP2 headers
   160  // reserved by gRPC protocol. Any other headers are classified as the
   161  // user-specified metadata.
   162  func isReservedHeader(hdr string) bool {
   163  	if hdr != "" && hdr[0] == ':' {
   164  		return true
   165  	}
   166  	switch hdr {
   167  	case "content-type",
   168  		"user-agent",
   169  		"grpc-message-type",
   170  		"grpc-encoding",
   171  		"grpc-message",
   172  		"grpc-status",
   173  		"grpc-timeout",
   174  		"grpc-status-details-bin",
   175  		// Intentionally exclude grpc-previous-rpc-attempts and
   176  		// grpc-retry-pushback-ms, which are "reserved", but their API
   177  		// intentionally works via metadata.
   178  		"te":
   179  		return true
   180  	default:
   181  		return false
   182  	}
   183  }
   184  
   185  // isWhitelistedHeader checks whether hdr should be propagated into metadata
   186  // visible to users, even though it is classified as "reserved", above.
   187  func isWhitelistedHeader(hdr string) bool {
   188  	switch hdr {
   189  	case ":authority", "user-agent":
   190  		return true
   191  	default:
   192  		return false
   193  	}
   194  }
   195  
   196  // contentSubtype returns the content-subtype for the given content-type.  The
   197  // given content-type must be a valid content-type that starts with
   198  // "application/grpc". A content-subtype will follow "application/grpc" after a
   199  // "+" or ";". See
   200  // https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests for
   201  // more details.
   202  //
   203  // If contentType is not a valid content-type for gRPC, the boolean
   204  // will be false, otherwise true. If content-type == "application/grpc",
   205  // "application/grpc+", or "application/grpc;", the boolean will be true,
   206  // but no content-subtype will be returned.
   207  //
   208  // contentType is assumed to be lowercase already.
   209  func contentSubtype(contentType string) (string, bool) {
   210  	if contentType == baseContentType {
   211  		return "", true
   212  	}
   213  	if !strings.HasPrefix(contentType, baseContentType) {
   214  		return "", false
   215  	}
   216  	// guaranteed since != baseContentType and has baseContentType prefix
   217  	switch contentType[len(baseContentType)] {
   218  	case '+', ';':
   219  		// this will return true for "application/grpc+" or "application/grpc;"
   220  		// which the previous validContentType function tested to be valid, so we
   221  		// just say that no content-subtype is specified in this case
   222  		return contentType[len(baseContentType)+1:], true
   223  	default:
   224  		return "", false
   225  	}
   226  }
   227  
   228  // contentSubtype is assumed to be lowercase
   229  func contentType(contentSubtype string) string {
   230  	if contentSubtype == "" {
   231  		return baseContentType
   232  	}
   233  	return baseContentType + "+" + contentSubtype
   234  }
   235  
   236  func (d *decodeState) status() *status.Status {
   237  	if d.data.statusGen == nil {
   238  		// No status-details were provided; generate status using code/msg.
   239  		d.data.statusGen = status.New(codes.Code(safeCastInt32(*(d.data.rawStatusCode))), d.data.rawStatusMsg)
   240  	}
   241  	return d.data.statusGen
   242  }
   243  
   244  func (d *decodeState) bizStatusErr() kerrors.BizStatusErrorIface {
   245  	if d.data.bizStatusErr == nil && d.data.bizStatusCode != nil {
   246  		d.data.bizStatusErr = kerrors.NewGRPCBizStatusErrorWithExtra(
   247  			safeCastInt32(*(d.data.bizStatusCode)), d.data.rawStatusMsg, d.data.bizStatusExtra)
   248  		if st, ok := d.data.bizStatusErr.(kerrors.GRPCStatusIface); ok {
   249  			st.SetGRPCStatus(d.status())
   250  		}
   251  	}
   252  	return d.data.bizStatusErr
   253  }
   254  
   255  // safeCastInt32 casts the number from int to int32 in safety.
   256  func safeCastInt32(n int) int32 {
   257  	if n > math.MaxInt32 || n < math.MinInt32 {
   258  		panic(fmt.Sprintf("Cast int to int32 failed, due to overflow, n=%d", n))
   259  	}
   260  	return int32(n)
   261  }
   262  
   263  const binHdrSuffix = "-bin"
   264  
   265  func encodeBinHeader(v []byte) string {
   266  	return base64.RawStdEncoding.EncodeToString(v)
   267  }
   268  
   269  func decodeBinHeader(v string) ([]byte, error) {
   270  	if len(v)%4 == 0 {
   271  		// Input was padded, or padding was not necessary.
   272  		return base64.StdEncoding.DecodeString(v)
   273  	}
   274  	return base64.RawStdEncoding.DecodeString(v)
   275  }
   276  
   277  func encodeMetadataHeader(k, v string) string {
   278  	if strings.HasSuffix(k, binHdrSuffix) {
   279  		return encodeBinHeader(([]byte)(v))
   280  	}
   281  	return v
   282  }
   283  
   284  func decodeMetadataHeader(k, v string) (string, error) {
   285  	if strings.HasSuffix(k, binHdrSuffix) {
   286  		b, err := decodeBinHeader(v)
   287  		return string(b), err
   288  	}
   289  	return v, nil
   290  }
   291  
   292  func (d *decodeState) decodeHeader(frame *grpcframe.MetaHeadersFrame) error {
   293  	// frame.Truncated is set to true when framer detects that the current header
   294  	// list size hits MaxHeaderListSize limit.
   295  	if frame.Truncated {
   296  		return status.New(codes.Internal, "peer header list size exceeded limit").Err()
   297  	}
   298  
   299  	for _, hf := range frame.Fields {
   300  		d.processHeaderField(hf)
   301  	}
   302  
   303  	if d.data.isGRPC {
   304  		if d.data.grpcErr != nil {
   305  			return d.data.grpcErr
   306  		}
   307  		if d.serverSide {
   308  			return nil
   309  		}
   310  		if d.data.rawStatusCode == nil && d.data.statusGen == nil {
   311  			// gRPC status doesn't exist.
   312  			// Set rawStatusCode to be unknown and return nil error.
   313  			// So that, if the stream has ended this Unknown status
   314  			// will be propagated to the user.
   315  			// Otherwise, it will be ignored. In which case, status from
   316  			// a later trailer, that has StreamEnded flag set, is propagated.
   317  			code := int(codes.Unknown)
   318  			d.data.rawStatusCode = &code
   319  		}
   320  		return nil
   321  	}
   322  
   323  	// HTTP fallback mode
   324  	if d.data.httpErr != nil {
   325  		return d.data.httpErr
   326  	}
   327  
   328  	var (
   329  		code = codes.Internal // when header does not include HTTP status, return INTERNAL
   330  		ok   bool
   331  	)
   332  
   333  	if d.data.httpStatus != nil {
   334  		code, ok = HTTPStatusConvTab[*(d.data.httpStatus)]
   335  		if !ok {
   336  			code = codes.Unknown
   337  		}
   338  	}
   339  
   340  	return status.New(code, d.constructHTTPErrMsg()).Err()
   341  }
   342  
   343  // constructErrMsg constructs error message to be returned in HTTP fallback mode.
   344  // Format: HTTP status code and its corresponding message + content-type error message.
   345  func (d *decodeState) constructHTTPErrMsg() string {
   346  	var errMsgs []string
   347  
   348  	if d.data.httpStatus == nil {
   349  		errMsgs = append(errMsgs, "malformed header: missing HTTP status")
   350  	} else {
   351  		errMsgs = append(errMsgs, fmt.Sprintf("%s: HTTP status code %d", http.StatusText(*(d.data.httpStatus)), *d.data.httpStatus))
   352  	}
   353  
   354  	if d.data.contentTypeErr == "" {
   355  		errMsgs = append(errMsgs, "transport: missing content-type field")
   356  	} else {
   357  		errMsgs = append(errMsgs, d.data.contentTypeErr)
   358  	}
   359  
   360  	return strings.Join(errMsgs, "; ")
   361  }
   362  
   363  func (d *decodeState) addMetadata(k, v string) {
   364  	if d.data.mdata == nil {
   365  		d.data.mdata = make(map[string][]string)
   366  	}
   367  	d.data.mdata[k] = append(d.data.mdata[k], v)
   368  }
   369  
   370  func (d *decodeState) processHeaderField(f hpack.HeaderField) {
   371  	switch f.Name {
   372  	case "content-type":
   373  		contentSubtype, validContentType := contentSubtype(f.Value)
   374  		if !validContentType {
   375  			d.data.contentTypeErr = fmt.Sprintf("transport: received the unexpected content-type %q", f.Value)
   376  			return
   377  		}
   378  		d.data.contentSubtype = contentSubtype
   379  		// TODO: do we want to propagate the whole content-type in the metadata,
   380  		// or come up with a way to just propagate the content-subtype if it was set?
   381  		// ie {"content-type": "application/grpc+proto"} or {"content-subtype": "proto"}
   382  		// in the metadata?
   383  		d.addMetadata(f.Name, f.Value)
   384  		d.data.isGRPC = true
   385  	case "grpc-encoding":
   386  		d.data.encoding = f.Value
   387  	case "grpc-accept-encoding":
   388  		d.data.acceptEncoding = f.Value
   389  	case "grpc-status":
   390  		code, err := strconv.Atoi(f.Value)
   391  		if err != nil {
   392  			d.data.grpcErr = status.Errorf(codes.Internal, "transport: malformed grpc-status: %v", err)
   393  			return
   394  		}
   395  		d.data.rawStatusCode = &code
   396  	case "grpc-message":
   397  		d.data.rawStatusMsg = decodeGrpcMessage(f.Value)
   398  	case "biz-status":
   399  		code, err := strconv.Atoi(f.Value)
   400  		if err != nil {
   401  			d.data.grpcErr = status.Errorf(codes.Internal, "transport: malformed biz-status: %v", err)
   402  			return
   403  		}
   404  		d.data.bizStatusCode = &code
   405  	case "biz-extra":
   406  		extra, err := utils.JSONStr2Map(f.Value)
   407  		if err != nil {
   408  			d.data.grpcErr = status.Errorf(codes.Internal, "transport: malformed biz-extra: %v", err)
   409  			return
   410  		}
   411  		d.data.bizStatusExtra = extra
   412  	case "grpc-status-details-bin":
   413  		v, err := decodeBinHeader(f.Value)
   414  		if err != nil {
   415  			d.data.grpcErr = status.Errorf(codes.Internal, "transport: malformed grpc-status-details-bin: %v", err)
   416  			return
   417  		}
   418  		s := &spb.Status{}
   419  		if err := proto.Unmarshal(v, s); err != nil {
   420  			d.data.grpcErr = status.Errorf(codes.Internal, "transport: malformed grpc-status-details-bin: %v", err)
   421  			return
   422  		}
   423  		d.data.statusGen = status.FromProto(s)
   424  	case "grpc-timeout":
   425  		d.data.timeoutSet = true
   426  		var err error
   427  		if d.data.timeout, err = decodeTimeout(f.Value); err != nil {
   428  			d.data.grpcErr = status.Errorf(codes.Internal, "transport: malformed time-out: %v", err)
   429  		}
   430  	case ":path":
   431  		d.data.method = f.Value
   432  	case ":status":
   433  		code, err := strconv.Atoi(f.Value)
   434  		if err != nil {
   435  			d.data.httpErr = status.Errorf(codes.Internal, "transport: malformed http-status: %v", err)
   436  			return
   437  		}
   438  		d.data.httpStatus = &code
   439  	case "grpc-tags-bin":
   440  		v, err := decodeBinHeader(f.Value)
   441  		if err != nil {
   442  			d.data.grpcErr = status.Errorf(codes.Internal, "transport: malformed grpc-tags-bin: %v", err)
   443  			return
   444  		}
   445  		d.data.statsTags = v
   446  		d.addMetadata(f.Name, string(v))
   447  	case "grpc-trace-bin":
   448  		v, err := decodeBinHeader(f.Value)
   449  		if err != nil {
   450  			d.data.grpcErr = status.Errorf(codes.Internal, "transport: malformed grpc-trace-bin: %v", err)
   451  			return
   452  		}
   453  		d.data.statsTrace = v
   454  		d.addMetadata(f.Name, string(v))
   455  	default:
   456  		if isReservedHeader(f.Name) && !isWhitelistedHeader(f.Name) {
   457  			break
   458  		}
   459  		v, err := decodeMetadataHeader(f.Name, f.Value)
   460  		if err != nil {
   461  			klog.Errorf("Failed to decode metadata header (%q, %q): %v", f.Name, f.Value, err)
   462  			return
   463  		}
   464  		d.addMetadata(f.Name, v)
   465  	}
   466  }
   467  
   468  type timeoutUnit uint8
   469  
   470  const (
   471  	hour        timeoutUnit = 'H'
   472  	minute      timeoutUnit = 'M'
   473  	second      timeoutUnit = 'S'
   474  	millisecond timeoutUnit = 'm'
   475  	microsecond timeoutUnit = 'u'
   476  	nanosecond  timeoutUnit = 'n'
   477  )
   478  
   479  func timeoutUnitToDuration(u timeoutUnit) (d time.Duration, ok bool) {
   480  	switch u {
   481  	case hour:
   482  		return time.Hour, true
   483  	case minute:
   484  		return time.Minute, true
   485  	case second:
   486  		return time.Second, true
   487  	case millisecond:
   488  		return time.Millisecond, true
   489  	case microsecond:
   490  		return time.Microsecond, true
   491  	case nanosecond:
   492  		return time.Nanosecond, true
   493  	default:
   494  	}
   495  	return
   496  }
   497  
   498  const maxTimeoutValue int64 = 100000000 - 1
   499  
   500  // div does integer division and round-up the result. Note that this is
   501  // equivalent to (d+r-1)/r but has less chance to overflow.
   502  func div(d, r time.Duration) int64 {
   503  	if m := d % r; m > 0 {
   504  		return int64(d/r + 1)
   505  	}
   506  	return int64(d / r)
   507  }
   508  
   509  // TODO(zhaoq): It is the simplistic and not bandwidth efficient. Improve it.
   510  func encodeTimeout(t time.Duration) string {
   511  	if t <= 0 {
   512  		return "0n"
   513  	}
   514  	if d := div(t, time.Nanosecond); d <= maxTimeoutValue {
   515  		return strconv.FormatInt(d, 10) + "n"
   516  	}
   517  	if d := div(t, time.Microsecond); d <= maxTimeoutValue {
   518  		return strconv.FormatInt(d, 10) + "u"
   519  	}
   520  	if d := div(t, time.Millisecond); d <= maxTimeoutValue {
   521  		return strconv.FormatInt(d, 10) + "m"
   522  	}
   523  	if d := div(t, time.Second); d <= maxTimeoutValue {
   524  		return strconv.FormatInt(d, 10) + "S"
   525  	}
   526  	if d := div(t, time.Minute); d <= maxTimeoutValue {
   527  		return strconv.FormatInt(d, 10) + "M"
   528  	}
   529  	// Note that maxTimeoutValue * time.Hour > MaxInt64.
   530  	return strconv.FormatInt(div(t, time.Hour), 10) + "H"
   531  }
   532  
   533  func decodeTimeout(s string) (time.Duration, error) {
   534  	size := len(s)
   535  	if size < 2 {
   536  		return 0, fmt.Errorf("transport: timeout string is too short: %q", s)
   537  	}
   538  	if size > 9 {
   539  		// Spec allows for 8 digits plus the unit.
   540  		return 0, fmt.Errorf("transport: timeout string is too long: %q", s)
   541  	}
   542  	unit := timeoutUnit(s[size-1])
   543  	d, ok := timeoutUnitToDuration(unit)
   544  	if !ok {
   545  		return 0, fmt.Errorf("transport: timeout unit is not recognized: %q", s)
   546  	}
   547  	t, err := strconv.ParseInt(s[:size-1], 10, 64)
   548  	if err != nil {
   549  		return 0, err
   550  	}
   551  	const maxHours = math.MaxInt64 / int64(time.Hour)
   552  	if d == time.Hour && t > maxHours {
   553  		// This timeout would overflow math.MaxInt64; clamp it.
   554  		return time.Duration(math.MaxInt64), nil
   555  	}
   556  	return d * time.Duration(t), nil
   557  }
   558  
   559  const (
   560  	spaceByte   = ' '
   561  	tildeByte   = '~'
   562  	percentByte = '%'
   563  )
   564  
   565  // encodeGrpcMessage is used to encode status code in header field
   566  // "grpc-message". It does percent encoding and also replaces invalid utf-8
   567  // characters with Unicode replacement character.
   568  //
   569  // It checks to see if each individual byte in msg is an allowable byte, and
   570  // then either percent encoding or passing it through. When percent encoding,
   571  // the byte is converted into hexadecimal notation with a '%' prepended.
   572  func encodeGrpcMessage(msg string) string {
   573  	if msg == "" {
   574  		return ""
   575  	}
   576  	lenMsg := len(msg)
   577  	for i := 0; i < lenMsg; i++ {
   578  		c := msg[i]
   579  		if !(c >= spaceByte && c <= tildeByte && c != percentByte) {
   580  			return encodeGrpcMessageUnchecked(msg)
   581  		}
   582  	}
   583  	return msg
   584  }
   585  
   586  func encodeGrpcMessageUnchecked(msg string) string {
   587  	var buf bytes.Buffer
   588  	for len(msg) > 0 {
   589  		r, size := utf8.DecodeRuneInString(msg)
   590  		for _, b := range []byte(string(r)) {
   591  			if size > 1 {
   592  				// If size > 1, r is not ascii. Always do percent encoding.
   593  				buf.WriteString(fmt.Sprintf("%%%02X", b))
   594  				continue
   595  			}
   596  
   597  			// The for loop is necessary even if size == 1. r could be
   598  			// utf8.RuneError.
   599  			//
   600  			// fmt.Sprintf("%%%02X", utf8.RuneError) gives "%FFFD".
   601  			if b >= spaceByte && b <= tildeByte && b != percentByte {
   602  				buf.WriteByte(b)
   603  			} else {
   604  				buf.WriteString(fmt.Sprintf("%%%02X", b))
   605  			}
   606  		}
   607  		msg = msg[size:]
   608  	}
   609  	return buf.String()
   610  }
   611  
   612  // decodeGrpcMessage decodes the msg encoded by encodeGrpcMessage.
   613  func decodeGrpcMessage(msg string) string {
   614  	if msg == "" {
   615  		return ""
   616  	}
   617  	lenMsg := len(msg)
   618  	for i := 0; i < lenMsg; i++ {
   619  		if msg[i] == percentByte && i+2 < lenMsg {
   620  			return decodeGrpcMessageUnchecked(msg)
   621  		}
   622  	}
   623  	return msg
   624  }
   625  
   626  func decodeGrpcMessageUnchecked(msg string) string {
   627  	var buf bytes.Buffer
   628  	lenMsg := len(msg)
   629  	for i := 0; i < lenMsg; i++ {
   630  		c := msg[i]
   631  		if c == percentByte && i+2 < lenMsg {
   632  			parsed, err := strconv.ParseUint(msg[i+1:i+3], 16, 8)
   633  			if err != nil {
   634  				buf.WriteByte(c)
   635  			} else {
   636  				buf.WriteByte(byte(parsed))
   637  				i += 2
   638  			}
   639  		} else {
   640  			buf.WriteByte(c)
   641  		}
   642  	}
   643  	return buf.String()
   644  }