aoitcloud

Guides and How Tos

Forcing HTTPS: Redirects Done Right

How to redirect HTTP to HTTPS correctly with 301s in Apache and Nginx, and how to avoid the redirect loops that break sites behind proxies and CDNs.

30 October 2026 5 min read

Forcing HTTPS: Redirects Done Right — AOIT Cloud knowledge base

Having a valid SSL certificate isn’t the same as forcing every visitor to use it. If your site still answers on plain HTTP, some visitors, old bookmarks, and search engine indexes will keep using the insecure version unless you actively redirect them. This post covers how to do that redirect correctly, the most common way people accidentally break their own site while doing it, and what it means for mixed content.

Why 301, Not 302

A 301 redirect tells the browser, and any search engine crawler, that the move from HTTP to HTTPS is permanent. A 302 says it’s temporary. For an HTTP-to-HTTPS redirect that you intend to keep forever, 301 is the correct status code. It lets browsers and crawlers update their records to go straight to the HTTPS URL next time, and it consolidates ranking signals onto the HTTPS version rather than splitting them between two URLs that serve the same content.

Use 302 only if you genuinely expect the redirect to be temporary, such as during a short migration window. For a permanent policy of “this site is HTTPS-only,” it should be 301.

Redirecting in Apache (.htaccess)

The most reliable way to force HTTPS in Apache is with mod_rewrite, checking the HTTPS server variable rather than trying to parse the requested URL:

RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]

This should sit near the top of your .htaccess file, above any other rewrite rules, so the redirect happens before anything else runs. If Apache sits behind a load balancer or reverse proxy that terminates TLS for you, %{HTTPS} may never be “on” from Apache’s point of view even on a secure request, which leads straight into the redirect loop problem covered below. This kind of proxy configuration is one of the things Managed Cloud Hosting handles for you rather than leaving it for you to get right by hand.

Redirecting in Nginx

In Nginx, the clean approach is a dedicated server block for port 80 that does nothing but redirect, leaving your real site configuration entirely on the port 443 block:

server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name example.com www.example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    # normal site configuration below
}

Using return 301 rather than a rewrite rule is preferred here: it’s simpler, faster, and doesn’t need a regular expression to do a job that’s really just “send everyone to the HTTPS version of this exact URL.”

Avoiding Redirect Loops

The most common way this goes wrong is a server sitting behind something that terminates TLS on its behalf, such as a load balancer, CDN, or reverse proxy. The browser connects over HTTPS to the proxy, but the proxy then talks to your actual web server over plain HTTP internally. If your redirect rule checks the connection your web server sees rather than the connection the browser made, it will conclude every request is insecure and redirect it, and the proxy will pass that redirect straight back to the browser, which requests HTTPS again, which the proxy again forwards as HTTP, forever.

The fix is to check the X-Forwarded-Proto header that a well-configured proxy sets, instead of (or as well as) the local HTTPS variable. In Apache:

RewriteEngine On
RewriteCond %{HTTPS} off
RewriteCond %{HTTP:X-Forwarded-Proto} !https
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]

And the equivalent check in Nginx:

if ($http_x_forwarded_proto = "http") {
    return 301 https://$host$request_uri;
}

A few other things worth checking if you hit a loop:

  • Make sure only one layer in the chain is doing the redirect. If your CDN already forces HTTPS and your origin server also has its own redirect rule that doesn’t recognise the proxy correctly, they can fight each other.
  • WordPress specifically can add its own loop into the mix if the siteurl or home options in the database are still set to an http:// address while a plugin or server rule is also forcing HTTPS.
  • Clear your browser cache or test in a private window; a 301 is cached aggressively by browsers, so an old broken redirect can persist even after you’ve fixed the server config.

A quick way to check what’s actually happening, without a browser’s cache getting in the way, is curl:

curl -I http://example.com/

Look for a single Location: https://example.com/ header and a 301 status. If you instead see it bouncing back to an http:// location, or curl reports too many redirects, that confirms a loop rather than a working redirect. If you’d rather be alerted automatically the moment a redirect like this breaks, rather than finding out from a customer, website monitoring covers exactly this kind of regression.

Mixed Content Implications

Forcing the top-level page to HTTPS doesn’t automatically fix everything the page loads. If your HTML still references images, scripts, stylesheets, or fonts using hardcoded http:// URLs, the browser will load the page securely but then flag or block those individual insecure resources. That’s mixed content, and it’s a separate problem from the redirect itself; a correct 301 gets visitors onto the HTTPS page, but it can’t rewrite the http:// references buried inside that page’s own markup or database content. We cover diagnosing and fixing that specifically in the next post in this series.

Summary

Force HTTPS with a 301, not a 302, so the redirect is treated as permanent by browsers and search engines alike. Put the redirect in its own dedicated block or rule rather than bolting it onto existing logic, and if you’re behind a proxy, load balancer, or CDN, check X-Forwarded-Proto rather than relying solely on the local HTTPS variable, or you’ll end up chasing a redirect loop instead of a working site. Test with curl -I before trusting your browser, since browsers cache 301s aggressively and can hide a broken configuration behind an old correct result.

Get in touch

Drop our team a message today