How to check HTTP to https redirect

Request the http:// URL, follow the chain and print where it ended: curl -s -o /dev/null -L -w '%{num_redirects} %{scheme} %{url_effective}' http://example.com/. A working redirect reports one hop and a scheme of https. Zero hops with scheme http means the site answered the insecure request instead of moving it.

Why check this

This is the check that decides whether a link pasted into a chat window, a QR code or an old bookmark stays in clear text. Run it at staging sign-off, after every vhost or CDN change, and on any host that was recently added to a certificate. When it fails, a login form loads over HTTP, the browser posts the password in clear text, and nothing in the application log looks unusual because the server answered normally.

Prerequisites

Steps

  1. Step 1.

    Follow the chain and print the hop count and the final URL in one line.

    curl -s -o /dev/null -L -w 'redirects=%{num_redirects} scheme=%{scheme} code=%{response_code} url=%{url_effective}\n' http://www.cloudflare.com/
    
    redirects=1 scheme=https code=200 url=https://www.cloudflare.com/
  2. Step 2.

    Run the same line against a host that serves plain HTTP, so the failing output is on record.

    curl -s -o /dev/null -L -w 'redirects=%{num_redirects} scheme=%{scheme} code=%{response_code} url=%{url_effective}\n' http://httpbin.org/get
    
    redirects=0 scheme=http code=200 url=http://httpbin.org/get

    code=200 on an http scheme is the failure. A status of 200 here means the content was served, not that the check passed.

  3. Step 3.

    Print every hop with its status code, so you can see how many there are and which one adds the policy header.

    curl -s -o /dev/null -L -D - http://cloudflare.com/ | grep -i -E '^HTTP/|^location|^strict-transport'
    
    HTTP/1.1 301 Moved Permanently
    Location: https://www.cloudflare.com/
    HTTP/2 103
    HTTP/2 200
    strict-transport-security: max-age=31536000; includeSubDomains

    The 103 line is an early hints response on the final hop, not a redirect. Only the 301 moved the client.

  4. Step 4.

    Confirm the path and the query survive the hop.

    curl -s -o /dev/null -D - 'http://www.cloudflare.com/plans/?x=1' | grep -i -E '^HTTP/|^Location'
    
    HTTP/1.1 301 Moved Permanently
    Location: https://www.cloudflare.com/plans/?x=1

    A Location that points at https://www.cloudflare.com/ with the path dropped would be the bug this step exists to catch.

How to read the result

| What you see | What it means | What to do | | --- | --- | --- | | redirects=1 scheme=https code=200 | One hop, correct scheme | Nothing. Repeat for the other hostname. | | redirects=0 scheme=http code=200 | The site serves content over plain HTTP | Release blocker. Every form on that page posts in clear text. | | redirects=2 or more | The scheme and the host change in separate hops | Collapse them. Each extra hop is another clear-text request if it stays on HTTP. | | 302 instead of 301 on the upgrade | The move is advertised as temporary | Use 301. Caches and clients keep retrying HTTP for a temporary redirect. | | Location loses the path | Deep links land on the homepage | Fix the rewrite rule. Password reset and invite links break on this. | | redirects=0 code=000 | Port 80 refused the connection | Decide deliberately. A closed port is safe but breaks typed hostnames. |

Common mistakes

Sign: The browser always lands on HTTPS, so the redirect is recorded as working, and curl disagrees.Cause: The browser stored an HSTS policy on an earlier visit and rewrites the URL before a request is made, so no HTTP request is sent at all. curl keeps no HSTS state by default, which is why it sees the real server behaviour.
Sign: Only the homepage is tested and the redirect passes.Cause: Scheme upgrades are often written as a single rule pointing at the site root. The homepage redirects correctly and every deep link is sent to the root, which shows up later as broken email links rather than as a redirect bug.
Sign: The redirect works and a session cookie is still sent in clear text.Cause: The insecure request happens before the redirect. Any cookie without the Secure attribute is attached to that first request, so the redirect does not protect an existing session.
Sign: The apex is fine and www times out, or the other way round.Cause: The two hostnames are separate server blocks with separate certificates. Testing one proves nothing about the other, and users type both.

What to check next

FAQ

How to check if a site is served over HTTPS?

Step 1 answers it. %{scheme} prints the scheme of the URL curl actually ended on, which is the fact you want, rather than the scheme you typed.

How to check if HTTPS is working?

Request the https:// URL directly and read the status line. A TLS failure returns exit code 60 with no status line, which is a different problem from a missing redirect on port 80.

Should the redirect be 301 or 302?

301 for a permanent scheme upgrade. A 302 tells caches and clients to keep asking over HTTP next time, which leaves the insecure request in place on every visit.

Does closing port 80 count as a pass?

It removes the clear-text response, and it also removes the redirect. Users who type the hostname get a connection error instead of the site, so most teams keep port 80 open and serve only the 301.

Why does curl show one hop and the browser network panel shows none?

The browser upgraded the URL from its own HSTS store before sending anything. Test in a fresh profile, or trust the curl result.

Verified

Verified by Maks Vernycurl 8.21.0

Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.

basic4 minpublished updated Maks Verny