{"record":{"id":"76e9d4d4e893ad26","repo":"grpc/grpc-go","slug":"malformed-binary-metadata-q-in-header-q-v","errorCode":null,"errorMessage":"malformed binary metadata %q in header %q: %v","messagePattern":"malformed binary metadata %q in header %q: (.+?)","errorType":"http","errorClass":null,"httpStatus":400,"severity":"warning","filePath":"internal/transport/handler_server.go","lineNumber":129,"sourceCode":"\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 {\n\t\t\t\tmsg := fmt.Sprintf(\"malformed binary metadata %q in header %q: %v\", v, k, err)\n\t\t\t\thttp.Error(w, msg, http.StatusBadRequest)\n\t\t\t\treturn nil, status.Error(codes.Internal, msg)\n\t\t\t}\n\t\t\tmetakv = append(metakv, k, v)\n\t\t}\n\t}\n\tst.headerMD = metadata.Pairs(metakv...)\n\n\treturn st, nil\n}\n\n// serverHandlerTransport is an implementation of ServerTransport\n// which replies to exactly one gRPC request (exactly one HTTP request),\n// using the net/http.Handler interface. This http.Handler is guaranteed\n// at this point to be speaking over HTTP/2, so it's able to speak valid\n// gRPC.\ntype serverHandlerTransport struct {\n\trw         http.ResponseWriter\n\treq        *http.Request","sourceCodeStart":111,"sourceCodeEnd":147,"githubUrl":"https://github.com/grpc/grpc-go/blob/0c51461d27177d997e14c642fe18c11668fc09a3/internal/transport/handler_server.go#L111-L147","documentation":"Raised server-side by gRPC-Go when a request header whose name ends in the '-bin' suffix carries a value that is not valid base64. Per the gRPC wire spec, binary metadata keys must be suffixed '-bin' and their values must be base64-encoded; decodeBinHeader (internal/transport/http_util.go:135) tries base64.StdEncoding for length-multiple-of-4 inputs and base64.RawStdEncoding otherwise, and any decoding error triggers this message. The server replies HTTP 400 and returns codes.Internal (handler_server.go:128-130). The error string interpolates the (still-encoded) offending value, the lowercased header key, and the base64 error, so it pinpoints which header is malformed.","triggerScenarios":"Any request header matching '*-bin' whose value is not decodable as base64: e.g. 'x-auth-token-bin: !!not-base64!!', a value that was URL-safe base64 ('-' or '_') where standard alphabet ('+' or '/') is expected, a value whose padding ('=') was stripped and whose length is not 4-aligned without that padding, or raw binary bytes that were never base64-encoded at all. Iteration happens over every value of every non-reserved header (handler_server.go:120-133), so the first bad '-bin' value aborts the whole request. Also fires in the http2_server.go:472 frame loop for the same reason.","commonSituations":"A non-grpc client (curl, browser fetch, another gRPC implementation, a hand-written HTTP/2 client) sending binary data without base64 wrapping. A gateway/proxy that decodes then re-encodes the header incorrectly, or that strips '=' padding. Client code that builds metadata with a '-bin' key but a plain string value instead of bytes. Migrating a key from non-binary to binary without re-encoding existing values. URL-safe base64 (base64.URLEncoding in Go) being used where gRPC requires standard (base64.StdEncoding). Locale- or transport-level corruption of '+', '/', or '='.","solutions":["Send binary metadata through grpc-go's metadata API, which encodes automatically: metadata.Pairs / metadata.AppendToOutgoingContext treat []byte values as binary and apply '-bin' + base64. Never hand-write the '-bin' header or base64-encode with the wrong alphabet.","If you encode by hand, use Go's base64.RawStdEncoding or base64.StdEncoding (NOT URLEncoding) — that is exactly what gRPC decodes with. Verify: len(value)%4==0 OR no padding, alphabet is A-Za-z0-9/+.","From curl/Postman/any raw client, base64-encode the binary payload with the standard alphabet before putting it in a '<key>-bin' header, and append '-bin' to the key name. Example: printf '%s' 'hello' | base64 -> 'aGVsbG8=' goes in 'x-greeting-bin: aGVsbG8='.","Check the interpolated values in the error message: %q (value) and %q (header) tell you exactly which header and bytes failed. If the value looks right, suspect a proxy stripping/re-encoding it — capture the header with tcpdump/h2c on the server side to see what actually arrived.","If the data is not actually binary, rename the header to drop the '-bin' suffix so gRPC treats it as ASCII metadata and skips base64 decoding entirely (decodeMetadataHeader short-circuits for non '-bin' keys, http_util.go:151)."],"exampleFix":"// before (wrong: '-bin' key with a non-base64 plain string)\nmd := metadata.Pairs(\"x-token-bin\", \"secret-token-value\")\nctx = metadata.NewOutgoingContext(ctx, md)\n// server: decodeBinHeader fails -> HTTP 400 malformed binary metadata\n\n// after (right: let grpc-go encode a []byte value)\nmd := metadata.Pairs(\"x-token-bin\", string([]byte(\"secret-token-value\")))\n// grpc-go detects '-bin' on the wire and base64-encodes the bytes.\n\n// after (right: if the value is text, drop the -bin suffix)\nmd := metadata.Pairs(\"x-token\", \"secret-token-value\")\n\n// after (right: manual standard-alphabet base64)\nimport \"encoding/base64\"\nhdr := base64.RawStdEncoding.EncodeToString([]byte(\"secret-token-value\"))\n// -> set header \"x-token-bin\" to hdr","handlingStrategy":"validation","validationCode":"// Validate that a '-bin' metadata value is legal on the wire.\nimport \"encoding/base64\"\n\nfunc isValidBinHeader(k, v string) bool {\n    if !strings.HasSuffix(k, \"-bin\") {\n        return true // non-binary header: no constraint from decodeBinHeader\n    }\n    // mirror decodeBinHeader: std if padded, raw otherwise\n    if len(v)%4 == 0 {\n        _, err := base64.StdEncoding.DecodeString(v)\n        return err == nil\n    }\n    _, err := base64.RawStdEncoding.DecodeString(v)\n    return err == nil\n}","typeGuard":"// Type guard for outgoing metadata: binary values must be []byte and the\n// key must end in '-bin'. grpc-go accepts both forms; this normalizes.\nfunc asBinaryMetadata(k string, v any) (mdKey string, mdVal string, ok bool) {\n    b, isBytes := v.([]byte)\n    if !isBytes {\n        return k, \"\", false\n    }\n    if !strings.HasSuffix(k, \"-bin\") {\n        k = k + \"-bin\"\n    }\n    return k, base64.RawStdEncoding.EncodeToString(b), true\n}","tryCatchPattern":null,"preventionTips":["Use metadata.Pairs / metadata.AppendToOutgoingContext with []byte for binary values so grpc-go base64-encodes and suffixes '-bin' for you.","Always use base64.StdEncoding or RawStdEncoding (standard alphabet) — never URLEncoding — when encoding '-bin' values by hand.","If a value is text, do not use a '-bin' key; rename it so the server treats it as ASCII.","In proxies/gateways, treat '*-bin' headers as opaque base64 blobs: do not decode/re-encode, do not strip '=' padding.","Add an integration test that sends every binary metadata key you produce against a real server to catch encoding mismatches before deployment."],"tags":["grpc","go","http2","protocol","metadata","base64","request-validation"],"backgroundTag":null,"analyzedSha":"0c51461d27177d997e14c642fe18c11668fc09a3","analyzedAt":"2026-08-11T14:49:15.055Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}