gofiber/fiber · critical
ErrUpstreamHostInvalid
ErrUpstreamHostInvalid
Error message
proxy: upstream host is empty or invalid
What it means
The Balancer middleware (proxy.Balancer) validates every cfg.Servers entry at construction via validateUpstreamForBalancer. ErrUpstreamHostInvalid fires when an entry cannot be parsed into a URL with a non-empty host. parseUpstream prepends "http://" to bare host:port strings (preserving historical Balancer behavior), so this only triggers on genuinely malformed input: an empty string, an unparseable URL, or a URL whose Hostname() is empty such as "http://:8080". The panic propagates through the panic(err) at proxy.go:41 inside the server loop.
Source
Thrown at middleware/proxy/proxy.go:41
// Balancer creates a load balancer among multiple upstream servers
func Balancer(config ...Config) fiber.Handler {
// Set default config
cfg := configDefault(config...)
policy := resolvePolicy(cfg.SecurityPolicy)
// Load balanced client
lbc := &fasthttp.LBClient{}
// Note that Servers, Timeout, WriteBufferSize, ReadBufferSize and TLSConfig
// will not be used if the client are set.
if cfg.Client == nil {
// Set timeout
lbc.Timeout = cfg.Timeout
// Validate each upstream against the configured policy and build
// a HostClient per server.
for _, server := range cfg.Servers {
u, err := validateUpstreamForBalancer(server, policy)
if err != nil {
panic(err)
}
client := &fasthttp.HostClient{
NoDefaultUserAgentHeader: true,
DisablePathNormalizing: true,
Addr: u.Host,
MaxConns: cfg.MaxConnsPerHost,
ReadBufferSize: cfg.ReadBufferSize,
WriteBufferSize: cfg.WriteBufferSize,
TLSConfig: secureTLSConfig(cfg.TLSConfig),
DialDualStack: cfg.DialDualStack,
MaxResponseBodySize: cfg.MaxResponseBodySize,
}
if u.Scheme == schemeHTTPS {View on GitHub (pinned to a105acad6c)
Solutions
- Filter empty/whitespace entries from Servers before constructing the Balancer: servers = slices.DeleteFunc(servers, func(s string) bool { return strings.TrimSpace(s) == "" }).
- Validate each entry at config-load time with url.Parse and confirm Hostname() != "", failing loudly with a descriptive error instead of a raw panic.
- Ensure every entry is either a bare host:port (auto-prefixed with http://) or a full scheme://host[:port] form.
Example fix
// before
app.Use(proxy.Balancer(proxy.Config{
Servers: strings.Split(os.Getenv("UPSTREAMS"), ","),
}))
// after
raw := strings.Split(os.Getenv("UPSTREAMS"), ",")
servers := slices.DeleteFunc(raw, func(s string) bool { return strings.TrimSpace(s) == "" })
if len(servers) == 0 {
log.Fatal("UPSTREAMS must list at least one host")
}
app.Use(proxy.Balancer(proxy.Config{Servers: servers})) Defensive patterns
Strategy: validation
Validate before calling
func validBalancerServers(servers []string) error {
for _, s := range servers {
if strings.TrimSpace(s) == "" {
return fmt.Errorf("empty upstream server entry")
}
u, err := url.Parse(s)
if err == nil && !strings.Contains(s, "://") {
u, err = url.Parse("http://" + s)
}
if err != nil {
return fmt.Errorf("parse upstream %q: %w", s, err)
}
if u.Hostname() == "" {
return fmt.Errorf("upstream %q has no host", s)
}
}
return nil
}
// call: if err := validBalancerServers(cfg.Servers); err != nil { log.Fatal(err) } Type guard
// confirms a server string parses to a URL with a non-empty host
func isBalancerServerValid(s string) bool {
if strings.TrimSpace(s) == "" { return false }
if !strings.Contains(s, "://") { s = "http://" + s }
u, err := url.Parse(s)
return err == nil && u.Hostname() != ""
} Prevention
- Treat upstream lists as untrusted config: validate and filter before passing to proxy.Balancer.
- Centralize env-var parsing so empty/trailing-comma elements are stripped once at load.
- Write a startup config-check test that calls your Balancer construction with realistic config values.
When it happens
Trigger: proxy.Balancer(proxy.Config{Servers: []string{""}}), or {"http://:8080"}, or an entry like ":::bad" that url.Parse rejects. Also fires when a config field or env var is unset and yields an empty element.
Common situations: Splitting an env var with strings.Split(os.Getenv("UPSTREAMS"), ",") where a trailing comma produces an empty element; a config template with a ${UPSTREAM} placeholder left blank; a service-discovery lookup returning an empty address for one node.
Related errors
- ErrUpstreamHostBlocked
- %w: %q
- ErrUpstreamSchemeNotAllowed
- proxy: WithClient requires a non-nil *fasthttp.Client
- Servers cannot be empty
AI-assisted analysis of gofiber/fiber@a105acad6c (2026-08-11).
Data as JSON: /api/errors/6adf3e8d7d583ebb.
Report an issue: GitHub.