grpc/grpc-go · warning
malformed binary metadata
Error message
malformed binary metadata %q in header %q: %v
What it means
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.
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).
Example fix
// before (wrong: '-bin' key with a non-base64 plain string)
md := metadata.Pairs("x-token-bin", "secret-token-value")
ctx = metadata.NewOutgoingContext(ctx, md)
// server: decodeBinHeader fails -> HTTP 400 malformed binary metadata
// after (right: let grpc-go encode a []byte value)
md := metadata.Pairs("x-token-bin", string([]byte("secret-token-value")))
// grpc-go detects '-bin' on the wire and base64-encodes the bytes.
// after (right: if the value is text, drop the -bin suffix)
md := metadata.Pairs("x-token", "secret-token-value")
// after (right: manual standard-alphabet base64)
import "encoding/base64"
hdr := base64.RawStdEncoding.EncodeToString([]byte("secret-token-value"))
// -> set header "x-token-bin" to hdr Defensive patterns
Strategy: validation
Validate before calling
// Validate that a '-bin' metadata value is legal on the wire.
import "encoding/base64"
func isValidBinHeader(k, v string) bool {
if !strings.HasSuffix(k, "-bin") {
return true // non-binary header: no constraint from decodeBinHeader
}
// mirror decodeBinHeader: std if padded, raw otherwise
if len(v)%4 == 0 {
_, err := base64.StdEncoding.DecodeString(v)
return err == nil
}
_, err := base64.RawStdEncoding.DecodeString(v)
return err == nil
} Type guard
// Type guard for outgoing metadata: binary values must be []byte and the
// key must end in '-bin'. grpc-go accepts both forms; this normalizes.
func asBinaryMetadata(k string, v any) (mdKey string, mdVal string, ok bool) {
b, isBytes := v.([]byte)
if !isBytes {
return k, "", false
}
if !strings.HasSuffix(k, "-bin") {
k = k + "-bin"
}
return k, base64.RawStdEncoding.EncodeToString(b), true
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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 '='.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- malformed grpc-timeout
- "allow_rules" is not present
- "allow_rules
- AuthInfo is nil
- authz: authorization policy file path is empty
AI-assisted analysis of grpc/grpc-go@0c51461d27 (2026-08-11).
Data as JSON: /api/errors/76e9d4d4e893ad26.
Report an issue: GitHub.
Appendix: source
Thrown at internal/transport/handler_server.go:129
}
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 {
msg := fmt.Sprintf("malformed binary metadata %q in header %q: %v", v, k, err)
http.Error(w, msg, http.StatusBadRequest)
return nil, status.Error(codes.Internal, msg)
}
metakv = append(metakv, k, v)
}
}
st.headerMD = metadata.Pairs(metakv...)
return st, nil
}
// serverHandlerTransport is an implementation of ServerTransport
// which replies to exactly one gRPC request (exactly one HTTP request),
// using the net/http.Handler interface. This http.Handler is guaranteed
// at this point to be speaking over HTTP/2, so it's able to speak valid
// gRPC.
type serverHandlerTransport struct {
rw http.ResponseWriter
req *http.RequestView on GitHub (pinned to 0c51461d27)