gastownhall/beads · error

--allowed-host is empty; pass the Host header value clients

Error message

--allowed-host is empty; pass the Host header value clients send, such as bd-myproject.beads.svc.cluster.local

What it means

ValidateAllowedHost rejects an --allowed-host value that is empty after trimming. Allowed hosts are matched against the incoming request's Host header (with the port stripped by hostOnly), so an empty entry can never match and the server would reject every client. The error names the exact thing to pass: the bare Host header value clients send.

Source

Thrown at internal/httpapi/server.go:1859

			if !containsIP(p.ips, ip) {
				p.ips = append(p.ips, ip)
			}
			continue
		}
		p.names[h] = true
	}
	return p
}

// ValidateAllowedHost refuses an allowlist entry that is not a bare host.
//
// The Host header's port is stripped before matching (hostOnly), so an entry
// carrying one would silently never match — and an operator who wrote it would
// reasonably read the startup line as proof that it does. A URL, a path or
// embedded whitespace is the same mistake in a louder form.
func ValidateAllowedHost(v string) error {
	if strings.TrimSpace(v) == "" {
		return errors.New("--allowed-host is empty; pass the Host header value clients send, such as bd-myproject.beads.svc.cluster.local")
	}
	if strings.ContainsAny(v, " \t\r\n") {
		return fmt.Errorf("--allowed-host %q contains whitespace; it must be a bare host name or IP", v)
	}
	if strings.ContainsAny(v, "/@") {
		return fmt.Errorf("--allowed-host %q looks like a URL; pass just the host, with no scheme and no path", v)
	}
	// An IPv6 address is spelled in brackets in a Host header, so an operator
	// copying one off the wire types it that way. hostOnly strips them before
	// matching, so the entry works; refusing it here — with a message about a
	// port it does not have — would be the validation lying about the policy.
	if net.ParseIP(strings.TrimSuffix(strings.TrimPrefix(v, "["), "]")) != nil {
		return nil
	}
	if strings.Contains(v, ":") {
		return fmt.Errorf("--allowed-host %q carries a port; the port is stripped from a request's Host before matching, so an entry with one could never match", v)
	}
	return nil

View on GitHub (pinned to 71377f2769)

Solutions

  1. Pass the actual Host header value clients use, e.g. --allowed-host bd-myproject.beads.svc.cluster.local (no scheme, no path, no port)
  2. Fix the surrounding config/secret/templating so the value is not empty at startup
  3. If multiple hosts are allowed, pass each as its own non-empty --allowed-host entry

Example fix

// before
bd serve --allowed-host=""
// after
bd serve --allowed-host=bd-myproject.beads.svc.cluster.local
Defensive patterns

Strategy: validation

Validate before calling

v := strings.TrimSpace(flagAllowedHost)
if v == "" {
    return errors.New("--allowed-host must be the Host header value clients send, e.g. bd-myproject.beads.svc.cluster.local")
}
if err := httpapi.ValidateAllowedHost(v); err != nil {
    return err
}

Prevention

When it happens

Trigger: Starting the server with --allowed-host="" or only whitespace; programmatically calling ValidateAllowedHost("") or with a value of only spaces/tabs before saving an allowed-host entry.

Common situations: An empty environment variable or config default silently substituted for the flag; templated deploy configs where the host placeholder resolved to empty; forgetting the flag entirely and defaulting to empty string.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/13f850e6512fad21. Report an issue: GitHub.