grpc/grpc-go · error
header key is empty
Error message
header key is empty
What it means
Returned by validateHeaderKey when the key has length zero. validateHeaderKey is called by both ApplyAdditions (for add/modify mutations) and ApplyRemovals (for removals), so an empty key from the ext_proc server on either path produces this. It is the first case in the validation switch, so it preempts all other checks.
Source
Thrown at internal/xds/httpfilter/extconfig.go:224
}
if !hmr.allow(header) {
if hmr.DisallowIsError {
return fmt.Errorf("header mutation disallowed by headerMutationRules for header %q", header)
}
continue
}
input.Delete(header)
}
return nil
}
// validateHeaderKey returns a non-nil error if key may not be mutated by an
// external processing server, either because the key is reserved or because it
// is not a valid gRPC header name.
func validateHeaderKey(key string) error {
switch {
case len(key) == 0:
return fmt.Errorf("header key is empty")
case key[0] == ':':
return fmt.Errorf("header key %q is a pseudo-header", key)
case key == "host":
return fmt.Errorf("header key %q is reserved", key)
case strings.HasPrefix(key, "grpc-"):
return fmt.Errorf("header key %q is in the reserved 'grpc-' space", key)
case key != strings.ToLower(key):
return fmt.Errorf("header key %q is not lowercase", key)
case len(key) > maxHeaderSize:
return fmt.Errorf("header key exceeds the maximum length of %d bytes", maxHeaderSize)
}
return imetadata.ValidateKey(key)
}
func (hmr *HeaderMutationRules) allow(key string) bool {
if hmr.DisallowExpr != nil && hmr.DisallowExpr.MatchString(key) {
return false
}View on GitHub (pinned to 0c51461d27)
Solutions
- On the ext_proc server, skip any mutation entry whose key is empty before sending the response.
- Validate the key is non-empty at the point of construction (fail fast).
- Add logging server-side to surface which code path produced the empty key.
- Write a regression test that round-trips mutations with edge-case keys.
Example fix
// before
for k, v := range kv { out = append(out, hdrOpt(k, v)) }
// after
for k, v := range kv {
if k == "" { continue }
out = append(out, hdrOpt(k, v))
} Defensive patterns
Strategy: validation
Validate before calling
// server-side: drop empty keys before sending
filtered := hvos[:0]
for _, h := range hvos {
if h.GetHeader().GetKey() == "" { continue }
filtered = append(filtered, h)
} Prevention
- Never construct a mutation with an empty key.
- Sanitize map-derived keys for zero values.
- Add a unit test enumerating edge-case keys.
- Log when a key is dropped so bugs surface early.
When it happens
Trigger: The ext_proc server sends a HeaderValueOption with header.key="" (additions) or includes "" in headersToRemove (removals). The client rejects it before applying any mutation.
Common situations: Server builds the key from a map lookup that returned zero value; server forwards a header whose name was stripped by an upstream proxy; off-by-one in a header-splitting routine yields an empty token.
Related errors
- header key exceeds the maximum length of %d bytes
- invalid header mutation: value for header key %q exceeds the
- header mutation disallowed by headerMutationRules for header
- header mutation disallowed by headerMutationRules for header
- header key %q is a pseudo-header
AI-assisted analysis of grpc/grpc-go@0c51461d27 (2026-08-11).
Data as JSON: /api/errors/582c1696d40a3671.
Report an issue: GitHub.