gofiber/fiber · error · ErrUpstreamSchemeNotAllowed

%w: %q

Error message

%w: %q

What it means

parseUpstreamScheme runs after a successful parseUpstream and rejects any scheme not in the policy allowlist (default http, https). The sentinel is ErrUpstreamSchemeNotAllowed, and the offending scheme is quoted. This is the central scheme gate consulted by both runtime helpers and Balancer construction.

Source

Thrown at middleware/proxy/security.go:327

		return u, nil
	}
	if err := validateHostForSSRF(u.Hostname()); err != nil {
		return nil, err
	}
	return u, nil
}

// parseUpstreamScheme parses raw and enforces the scheme allowlist and
// host presence without performing any DNS resolution. url.Parse can
// leave Host set while Hostname() is empty (e.g. "http://:8080"), so the
// presence check uses Hostname.
func parseUpstreamScheme(raw string, policy SecurityPolicy) (*url.URL, error) {
	u, err := parseUpstream(raw)
	if err != nil {
		return nil, err
	}
	if !schemeAllowed(u.Scheme, policy.AllowedSchemes) {
		return nil, fmt.Errorf("%w: %q", ErrUpstreamSchemeNotAllowed, u.Scheme)
	}
	if u.Hostname() == "" {
		return nil, ErrUpstreamHostInvalid
	}
	return u, nil
}

// validateUpstreamForBalancer validates a statically configured Balancer
// upstream. It enforces the scheme allowlist and rejects IP-literal hosts
// in blocked ranges, but defers hostname resolution to the SSRF-guarded
// dialer (see newSSRFDialer). Deferring DNS keeps a transient resolver
// failure at startup from panicking the application (e.g. crash loops in
// container orchestrators) and re-checks the resolved IP on every dial,
// which also defeats DNS-rebinding.
func validateUpstreamForBalancer(raw string, policy SecurityPolicy) (*url.URL, error) {
	u, err := parseUpstreamScheme(raw, policy)
	if err != nil {
		return nil, err

View on GitHub (pinned to a105acad6c)

Solutions

  1. Change the upstream to use http:// or https:// (the defaults), or another scheme you have deliberately added to SecurityPolicy.AllowedSchemes.
  2. For websocket traffic, use http(s):// and let fiber/proxy handle the Upgrade header — do not use ws://.
  3. If you genuinely need an additional scheme, pass it via Config.SecurityPolicy.AllowedSchemes = []string{"http","https","foo"} (and document the security implications).
  4. Audit all Balancer.Servers, proxy.Targets, and WithSecurityPolicy call sites for the rejected scheme.
  5. Print SecurityPolicy.AllowedSchemes at startup to confirm what is permitted.

Example fix

// before: disallowed scheme
balancer.Servers = []string{"ws://upstream:8080"}

// after: http with Upgrade handled by the upstream
balancer.Servers = []string{"http://upstream:8080"}
Defensive patterns

Strategy: validation

Validate before calling

func allowedScheme(raw string, policy proxy.SecurityPolicy) error {
  u, err := url.Parse(strings.TrimSpace(raw))
  if err != nil { return err }
  allowed := policy.AllowedSchemes
  if len(allowed) == 0 { allowed = []string{"http","https"} }
  for _, s := range allowed { if strings.EqualFold(s, u.Scheme) { return nil } }
  return fmt.Errorf("scheme %q not allowed", u.Scheme)
}

Try / catch

if err := balancer.Build(); err != nil {
  if errors.Is(err, proxy.ErrUpstreamSchemeNotAllowed) { /* fix config */ }
}

Prevention

When it happens

Trigger: Configuring an upstream with scheme file://, ftp://, gopher://, ws://, or any custom scheme not present in SecurityPolicy.AllowedSchemes. Also when scheme normalization failed (e.g. uppercase HTTPS slipped through if you narrowed the allowlist case-sensitively — though schemeAllowed uses EqualFold).

Common situations: Operator expects file:// to work for local proxying; legacy config with ws:// for a websocket service that should be proxied via http(s) upgrade instead; a SecurityPolicy explicitly narrowed to only https while a config still has an http:// entry; misordered env interpolation producing an empty scheme.

Related errors


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