netbirdio/netbird · error
custom_headers is not supported for L4 services
Error message
custom_headers is not supported for L4 services
What it means
Returned by validateL4Target when target.options.custom_headers is a non-empty map. Custom headers are injected into HTTP requests the proxy sends upstream; a TCP/UDP/TLS passthrough has no request/response structure to hang headers on. Note this fires even if the map exists but is empty - no, an empty map passes; only len > 0 is rejected.
Source
Thrown at management/internals/modules/reverseproxy/service/service.go:1072
return fmt.Errorf("invalid target_type %q for L4 service", target.TargetType)
}
if target.Path != nil && *target.Path != "" && *target.Path != "/" {
return errors.New("path is not supported for L4 services")
}
if target.Options.SessionIdleTimeout < 0 {
return errors.New("session_idle_timeout must be positive for L4 services")
}
if target.Options.RequestTimeout < 0 {
return errors.New("request_timeout must be positive for L4 services")
}
if target.Options.SkipTLSVerify {
return errors.New("skip_tls_verify is not supported for L4 services")
}
if target.Options.PathRewrite != "" {
return errors.New("path_rewrite is not supported for L4 services")
}
if len(target.Options.CustomHeaders) > 0 {
return errors.New("custom_headers is not supported for L4 services")
}
return nil
}
// Service mode constants.
const (
ModeHTTP = "http"
ModeTCP = "tcp"
ModeUDP = "udp"
ModeTLS = "tls"
)
// Target protocol constants (URL scheme for backend connections).
const (
TargetProtoHTTP = "http"
TargetProtoHTTPS = "https"
TargetProtoTCP = "tcp"
TargetProtoUDP = "udp"View on GitHub (pinned to 93e97f4bf1)
Solutions
- Delete the custom_headers map (or leave it empty) on L4 targets.
- If the upstream is really HTTP and needs the header, run the service in mode http.
- For credentials on L4, have the client send them inside the protocol stream (e.g. DB auth), not as proxy-injected headers.
Example fix
// before
"options": { "custom_headers": { "X-Upstream-Token": "abc" } }
// after
"options": {} Defensive patterns
Strategy: validation
Validate before calling
func checkL4NoCustomHeaders(o TargetOptions) error {
if len(o.CustomHeaders) > 0 {
return errors.New("custom_headers is http-only; remove it from L4 targets")
}
return nil
} Type guard
func isL4CustomHeadersClean(o TargetOptions) bool {
return len(o.CustomHeaders) == 0
} Try / catch
if err := svc.Validate(); err != nil {
if strings.Contains(err.Error(), "custom_headers is not supported for L4") {
return respondBadRequest(errors.New("L4 has no headers; authenticate inside the protocol instead"))
}
return respondBadRequest(err)
} Prevention
- Never plan to inject auth headers into a byte-stream forward - it cannot work.
- Gate header options in your config schema on mode == http.
- Move upstream credentials into the protocol's own handshake (DB users, SASL, etc.).
When it happens
Trigger: An L4 target whose options include custom_headers like {"X-Client": "portal"} copied from an HTTP target; injecting an auth header by habit on a database forward.
Common situations: Trying to pass a static API key or tracing header to an upstream that is actually a raw TCP service. Duplicating a fully-populated options block between HTTP and L4 services.
Related errors
- skip_tls_verify is not supported for L4 services
- path_rewrite is not supported for L4 services
- domain is required for TCP/UDP services (used for cluster de
- auth is not supported for TCP/UDP services
- TCP/UDP services must have exactly one target
AI-assisted analysis of netbirdio/netbird@93e97f4bf1 (2026-08-16).
Data as JSON: /api/errors/f873141d7fa11526.
Report an issue: GitHub.