{"record":{"id":"befaf199eecb5138","repo":"grpc/grpc-go","slug":"malformed-grpc-timeout-v","errorCode":null,"errorMessage":"malformed grpc-timeout: %v","messagePattern":"malformed grpc-timeout: (.+?)","errorType":"http","errorClass":null,"httpStatus":400,"severity":"warning","filePath":"internal/transport/handler_server.go","lineNumber":109,"sourceCode":"\t}\n\tst := &serverHandlerTransport{\n\t\trw:             w,\n\t\treq:            r,\n\t\tclosedCh:       make(chan struct{}),\n\t\twrites:         make(chan func()),\n\t\tpeer:           p,\n\t\tcontentType:    contentType,\n\t\tcontentSubtype: contentSubtype,\n\t\tstats:          stats,\n\t\tbufferPool:     bufferPool,\n\t}\n\tst.logger = prefixLoggerForServerHandlerTransport(st)\n\n\tif v := r.Header.Get(\"grpc-timeout\"); v != \"\" {\n\t\tto, err := decodeTimeout(v)\n\t\tif err != nil {\n\t\t\tmsg := fmt.Sprintf(\"malformed grpc-timeout: %v\", err)\n\t\t\thttp.Error(w, msg, http.StatusBadRequest)\n\t\t\treturn nil, status.Error(codes.Internal, msg)\n\t\t}\n\t\tst.timeoutSet = true\n\t\tst.timeout = to\n\t}\n\n\tmetakv := []string{\"content-type\", contentType}\n\tif r.Host != \"\" {\n\t\tmetakv = append(metakv, \":authority\", r.Host)\n\t}\n\tfor k, vv := range r.Header {\n\t\tk = strings.ToLower(k)\n\t\tif isReservedHeader(k) && !isWhitelistedHeader(k) {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, v := range vv {\n\t\t\tv, err := decodeMetadataHeader(k, v)\n\t\t\tif err != nil {","sourceCodeStart":91,"sourceCodeEnd":127,"githubUrl":"https://github.com/grpc/grpc-go/blob/0c51461d27177d997e14c642fe18c11668fc09a3/internal/transport/handler_server.go#L91-L127","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before (hand-rolled grpc-timeout — wrong format)\nreq.Header.Set(\"grpc-timeout\", \"30s\")\n// decodeTimeout: unit 's' not recognized -> HTTP 400 malformed grpc-timeout\n\n// after — let grpc-go encode it; only use the public context API\nctx, cancel := context.WithTimeout(ctx, 30*time.Second)\ndefer cancel()\nresp, err := client.MyMethod(ctx, req)\n\n// after — if you format manually, use the legal units\nreq.Header.Set(\"grpc-timeout\", \"30S\") // 30 seconds","handlingStrategy":"validation","validationCode":"// Validate a grpc-timeout value before sending it.\n// Returns true iff decodeTimeout would accept it.\nfunc isValidGrpcTimeout(s string) bool {\n    if len(s) < 2 || len(s) > 9 {\n        return false\n    }\n    switch s[len(s)-1] {\n    case 'H', 'M', 'S', 'm', 'u', 'n':\n    default:\n        return false\n    }\n    for i := 0; i < len(s)-1; i++ {\n        if s[i] < '0' || s[i] > '9' {\n            return false\n        }\n    }\n    return true\n}\n\n// Preferred: never format it yourself.\nimport \"google.golang.org/grpc/internal/grpcutil\"\nhdr := grpcutil.EncodeDuration(30 * time.Second) // always valid","typeGuard":"// Narrow a context deadline into a legal grpc-timeout string,\n// guaranteeing the server's decodeTimeout cannot reject it.\nfunc safeGrpcTimeout(ctx context.Context) (string, bool) {\n    dl, ok := ctx.Deadline()\n    if !ok {\n        return \"\", false // no deadline -> omit header, server skips it\n    }\n    return grpcutil.EncodeDuration(time.Until(dl)), true\n}","tryCatchPattern":null,"preventionTips":["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."],"tags":["grpc","go","http2","protocol","server","timeout","request-validation"],"backgroundTag":null,"analyzedSha":"0c51461d27177d997e14c642fe18c11668fc09a3","analyzedAt":"2026-08-11T14:49:15.055Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}