netbirdio/netbird · error

path is not supported for L4 services

Error message

path is not supported for L4 services

What it means

Returned by validateL4Target when the target's Path pointer is non-nil and set to anything other than "" or "/". Path-based routing is an HTTP concept (choosing a backend by URL path); an L4 service forwards opaque bytes and never inspects them, so a path cannot influence routing. The empty-string and "/" exceptions exist so HTTP-derived defaults survive the copy.

Source

Thrown at management/internals/modules/reverseproxy/service/service.go:1057

		return errors.New("target port is required for L4 services")
	}
	switch target.TargetType {
	case TargetTypePeer, TargetTypeHost, TargetTypeDomain:
		if err := validateDirectUpstreamHost(0, target); err != nil {
			return err
		}
	case TargetTypeSubnet:
		if target.Host == "" {
			return errors.New("target host is required for subnet targets")
		}
	case TargetTypeCluster:
		// target_id carries the cluster address; the proxy resolves
		// the upstream at request time.
	default:
		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
}

View on GitHub (pinned to 93e97f4bf1)

Solutions

  1. Remove the path key (or set it to null, "", or exactly "/") on L4 targets.
  2. Clean shared templates so mode-specific fields are conditionally emitted.
  3. If you were trying to route by path, that is mode http territory - restructure the service accordingly.

Example fix

// before
{ "target_type": "peer", "target_id": "peer-a", "port": 5432, "path": "/db" }

// after
{ "target_type": "peer", "target_id": "peer-a", "port": 5432 }
Defensive patterns

Strategy: validation

Validate before calling

func checkL4NoPath(t Target) error {
	if t.Path != nil && *t.Path != "" && *t.Path != "/" {
		return errors.New("path is http-only; remove it from L4 targets")
	}
	return nil
}

Type guard

func isL4PathClean(t Target) bool {
	return t.Path == nil || *t.Path == "" || *t.Path == "/"
}

Try / catch

if err := svc.Validate(); err != nil {
	if strings.Contains(err.Error(), "path is not supported for L4") {
		return respondBadRequest(errors.New("delete the path key from L4 targets"))
	}
	return respondBadRequest(err)
}

Prevention

When it happens

Trigger: An L4 target whose JSON carries "path": "/api" left over from an HTTP multi-target definition; YAML anchors shared between HTTP and TCP service templates that include a path key.

Common situations: Cloning an HTTP service (where each target had a path) into a tcp/udp/tls service. Tooling that emits a default path of "/v1" for every target regardless of mode.

Related errors


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