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
- Add either subdomain = "myapp" (resolved against frps subDomainHost) or customDomains = ["app.example.com"] to the http/https/tcpmux proxy.
- If you actually want port-based routing, change type back to "tcp" and use remotePort.
- 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
- http/https/tcpmux proxies are routed by hostname: always set subdomain or customDomains.
- tcp/udp proxies use remotePort — pick the right type first.
- Set up DNS for customDomains to point at the frps host.
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
- name should not be empty
- invalid type [%s]
- localAddr is required
- localPath is required
- unixPath is required
AI-assisted analysis of fatedier/frp@6c8a8d0a97 (2026-08-15).
Data as JSON: /api/errors/80b654c11274b56f.
Report an issue: GitHub.