{"record":{"id":"6adf3e8d7d583ebb","repo":"gofiber/fiber","slug":"errupstreamhostinvalid-6adf3e","errorCode":"ErrUpstreamHostInvalid","errorMessage":"proxy: upstream host is empty or invalid","messagePattern":"proxy: upstream host is empty or invalid","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"middleware/proxy/proxy.go","lineNumber":41,"sourceCode":"// Balancer creates a load balancer among multiple upstream servers\nfunc Balancer(config ...Config) fiber.Handler {\n\t// Set default config\n\tcfg := configDefault(config...)\n\tpolicy := resolvePolicy(cfg.SecurityPolicy)\n\n\t// Load balanced client\n\tlbc := &fasthttp.LBClient{}\n\t// Note that Servers, Timeout, WriteBufferSize, ReadBufferSize and TLSConfig\n\t// will not be used if the client are set.\n\tif cfg.Client == nil {\n\t\t// Set timeout\n\t\tlbc.Timeout = cfg.Timeout\n\t\t// Validate each upstream against the configured policy and build\n\t\t// a HostClient per server.\n\t\tfor _, server := range cfg.Servers {\n\t\t\tu, err := validateUpstreamForBalancer(server, policy)\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\n\t\t\tclient := &fasthttp.HostClient{\n\t\t\t\tNoDefaultUserAgentHeader: true,\n\t\t\t\tDisablePathNormalizing:   true,\n\t\t\t\tAddr:                     u.Host,\n\t\t\t\tMaxConns:                 cfg.MaxConnsPerHost,\n\n\t\t\t\tReadBufferSize:  cfg.ReadBufferSize,\n\t\t\t\tWriteBufferSize: cfg.WriteBufferSize,\n\n\t\t\t\tTLSConfig: secureTLSConfig(cfg.TLSConfig),\n\n\t\t\t\tDialDualStack: cfg.DialDualStack,\n\n\t\t\t\tMaxResponseBodySize: cfg.MaxResponseBodySize,\n\t\t\t}\n\t\t\tif u.Scheme == schemeHTTPS {","sourceCodeStart":23,"sourceCodeEnd":59,"githubUrl":"https://github.com/gofiber/fiber/blob/a105acad6c1e4576a77f01e02973f67e962bb58d/middleware/proxy/proxy.go#L23-L59","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\napp.Use(proxy.Balancer(proxy.Config{\n    Servers: strings.Split(os.Getenv(\"UPSTREAMS\"), \",\"),\n}))\n\n// after\nraw := strings.Split(os.Getenv(\"UPSTREAMS\"), \",\")\nservers := slices.DeleteFunc(raw, func(s string) bool { return strings.TrimSpace(s) == \"\" })\nif len(servers) == 0 {\n    log.Fatal(\"UPSTREAMS must list at least one host\")\n}\napp.Use(proxy.Balancer(proxy.Config{Servers: servers}))","handlingStrategy":"validation","validationCode":"func validBalancerServers(servers []string) error {\n    for _, s := range servers {\n        if strings.TrimSpace(s) == \"\" {\n            return fmt.Errorf(\"empty upstream server entry\")\n        }\n        u, err := url.Parse(s)\n        if err == nil && !strings.Contains(s, \"://\") {\n            u, err = url.Parse(\"http://\" + s)\n        }\n        if err != nil {\n            return fmt.Errorf(\"parse upstream %q: %w\", s, err)\n        }\n        if u.Hostname() == \"\" {\n            return fmt.Errorf(\"upstream %q has no host\", s)\n        }\n    }\n    return nil\n}\n// call: if err := validBalancerServers(cfg.Servers); err != nil { log.Fatal(err) }","typeGuard":"// confirms a server string parses to a URL with a non-empty host\nfunc isBalancerServerValid(s string) bool {\n    if strings.TrimSpace(s) == \"\" { return false }\n    if !strings.Contains(s, \"://\") { s = \"http://\" + s }\n    u, err := url.Parse(s)\n    return err == nil && u.Hostname() != \"\"\n}","tryCatchPattern":null,"preventionTips":["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."],"tags":["proxy","config","startup","panic","ssrf"],"backgroundTag":null,"analyzedSha":"a105acad6c1e4576a77f01e02973f67e962bb58d","analyzedAt":"2026-08-11T17:33:26.942Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}