How HTTP Works

Request header · non-standard

> X-Forwarded-Proto: https

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.

Reviewed 2 min readintermediate2 sourcesTry itMarkdown
Direction
Request
Category
Proxies
Spec
MDN reference
Status
Non-standard: De-facto convention; the standard form is Forwarded
JS can set it
Yes
CORS-safelisted
No: may trigger a preflight
On this page

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:

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

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

X-Forwarded-Proto: https

MDN 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:

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:

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 and nginx 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:

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 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:

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 expresses the scheme as proto=https. Switching names requires explicit support in the application; Express’s documented trust proxy handling uses X-Forwarded fields.

Frequently asked questions

What is X-Forwarded-Proto?

X-Forwarded-Proto tells an origin app whether the original client connected to the edge over HTTP or HTTPS before a proxy forwarded the request inward.

Why is X-Forwarded-Proto important?

Without it, an app behind TLS termination may think every request is plain HTTP. That affects redirects, secure cookies, absolute URL generation, and security behavior.

How do I use X-Forwarded-Proto?

Use it only through trusted proxy configuration. Express uses it for req.protocol when trust proxy accepts the proxy address.

Can X-Forwarded-Proto be spoofed?

Yes. Never trust it from arbitrary clients. Only trust it when your proxy overwrites it and your app accepts it only from known proxy layers.

Sources

  1. MDN Web Docs: X-Forwarded-Protodeveloper.mozilla.org
  2. RFC 7239: Forwardedrfc-editor.org

Keep going

Browse /search