grpc/grpc-go · warning

malformed grpc-timeout

Error message

malformed grpc-timeout: %v

What it means

Raised server-side by gRPC-Go when an inbound HTTP/2 request carries a 'grpc-timeout' header whose value fails decodeTimeout parsing (internal/transport/handler_server.go:108). The header format is '<digits><unit>' where unit is one of H, M, S, m, u, n and the digit part is at most 8 digits (total length 2-9). decodeTimeout rejects: strings shorter than 2 chars, longer than 9 chars, an unrecognized unit byte, or a non-numeric prefix (internal/transport/http_util.go:188). On failure the server replies HTTP 400 and returns codes.Internal to the caller. It is a request-rejection error, not a transport crash.

Solutions

  1. Do not set grpc-timeout yourself — let the grpc-go client encode it: it uses grpcutil.EncodeDuration (internal/grpcutil/encode_duration.go:41), which always emits a valid '<digits><unit>' string. Remove any code that writes the header by hand or appends it via metadata/headers.
  2. If you must format it manually, use exactly the 6 legal units in gRPC's spec: H (hour), M (minute), S (second), m (millisecond), u (microsecond), n (nanosecond), with at most 8 decimal digits, e.g. '30S' or '200m'. Validate length is 2-9 bytes and the last byte is one of H/M/S/m/u/n before sending.
  3. Inspect the exact bytes on the wire with grpcurl --emit-defaults, a net/http DumpRequest, or h2c/hexdump to see what value the client actually sent — the wrapped error message in 'malformed grpc-timeout: <err>' tells you which decodeTimeout branch failed (too short / too long / bad unit / parse error).
  4. Check every proxy, sidecar, load balancer, and middleware in the path for a rule that rewrites, trims, or upper-cases the grpc-timeout header; gRPC unit bytes are case-sensitive ('m' vs 'M') and proxies sometimes 'fix' them.
  5. If you operate the server and cannot fix the client, log r.Header.Get("grpc-timeout") before decodeTimeout in a wrapped handler to identify the offending caller, then push the format fix upstream.

Example fix

// before (hand-rolled grpc-timeout — wrong format)
req.Header.Set("grpc-timeout", "30s")
// decodeTimeout: unit 's' not recognized -> HTTP 400 malformed grpc-timeout

// after — let grpc-go encode it; only use the public context API
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
resp, err := client.MyMethod(ctx, req)

// after — if you format manually, use the legal units
req.Header.Set("grpc-timeout", "30S") // 30 seconds
Defensive patterns

Strategy: validation

Validate before calling

// Validate a grpc-timeout value before sending it.
// Returns true iff decodeTimeout would accept it.
func isValidGrpcTimeout(s string) bool {
    if len(s) < 2 || len(s) > 9 {
        return false
    }
    switch s[len(s)-1] {
    case 'H', 'M', 'S', 'm', 'u', 'n':
    default:
        return false
    }
    for i := 0; i < len(s)-1; i++ {
        if s[i] < '0' || s[i] > '9' {
            return false
        }
    }
    return true
}

// Preferred: never format it yourself.
import "google.golang.org/grpc/internal/grpcutil"
hdr := grpcutil.EncodeDuration(30 * time.Second) // always valid

Type guard

// Narrow a context deadline into a legal grpc-timeout string,
// guaranteeing the server's decodeTimeout cannot reject it.
func safeGrpcTimeout(ctx context.Context) (string, bool) {
    dl, ok := ctx.Deadline()
    if !ok {
        return "", false // no deadline -> omit header, server skips it
    }
    return grpcutil.EncodeDuration(time.Until(dl)), true
}

Prevention

When it happens

Trigger: A client sends the literal string 'grpc-timeout: tomorrow' (unit 'w' not recognized — see handler_server_test.go:165), or 'grpc-timeout: 18f6n' (9 chars but 'f6' is not decimal — see transport_test.go:2359). Also fires for a value with no unit ('100'), a value longer than 9 bytes ('123456789H'), or a value shorter than 2 bytes. The check runs only when the header is present and non-empty (handler_server.go:105), so omitting grpc-timeout entirely never triggers it. Fires in both the http/2 transport path (http2_server.go:457) and the net/http Handler path (handler_server.go:108) used when grpc is mounted behind a reverse proxy.

Common situations: Hand-rolled or non-grpc clients (raw curl, Postman, a Python requests script) setting a human-readable timeout like '30s' instead of the gRPC wire format '30S'. A proxy or API gateway (Envoy, nginx, Kong, an AWS ALB with a timeout-rewrite rule) that rewrites or 'normalizes' the header. A transcoding layer (grpc-gateway, gRPC-Web) that passes a context deadline through as the wrong format. Mismatched units between producer and consumer of the header (e.g. 'ms' written as a suffix instead of 'm'). Interop testing against an old or buggy client library.

Understand the failure class

Related errors


AI-assisted analysis of grpc/grpc-go@0c51461d27 (2026-08-11). Data as JSON: /api/errors/befaf199eecb5138. Report an issue: GitHub.

Appendix: source

Thrown at internal/transport/handler_server.go:109

	}
	st := &serverHandlerTransport{
		rw:             w,
		req:            r,
		closedCh:       make(chan struct{}),
		writes:         make(chan func()),
		peer:           p,
		contentType:    contentType,
		contentSubtype: contentSubtype,
		stats:          stats,
		bufferPool:     bufferPool,
	}
	st.logger = prefixLoggerForServerHandlerTransport(st)

	if v := r.Header.Get("grpc-timeout"); v != "" {
		to, err := decodeTimeout(v)
		if err != nil {
			msg := fmt.Sprintf("malformed grpc-timeout: %v", err)
			http.Error(w, msg, http.StatusBadRequest)
			return nil, status.Error(codes.Internal, msg)
		}
		st.timeoutSet = true
		st.timeout = to
	}

	metakv := []string{"content-type", contentType}
	if r.Host != "" {
		metakv = append(metakv, ":authority", r.Host)
	}
	for k, vv := range r.Header {
		k = strings.ToLower(k)
		if isReservedHeader(k) && !isWhitelistedHeader(k) {
			continue
		}
		for _, v := range vv {
			v, err := decodeMetadataHeader(k, v)
			if err != nil {

View on GitHub (pinned to 0c51461d27)