Cover graphic with the kicker WordPress force HTTPS, read from the port-80 log, September 2026, and the headline The site address said https. 34,847 requests arrived over plain HTTP anyway. Subtitle: WordPress redirects three routes to HTTPS by itself and answers every other one on the scheme it was asked; eighteen days of one origin's port-80 log, what came in, what got a 200, and the toggle that was off. Three figures: 13.9 percent of origin requests arrived over plain HTTP in 18 days; 3 routes redirected to HTTPS by WordPress, home, wp-login.php and wp-admin; 2,681 directory listings served with a 200, now off. Footer: mxchat.ai, port-80 access log read 18 September 2026, Cloudflare in front, Always Use HTTPS off, no HSTS.

WordPress Force HTTPS: What Still Answers on Port 80

Every guide to forcing HTTPS on WordPress starts the same way: go to Settings → General, change both addresses to https://, save. This site did that years ago. Its certificate is fine, its canonical tags say https, and Cloudflare sits in front of it. Then I read the access log that Apache keeps for port 80, which I had never opened, and found 34,847 requests in eighteen days that arrived over plain HTTP and were answered over plain HTTP. That is 13.9% of everything the origin served in the window. The site address said https. The site did not enforce it.

This post is what those requests were, what WordPress did with them, why it redirected exactly three routes and nothing else, and which switch was off. The measurements are from one production install: WordPress 7.1 on cPanel, Apache, PHP 8.2, Cloudflare on the Pro plan with SSL mode Full. If you run anything similar, the same log is on your server and the same probe takes five minutes.

Nineteen routes, one scheme: what plain HTTP actually got

Before reading the log I asked the site directly. Nineteen requests, each to http:// rather than https://, through Cloudflare like a visitor’s browser would go, with a query string so that no cache could answer for WordPress. Three routes redirected. Sixteen did not.

Route over plain HTTPResponseWho decided
/ (front page)301 → httpsWordPress, X-Redirect-By: WordPress
/wp-login.php302 → httpsWordPress
/wp-admin/302 → httpsWordPress
A blog post (the most-linked page on the site)200, full pagenobody
/documentation/, /blog/, /shop/200nobody
/my-account/ (the WooCommerce login form)200nobody
/feed/200nobody
/wp-json/ and /wp-json/wp/v2/posts200, JSONnobody
/wp-admin/admin-ajax.php (POST)200nobody
/sitemap_index.xml, /post-sitemap1.xml200nobody
/robots.txt, an uploaded image200Apache, never reaches PHP
/xmlrpc.php404a deny rule in .htaccess
/product/…, /cart/, /checkout/403a Cloudflare managed challenge, which answers curl with a 403 on either scheme

The three redirects are not a policy. They are three separate lines of core that happen to check the scheme, and once you read them you can predict the other sixteen.

  • wp-login.php and wp-admin. wp-includes/default-constants.php (lines 353–360 in 7.1) defines FORCE_SSL_ADMIN as true whenever the site URL starts with https. wp-login.php line 15 then does if ( force_ssl_admin() && ! is_ssl() ) and redirects; every admin screen goes through auth_redirect(), which does the same. This is the only scheme enforcement WordPress ships, and it covers logging in and the dashboard. It does not cover admin-ajax.php, which is loaded by the same directory but never calls auth_redirect().
  • The front page. This site uses a static page as its front page. redirect_canonical() in wp-includes/canonical.php (line 264) has a branch for exactly that: when the page on front is requested, redirect to home_url( '/' ). home_url() builds from the https site address, so the requested http://mxchat.ai/ and the canonical https://mxchat.ai/ differ and a 301 goes out. It is an accident of the static-front-page rule. A site with a posts index as its front page would not get it.
  • Everything else. redirect_canonical() starts (line 71) by building the requested URL from the scheme the request came in on, and every canonical comparison keeps that scheme. Posts, pages, archives and feeds are canonicalised for slashes, capitalisation and query arguments, never for http versus https. The REST API exits at parse_request, before template_redirect ever fires, so canonical redirects do not apply to it at all. Static files never reach PHP.

So the honest summary of “WordPress forces HTTPS when the site address is https” is: it forces it for the login form and the dashboard, and by coincidence for a static front page. The rest is your web server’s job, or your CDN’s. On this site neither had been told.

The switch that was off

Cloudflare has a zone setting called Always Use HTTPS. It answers every plain-HTTP request at the edge with a 301 to the https version, before the request reaches the origin. I read the zone through the API this morning: always_use_https: off, ssl: full, automatic_https_rewrites: off. No page rules, no redirect rules. There is no RewriteCond %{HTTPS} block in .htaccess either, and the origin sends no Strict-Transport-Security header, so a browser that has been to the site over HTTPS has not been told to stop trying HTTP.

With SSL mode Full, Cloudflare connects to the origin on the same scheme the visitor used, so every plain-HTTP visitor and bot lands on port 80 at the origin and gets logged there. That log is the rest of this post.

Horizontal bar chart titled What 34,847 plain-HTTP requests got from one WordPress origin, covering every request logged on port 80 from 31 August to 17 September 2026, 18 days, 13.9 percent of all origin requests in the window. Bars by status: 403 Forbidden 12,689 or 36.4 percent, annotated the .htaccess IP blocks, xmlrpc.php and wordlist paths; 200 OK 10,728 or 30.8 percent, served over plain HTTP; 404 Not Found 6,075 or 17.4 percent; 500 Server Error 2,706 or 7.8 percent, core PHP files loaded directly and fatal on include; 301 Moved 2,450 or 7.0 percent, 868 of them the home page; 302 Found 158 or 0.5 percent, 52 of them wp-login.php; other 41. Footer: redirects of any kind 2,608 requests, 7.5 percent; WordPress redirected exactly three routes to HTTPS on its own, the front page, wp-login.php and wp-admin, and everything else answered on the scheme it was asked on.

Eighteen days of port 80: 34,847 requests, 1,764 addresses

cPanel keeps two access logs per site, one for port 443 and one for port 80. Yesterday’s post on who was running this site’s WP-Cron was the first time I opened the second one, because the answer to that question was in it. Today I read the whole thing: 31 August 08:27 to 17 September 23:45, 34,847 requests from 1,764 distinct addresses. The HTTPS log for the same window holds 215,487, so plain HTTP was 13.9% of what the origin handled.

By status, 36.4% were refused with a 403, mostly by the IP-block and xmlrpc.php rules already in .htaccess. 30.8% got a 200. 17.4% were 404s, 7.8% were 500s (more on those below), and 7.5% were redirects of any kind. Of the 2,450 permanent redirects, 868 were the front page doing its accidental https redirect; of the 158 temporary ones, 52 were wp-login.php doing its deliberate one. Those two numbers are the whole of WordPress’s contribution to the problem.

By user agent, 42.1% sent none at all, 32.0% sent a browser string, 23.1% identified as some crawler or tool, and 2.7% were Cpanel-HTTP-Client, which is the server checking its own certificate-validation files (they have to be reachable over port 80, and they were: 878 ACME challenge requests, every one a 200, every one legitimate). Verified Googlebot, by address range and reverse DNS rather than by the string it sends, accounts for 48 requests, against 3,207 over HTTPS in the same 18 days. Google does hold http:// URLs for this host; it just uses them rarely. Thirty-one of the 48 came on 17 September, when Googlebot asked for http://mxchat.ai/, took the 301, and then fetched 29 stylesheets and scripts over plain HTTP anyway, because the redirect only moved the document, not the assets it was rendering.

Vertical bar chart titled Plain-HTTP requests per day: two sweeps hold 46 percent of the window, showing requests on port 80 at the origin for each day from 31 August to 17 September 2026. Most days are blue bars between 397 and 2,566: 1,761; 1,877; 2,566; 2,314; 397; 666; 1,584; 569; then 8 September in red at 9,445; 919; 494; then 11 September in red at 6,506; 1,905; 924; 1,183; 592; 568; 577. Subtitle: 8 September is one address, 146.70.194.252, 8,446 requests, and 11 September is three addresses in one slash-24, 185.177.72.x, 5,880. Footer: the other sixteen days run 397 to 2,566 requests, wordlist scanners, DotBot, cPanel's own certificate checks, and 48 verified Googlebot requests in the whole window against 3,207 over HTTPS in the same 18 days.

Two days carry 45.8% of the window. On 8 September one address, 146.70.194.252, made 8,446 requests; on 11 September three addresses in 185.177.72.0/24 made 5,880 between them. Neither was looking for pages. They were working through WordPress core’s file list, which brings us to the 200s.

Where the 10,728 plain-HTTP 200s went

Horizontal bar chart titled Where the 10,728 plain-HTTP 200s went, every 200 response on port 80 classified by path. Core PHP files loaded directly 5,669, annotated 6,598 distinct paths from 142 addresses and 2,705 more got a 500; directory listings 2,681, 1,357 directories under wp-includes and wp-content, autoindex was on; ACME challenges from cPanel AutoSSL 878, legitimate, certificate validation has to arrive over port 80; HTML pages 701, 298 URLs, 223 with a browser user agent, my-account 25 times; static assets 463; robots.txt 214, 11 by verified Googlebot; RSS feeds 102, 78 DotBot and 20 wp.com feedbot for two subscribers; REST API 16; wp-cron.php 4, the four passes that ran this site's scheduler. Footer: direct file loads and directory listings are not an HTTP problem, they answer the same over HTTPS; the port-80 log is just where they are easy to see because nothing else is there.

5,669 of the 200s were core PHP files requested directly: /wp-includes/PHPMailer/POP3.php, /wp-includes/fonts/class-wp-font-face.php, /wp-admin/install.php, 6,598 distinct paths from 142 addresses, 15,535 requests in all. Ask Apache for a class file and PHP runs it on its own, outside WordPress. Most such files just declare a class and exit, which is an empty 200. Some reference a parent class that was never loaded, which is a fatal error and a 500: 2,705 of those, each one written to the PHP error log, which is now 27 MB and has 516 entries for class-wp-widget-block.php alone. The scanner does not care which; the pattern of 200s and 500s across a few thousand files is a version fingerprint, and one sweep of it is what 8 September looks like.

2,681 were directory listings. Request /wp-includes/js/jquery/ and, on this server until this morning, Apache answered with an Index of page listing every file. The scanners walked 1,357 directories under wp-includes and wp-content that way, and /wp-content/uploads/ 47 times. I confirmed it over HTTPS before touching anything: /wp-content/uploads/2025/01/ rendered an 11 KB listing of every image uploaded that month, and the plugin directories listed their source files. This is not an HTTP problem and it is not new. It is the Options -Indexes line from the WordPress hardening guide, missing, and the port-80 log is simply where it showed up first because scanners prefer port 80 and nothing else is in that file to hide it.

701 were HTML pages, 298 distinct URLs, of which 223 requests carried a browser user agent, and 25 of those were for /my-account/. That is the WooCommerce login form, served over plain HTTP, with a password field. Anyone who typed the address without the scheme and had not visited before, on a browser without HTTPS-first defaults, got exactly that. Cloudflare re-encrypts the hop to the origin under Full mode, so the plaintext leg is visitor-to-edge, but the visitor’s password crossed it. The rest of the page class is what you would expect: /add-ons/, /documentation/, /about/, the terms and privacy pages, the blog index.

102 were RSS feeds, 78 of them to DotBot and 20 to wp.com feedbot, which is Jetpack’s subscription service polling the feed over http on behalf of its two subscribers. 214 were robots.txt, 11 of them from verified Googlebot. 16 were the REST API, and 4 were wp-cron.php, which is the part that stops me from fixing this today.

What this costs, and to whom

The security cost is concentrated in one row of that chart: a login form over plain HTTP, 25 times in 18 days, plus 223 other page loads with browser user agents that may have carried a session cookie. WordPress sets its auth cookie with the secure flag only when the login happened over HTTPS, which on this site it always did, so an existing session would not have leaked; a new login typed into the plain-HTTP form is the exposure. The scanning traffic is not a scheme problem. The direct file loads, the 500s and the directory listings answer identically over HTTPS, and after last week’s audit of what this install keeps in plaintext a file listing of the uploads directory is the mildest thing on the list. It is still a listing.

The SEO cost is small and real. Google holds http:// versions of this site’s URLs, fetches them occasionally, follows the front page’s 301, and then fetches assets over http. Every page carries a canonical tag pointing at https, so nothing is being indexed twice, but 48 crawl requests in a fortnight went to a scheme the site does not want, on a site where five of the last thirteen posts were never fetched at all. The sitemap, the feed and the REST index all answering on http means every discovery surface the site has is also duplicated on http. None of that is fatal. All of it is unnecessary.

The operational cost is the error log. 2,705 fatal errors from direct file loads in 18 days is 150 a day of noise in the one file you read when something is actually wrong, and on a host where every PHP request costs 1.6 seconds of bootstrap, 15,535 direct file loads are 15,535 PHP processes started for a scanner’s benefit.

The fix ladder, and the rung I did not climb

FixWhereWhat it closesStatus here
Options -Indexestop of .htaccessthe 2,681 directory listings; directories now return a 404done today, verified over HTTPS and HTTP
Always Use HTTPSCloudflare, SSL/TLS → Edge Certificatesevery plain-HTTP request from a visitor or bot, answered with a 301 at the edge; the origin never sees itdeliberately not yet, see below
HSTSCloudflare, same screen, or a header from the originreturn visitors’ browsers stop trying http at all; a downgrade on first visit becomes the only windowafter Always Use HTTPS, with a short max-age first
RewriteCond %{HTTPS} off → 301.htaccess, before the WordPress blockplain-HTTP requests that reach the origin without going through Cloudflare, which the edge toggle cannot seeafter the crontab line, same reason
Deny direct loads in wp-includes.htaccess, the hardening guide’s blockthe 5,669 200s (almost all of them empty) and the 2,705 fatals; scanners get a 403 without starting PHPnext; needs a test against the few files legitimately loaded directly (ms-files.php, wp-tinymce.php)
define( 'FORCE_SSL_ADMIN', true )wp-config.phpnothing new here; core already sets it from the https site addressnot needed

Always Use HTTPS is a single toggle and the correct fix, and I am not flipping it today for a reason that only makes sense if you read the previous post. This site’s wp-config.php has DISABLE_WP_CRON set and no crontab entry to replace it, so nothing on the server runs the scheduler. The only requests for wp-cron.php that reached PHP in eighteen days were the four in that chart, all from webshell scanners working a wordlist over plain HTTP, and each one ran the queue. Turn on Always Use HTTPS, or add the rewrite rule at the origin, and those four become 301s, the scheduler never runs again, and 55 overdue events become 57. The order has to be: a real crontab line first, then the redirect. That is a two-line change and it is the site owner’s, not mine, so the redirect waits, and the login form stays on http for now, and I have said so in writing rather than quietly.

The directory listings did not have to wait. Options -Indexes touches nothing the scheduler depends on, so it went in this morning at the top of .htaccess with a backup beside it. Every directory URL that returned an Index of page yesterday returns the theme’s 404 now, checked at the origin and through the CDN. The one side effect worth naming: a scanner walking directories used to get a cheap autoindex response from Apache, and now gets WordPress’s 404 template, which is a full bootstrap. At 150 directory requests a day that is noise; the wp-includes deny rule, when it goes in, will make it a 403 before PHP starts.

How to read your own port-80 log in five minutes

On cPanel the file is ~/access-logs/<domain>, with the HTTPS log beside it as <domain>-ssl_log, and the previous month is under ~/logs/ gzipped. Note that the port-80 file is written with a delay of several minutes on a quiet site: my own probe requests took twelve minutes to appear, which nearly convinced me they had not reached the origin at all. Three commands answer the three questions this post asked:

  1. How much arrives over plain HTTP? wc -l on both files. Anything above a few percent means nothing upstream is redirecting.
  2. What does WordPress do with it? awk '{print $9}' | sort | uniq -c for the status mix, then awk '$9==200 {print $7}' | sort | uniq -c | sort -rn | head -50 for what was actually served. If you see paths ending in / under wp-includes with a 200, you have listings. If you see .php files under wp-includes with a 200 or a 500, you have direct loads.
  3. Who is Google here? Filter on ^66\.249\. and a Googlebot user agent, not on the user agent alone; on this site a third of the “Googlebot” strings come from other addresses. Then check curl -sI http://yourdomain/some-post/ yourself. If it is a 200 rather than a 301, Google is getting the same.

Then the one toggle, unless you also have a scheduler that is being run by strangers, in which case fix that first. If you are running MxChat on the site, nothing in the plugin cares which scheme the request arrives on; the chat endpoint answers on both, which is one more reason to close the plain one, and the documentation assumes you have.

Frequently asked questions

My WordPress address is https. Doesn’t that force HTTPS?

It forces the login form and the admin screens, through FORCE_SSL_ADMIN, which core sets automatically from the https address. It does not redirect posts, pages, feeds, the REST API, admin-ajax.php or static files. Those need a rule at the web server or the CDN.

Why did the front page redirect but the posts did not?

Because the front page on this site is a static page, and redirect_canonical() has a special case that sends requests for the page-on-front to home_url('/'), which carries the https scheme. Every other canonical comparison preserves the scheme the request arrived on. A site whose front page is the posts index would not get even that redirect.

Cloudflare’s SSL mode is Full. Isn’t the connection already encrypted?

The hop from Cloudflare to your origin is, on either scheme. The hop from the visitor to Cloudflare is only encrypted if the visitor used https, and with Always Use HTTPS off, Cloudflare passes plain-HTTP requests through to the origin on port 80 rather than redirecting them. Full mode protects the back half. The toggle protects the front half.

Does serving pages on both schemes hurt rankings?

Not directly, as long as the canonical tag and the sitemap point at https, which they do here. It spends crawl on URLs you do not want indexed, and it gives Googlebot a working http version of every discovery surface. On a site that already struggles to get new posts fetched, that is crawl budget going to the wrong scheme.

Why not just add the .htaccess redirect and be done?

On most sites, do exactly that, or flip the CDN toggle, which is cheaper still. On this site the only process running WP-Cron for the last six weeks has been a scanner arriving over plain HTTP, so closing plain HTTP before adding a real cron job would stop the scheduler entirely. The order matters here and probably nowhere else.

Similar Posts