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

  1. On the ext_proc server, skip any mutation entry whose key is empty before sending the response.
  2. Validate the key is non-empty at the point of construction (fail fast).
  3. Add logging server-side to surface which code path produced the empty key.
  4. 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

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


AI-assisted analysis of grpc/grpc-go@0c51461d27 (2026-08-11). Data as JSON: /api/errors/582c1696d40a3671. Report an issue: GitHub.