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

  1. Delete the custom_headers map (or leave it empty) on L4 targets.
  2. If the upstream is really HTTP and needs the header, run the service in mode http.
  3. 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

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


AI-assisted analysis of netbirdio/netbird@93e97f4bf1 (2026-08-16). Data as JSON: /api/errors/f873141d7fa11526. Report an issue: GitHub.