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
- 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.
- 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.
- 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).
- 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.
- 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
- Never set grpc-timeout by hand — derive it from context.WithTimeout and let grpc-go's EncodeDuration format it.
- Unit bytes are case-sensitive: 'S' is seconds, 's' is invalid; document this anywhere a proxy formats the header.
- Treat any HTTP/2 middleware that rewrites request headers as a suspect; capture grpc-timeout on the server to confirm it arrived intact.
- If you build a custom client, add a unit test that round-trips EncodeDuration -> decodeTimeout to catch format regressions.
- Log the raw header value at the trust boundary when a 400 occurs so offending callers are identifiable without reproducing locally.
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
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- malformed binary metadata
- grpc: the server has been stopped
- missing server_listener_resource_name_template in the…
- %v: %v
- "allow_rules" is not present
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)