fatedier/frp · error

subdomain and custom domains should not be both empty

Error message

subdomain and custom domains should not be both empty

What it means

Validation error from validateDomainConfigForClient: an frpc proxy of an HTTP/HTTPS/TCP-mux type has neither subdomain nor customDomains set. These proxies are routed by hostname on frps, so at least one routing domain must be declared client-side. Checked by validateTCPMuxProxyConfigForClient / validateHTTPProxyConfigForClient / validateHTTPSProxyConfigForClient via the shared DomainConfig.

Source

Thrown at pkg/config/v1/validation/proxy.go:76

	if c.Plugin.Type != "" {
		if err := ValidateClientPluginOptions(c.Plugin.ClientPluginOptions); err != nil {
			return fmt.Errorf("plugin %s: %v", c.Plugin.Type, err)
		}
	}
	return nil
}

func validateProxyBaseConfigForServer(c *v1.ProxyBaseConfig) error {
	if err := ValidateAnnotations(c.Annotations); err != nil {
		return err
	}
	return nil
}

func validateDomainConfigForClient(c *v1.DomainConfig) error {
	if c.SubDomain == "" && len(c.CustomDomains) == 0 {
		return errors.New("subdomain and custom domains should not be both empty")
	}
	return nil
}

func validateDomainConfigForServer(c *v1.DomainConfig, s *v1.ServerConfig) error {
	subDomainHost := strings.ToLower(s.SubDomainHost)
	for _, domain := range c.CustomDomains {
		canonicalDomain := strings.ToLower(domain)
		if subDomainHost != "" && len(strings.Split(subDomainHost, ".")) < len(strings.Split(canonicalDomain, ".")) {
			if strings.HasSuffix(canonicalDomain, "."+subDomainHost) {
				return fmt.Errorf("custom domain [%s] should not belong to subdomain host [%s]", domain, s.SubDomainHost)
			}
		}
	}

	if c.SubDomain != "" {
		if s.SubDomainHost == "" {
			return errors.New("subdomain is not supported because this feature is not enabled in server")

View on GitHub (pinned to 6c8a8d0a97)

Solutions

  1. Add either subdomain = "myapp" (resolved against frps subDomainHost) or customDomains = ["app.example.com"] to the http/https/tcpmux proxy.
  2. If you actually want port-based routing, change type back to "tcp" and use remotePort.
  3. For customDomains pointing at the server, ensure DNS for those domains routes to the frps host.

Example fix

# before
[[proxies]]
name = "web"
type = "http"
remotePort = 8080

# after
[[proxies]]
name = "web"
type = "http"
subdomain = "web"
Defensive patterns

Strategy: validation

Validate before calling

for _, p := range clientCfg.Proxies {
    switch v := p.(type) {
    case *v1.HTTPProxyConfig:
        if v.SubDomain == "" && len(v.CustomDomains) == 0 {
            return errors.New("http proxy needs subdomain or customDomains")
        }
    case *v1.HTTPSProxyConfig:
        if v.SubDomain == "" && len(v.CustomDomains) == 0 {
            return errors.New("https proxy needs subdomain or customDomains")
        }
    }
}

Type guard

func domainRouted(d v1.DomainConfig) bool {
    return d.SubDomain != "" || len(d.CustomDomains) > 0
}

Try / catch

if err := validation.ValidateProxyConfigurerForClient(p); err != nil {
    if strings.Contains(err.Error(), "subdomain and custom domains should not be both empty") {
        // add subdomain or customDomains, or switch type to tcp with remotePort
    }
    return err
}

Prevention

When it happens

Trigger: A [[proxies]] entry with type = "http" (or https/tcpmux) that declares neither subdomain nor customDomains — e.g. an http proxy copied from a tcp template where only remotePort was set.

Common situations: Converting a tcp proxy to http/https and forgetting that remotePort is not used for routing; declaring domains under the wrong table so DomainConfig decodes empty; expecting the server's subdomainHost to provide a default (it does not — a subdomain value is still required).

Related errors


AI-assisted analysis of fatedier/frp@6c8a8d0a97 (2026-08-15). Data as JSON: /api/errors/80b654c11274b56f. Report an issue: GitHub.