# X-Forwarded-Proto

> Learn how the X-Forwarded-Proto header identifies the original protocol (HTTP/HTTPS) used by clients connecting through proxies or load balancers.

Source: https://howhttpworks.com/headers/x-forwarded-proto
Last reviewed: 2026-10-05

> **TL;DR:** `X-Forwarded-Proto: https` tells an origin the client-facing scheme reported by a proxy. It can fix HTTPS redirect loops after TLS termination, but only if the proxy overwrites the field and the app trusts that proxy.

## Why This Header Exists

A TLS-terminating proxy can forward over plain HTTP:

```text
Browser --HTTPS--> Reverse proxy --HTTP--> App
```

The backend socket then reports HTTP. This illustrative field carries the outer scheme:

```http
X-Forwarded-Proto: https
```

[MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-Proto) documents it as a de-facto field, not an HTTP standard.

Apps use the request scheme for redirect decisions and absolute URL generation. Cookie middleware can also depend on whether a request is considered secure. These behaviors depend on the framework; merely receiving the header does not change them.

## The Classic Redirect Loop

If the app redirects every HTTP socket to HTTPS, each browser retry arrives through the same HTTP backend connection and gets redirected again. The fix is to have scheme detection use verified proxy information rather than just the inner socket.

## Framework Handling

For Express behind nginx on the **same machine**, with the app bound to loopback, use:

```javascript
import express from 'express'

const app = express()
app.set('trust proxy', 'loopback')
app.get('/scheme', (req, res) => {
  res.json({ protocol: req.protocol, secure: req.secure })
})
app.listen(3000, '127.0.0.1')
```

Inside nginx's existing TLS-terminating proxy location:

```nginx
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://127.0.0.1:3000;
```

The proxy overwrites a supplied value. Express then uses it for `req.protocol` and `req.secure`. This assumes local processes are trusted and nginx receives the client connection directly. See [Express behind proxies](https://expressjs.com/en/guide/behind-proxies/) and [nginx proxy_set_header](https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_set_header).

A numeric `trust proxy` setting counts hops; it is not an allowlist of proxy identities. Different-length routes can make a client closer than the configured count. Prefer a policy that matches the actual network.

After configuring proxy trust, a redirect can use `req.secure`. Place this before application routes and use your configured public hostname:

```javascript
app.use((req, res, next) => {
  if (!req.secure) {
    const path = new URL(req.originalUrl, 'http://internal')
    const target = new URL('https://app.example.com')
    target.pathname = path.pathname
    target.search = path.search
    return res.redirect(308, target.href)
  }
  next()
})
```

The configured origin keeps the destination authority under application control; only the request path and query are copied. `308` preserves the request method; a redirected POST remains a POST. [MDN's 308 reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/308) describes that behavior. Behind an HTTP-only edge listener, this middleware always redirects; the edge must also provide the HTTPS route.

## Security Rule: Trust Boundaries Matter

For the setup above, send a forged header through nginx and inspect `/scheme`:

```bash
curl -i -H 'X-Forwarded-Proto: http' https://app.example.com/scheme
```

Replace the hostname with your TLS virtual host. The app should report the HTTPS edge scheme, because nginx overwrites the supplied field. If a CDN terminates TLS before nginx, `$scheme` instead describes CDN-to-nginx transport; use that provider's documented scheme signal and restrict who can reach the origin.

Do not build redirect destinations from an unchecked Host value. A configured public origin avoids making the request choose a redirect hostname.

## Standard Alternative

[Forwarded](https://howhttpworks.com/headers/forwarded) expresses the scheme as `proto=https`. Switching names requires explicit support in the application; Express's documented trust proxy handling uses X-Forwarded fields.

## Related Headers

- [Forwarded](https://howhttpworks.com/headers/forwarded)
- [X-Forwarded-For](https://howhttpworks.com/headers/x-forwarded-for)
- [Host](https://howhttpworks.com/headers/host)
