Force HTTPS in .htaccess, with one folder left on HTTP

Every page of a site should be served over HTTPS. Now and then one directory cannot be: a partner posts callbacks to an old endpoint without TLS, a device in the field speaks HTTP only, or an ACME client needs to answer a challenge before a certificate exists. The job is then to redirect everything to HTTPS except that one path.

The rule

In the .htaccess file of the document root:

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

Line by line: the engine is switched on, the request is only redirected when it did not arrive over TLS (directly or through a proxy), the directory /customers is excluded, and everything else goes to the same address on https. The query string is carried over automatically, because the target has no ? of its own.

Result for a site with the exception on /customers:

Request Answer
http://example.com/ 301 to https://example.com/
http://example.com/contact?ref=mail 301 to https://example.com/contact?ref=mail
http://example.com/customers/api.php 200, served over HTTP

The prefix trap

A condition like !^/customers/? looks equivalent, but RewriteCond is not anchored at the end. The pattern matches any path that starts with /customers, so /customers-archive/ and /customersonly.html are silently excluded from HTTPS as well. ^/customers(/|$) matches the directory and nothing else.

The same applies to the opposite mistake: !^customers without the leading slash never matches at all in .htaccess context, because REQUEST_URI always begins with a slash. The redirect then runs for the folder you wanted to protect.

Keep the ACME path open

If certificates are renewed with Let’s Encrypt over the http-01 challenge, the validation request must not be redirected while the certificate is missing or expired. One more condition keeps it reachable:

RewriteCond %{REQUEST_URI} !^/\.well-known/acme-challenge/ [NC]

This line costs nothing and saves the renewal that fails at the worst possible moment.

HSTS cancels the exception

A site on HTTPS usually also sends this header:

Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"

Once a browser has seen it, it rewrites every http:// request for that host to https:// before it leaves the machine. Apache never gets the request, so no rewrite rule can make an exception. For a human visitor the HTTP folder is then unreachable, while a server-to-server client that ignores HSTS still gets through — a difference that is hard to debug from a support ticket.

So decide: either the whole host is HTTPS-only with HSTS, or the exception lives on a hostname of its own, for example legacy.example.com, without the header and outside includeSubDomains.

The nginx version

There is no .htaccess in nginx; the same logic belongs in the server block that listens on port 80:

server {
    listen 80;
    server_name example.com;
    root /var/www/example;

    location ^~ /customers/ {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

location ^~ wins over the regular prefix match, so the exception is evaluated first.

Two checks before you call it done

Test the behaviour from the outside, not in a browser that may have cached a redirect:

curl -sI http://example.com/           | head -n 2
curl -sI http://example.com/customers/ | head -n 2

The first must answer 301 with a Location on https, the second 200.

And for search engines: content that stays available over both http and https is duplicate content. The excluded directory should either be a machine endpoint that nobody searches for, or carry a canonical link to the HTTPS address — otherwise the exception costs you rankings you did not intend to trade away.

Frequently asked questions

Why does my site redirect in an endless loop after adding these rules?

Because something in front of Apache terminates TLS — a load balancer, a CDN or a reverse proxy. Apache then sees plain HTTP on every request, %{HTTPS} stays off, and it redirects a request that already arrived over HTTPS. Add the condition RewriteCond %{HTTP:X-Forwarded-Proto} !https, which reads the header the proxy sets.

Should I use 301 or 302 for the redirect?

Test with R=302 and switch to R=301 when the rules behave. Browsers cache a 301 for a long time and often ignore a later change, so a wrong permanent redirect can follow you around for weeks on your own machine.

Can I keep HSTS and still have an HTTP folder?

No. Strict-Transport-Security tells the browser to upgrade every request to this host for the whole max-age, so a returning visitor never reaches the HTTP version. If a directory must stay on HTTP, put it on a separate hostname that is not covered by your HSTS header and not in the preload list.

Where do the rules go in a WordPress .htaccess?

Above the "# BEGIN WordPress" block. The WordPress rules end in a front controller that stops further processing, and anything you add inside the block is overwritten the next time permalinks are saved.

Questions about this topic?

We help with SEO, AI adoption and e-commerce.

Discuss your project →