Cloudflare Flexible SSL sends a Worker's subrequest into a redirect loop

Christian Drexler

I run a small Cloudflare Worker on a subdomain of this site. It receives a few events from the pages and writes them into a database that sits on a server of mine, reachable under another hostname. The same Worker does the same job on other domains without complaint.

On this domain it wrote nothing. The pages loaded, the Worker answered, and no row arrived. It stayed that way until I looked, because nothing anywhere was red.

The symptom

The Worker's log had one line per attempt, and it was not a line I expected from a database call:

Too many redirects

The Worker has a small health route that tries a trivial query and reports the result. On the healthy domains it says db_connected: true. Here it said db_connected: null. The query did not fail with an error from the database. It never got as far as the database.

What was going on

Every Cloudflare zone has an SSL mode that decides how Cloudflare talks to the server behind it. The zones where the Worker behaved are on Full (strict). This one was still on Flexible.

Flexible means: the visitor talks HTTPS to Cloudflare, and Cloudflare talks plain HTTP to the origin. It exists for origins that have no certificate at all.

The mode also applies to what a Worker fetches inside its own zone. The database hostname the Worker calls lives in the same zone as the Worker on this domain. So the Worker's request, written as https:// in the code, left as plain HTTP. And a plain HTTP request to that hostname is answered the way it should be: with a 301 to the HTTPS address.

Put those two together and follow one request:

  1. The Worker fetches https://db.example.com/.
  2. Because the zone is on Flexible, the request goes out over HTTP.
  3. The answer is: use HTTPS, 301.
  4. The Worker follows the redirect and fetches https://db.example.com/ again.
  5. Back to step 2.

After a number of rounds the runtime gives up with "Too many redirects". The request was HTTPS all along in the Worker's code. The zone setting kept downgrading it on the way out, and the redirect kept sending it back.

This is the same loop that produces the well-known ERR_TOO_MANY_REDIRECTS in a browser on a Flexible zone. In a browser you see it at once, because the page does not load. In a Worker's subrequest nobody sees it. The Worker catches the error, the visitor gets their response, and the only trace is a log line.

Why the other domains were fine

On the other domains the Worker runs in a zone that is on Full (strict), so its request to the database hostname leaves as HTTPS and arrives as HTTPS. The Worker code was identical everywhere. So was the database. The only difference was one dropdown in one zone's settings, chosen long before there was a Worker.

That is what made it slow to find. I compared code, bindings, routes and secrets between a working domain and the broken one, and they matched line for line.

The fix

In the Cloudflare dashboard: the zone, then SSL/TLS, then Overview, and set the encryption mode to Full (strict). With that, Cloudflare connects to the origin over HTTPS and checks its certificate, and a Worker's HTTPS request stays HTTPS. Nothing has a reason to redirect, and the loop is gone.

Full (strict) needs a valid certificate on the origin. If the server has none, a Cloudflare Origin Certificate is free, lasts up to fifteen years and takes two minutes to install. There is no good reason left to run a zone on Flexible, and several to get off it: with Flexible the stretch between Cloudflare and your server is unencrypted, whatever the padlock in the browser suggests.

The change is live within seconds. I switched the mode, called the health route, and it said db_connected: true.

Before you flip the switch

One caution. Changing the mode affects every hostname in the zone. If some other origin in that zone really has no certificate, it will start failing the moment Cloudflare insists on HTTPS. So look through the DNS records of the zone first and make sure each proxied origin can answer on port 443. You can check from a terminal without going through Cloudflare:

curl -sI --resolve example.com:443:203.0.113.10 https://example.com/ | head -1

That asks the origin's IP address directly for the hostname. A status line means the origin speaks HTTPS for that name. A connection error means it does not, and that hostname needs a certificate before the zone can move.

The check I added

The health route was already there. What was missing was anyone asking it. Now, whenever the Worker is attached to a new domain, the last step is one line:

curl -s https://t.example.com/health

It has to say db_connected: true. A Worker that answers proves only that the Worker runs. The health line proves it can reach what it writes to, through the zone it actually sits in.

The SSL mode is now also the first thing I look at when a Worker behaves differently on one domain than on the others. It is a setting from the zone's first day, and nobody remembers choosing it.