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

  1. Filter empty/whitespace entries from Servers before constructing the Balancer: servers = slices.DeleteFunc(servers, func(s string) bool { return strings.TrimSpace(s) == "" }).
  2. Validate each entry at config-load time with url.Parse and confirm Hostname() != "", failing loudly with a descriptive error instead of a raw panic.
  3. 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

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


AI-assisted analysis of gofiber/fiber@a105acad6c (2026-08-11). Data as JSON: /api/errors/6adf3e8d7d583ebb. Report an issue: GitHub.