ory/hydra · error

expected X-Forwarded-Proto header to be https but got: %s

Error message

expected X-Forwarded-Proto header to be https but got: %s

What it means

This error is returned by the EnforceTLSRequests middleware in oryx/tlsx when a request arrives over plain HTTP (r.TLS == nil), the client IP is inside an allowed termination CIDR, but the X-Forwarded-Proto header is present yet not equal to "https". The middleware trusts a TLS-terminating proxy (load balancer, ingress, reverse proxy) to set this header correctly, and refuses to serve traffic whose forwarded protocol does not indicate HTTPS. It responds with HTTP 502 Bad Gateway and this message echoing the actual header value.

Source

Thrown at oryx/tlsx/termination.go:67

			d.Logger().WithRequest(r).WithError(errors.New("TLS termination is not enabled")).Error("Could not serve http connection")
			d.Writer().WriteErrorCode(rw, r, http.StatusBadGateway, errors.New("can not serve request over insecure http"))
			return
		}

		if err := matchesRange(r, networks); err != nil {
			d.Logger().WithRequest(r).WithError(err).Warnln("Could not serve http connection")
			d.Writer().WriteErrorCode(rw, r, http.StatusBadGateway, errors.New("can not serve request over insecure http"))
			return
		}

		proto := r.Header.Get("X-Forwarded-Proto")
		if proto == "" {
			d.Logger().WithRequest(r).WithError(errors.New("X-Forwarded-Proto header is missing")).Error("Could not serve http connection")
			d.Writer().WriteErrorCode(rw, r, http.StatusBadGateway, errors.New("can not serve request over insecure http"))
			return
		} else if proto != "https" {
			d.Logger().WithRequest(r).WithError(errors.New("X-Forwarded-Proto header is missing")).Error("Could not serve http connection")
			d.Writer().WriteErrorCode(rw, r, http.StatusBadGateway, errors.Errorf("expected X-Forwarded-Proto header to be https but got: %s", proto))
			return
		}

		next(rw, r)
	}), nil
}

func matchesRange(r *http.Request, networks []*net.IPNet) error {
	remoteIP, _, err := net.SplitHostPort(r.RemoteAddr)
	if err != nil {
		return errors.WithStack(err)
	}

	check := []string{remoteIP}
	for fwd := range strings.SplitSeq(r.Header.Get("X-Forwarded-For"), ",") {
		check = append(check, strings.TrimSpace(fwd))
	}

View on GitHub (pinned to 4174065ffb)

Solutions

  1. Fix the TLS-terminating proxy to set X-Forwarded-Proto: https when the original request was HTTPS (e.g. nginx: proxy_set_header X-Forwarded-Proto $scheme; with the external listener on 443).
  2. If using Cloudflare or an ALB, switch SSL mode from Flexible to Full/Full(strict) so the forwarded proto is https.
  3. Ensure no intermediate proxy overwrites the header; remove duplicate proxy_set_header directives.
  4. For local testing over plain HTTP, add the client IP to allowTerminationFrom and bypass the proxy, or serve the service directly over TLS.

Example fix

// before (nginx location behind TLS)
proxy_set_header X-Forwarded-Proto http;
// after
proxy_set_header X-Forwarded-Proto https;
Defensive patterns

Strategy: validation

Validate before calling

// ensure your proxy sets the header before forwarding
if r.Header.Get("X-Forwarded-Proto") != "https" {
    // request will be rejected by EnforceTLSRequests; fix proxy config first
}

Prevention

When it happens

Trigger: A proxy/ingress terminates TLS but forwards X-Forwarded-Proto: http (or http/1.1, http, HTTP, etc.) to the Ory service; a misconfigured load balancer health/forwarding rule; a developer curling the internal plain-HTTP port while the ingress injects a wrong header value.

Common situations: Kubernetes ingress (nginx, ALB) with ssl-redirect or X-Forwarded-Proto override misconfigured; Cloudflare in Flexible SSL mode sending proto=http; double proxies where the inner proxy overwrites X-Forwarded-Proto; self-signed local TLS setups where the upstream port is accessed over http.

Related errors


AI-assisted analysis of ory/hydra@4174065ffb (2026-09-03). Data as JSON: /api/errors/eb2ac74a13d278a0. Report an issue: GitHub.