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

  1. 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.
  2. 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/+.
  3. 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='.
  4. 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.
  5. 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

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

Related errors


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.Request

View on GitHub (pinned to 0c51461d27)