Cravatar Overseas CF Acceleration Pitfalls — From 525 to 403 to 301 to 404 and Finally to 200

Background

cravatar.com is an avatar service with over 20 million daily API calls. Its origin server is located in Nanjing (140.210.20.196), resulting in high latency for overseas users. Goal: Accelerate overseas traffic via Cloudflare CDN, while leaving domestic traffic unchanged.

Sounds simple, right? Just use Alibaba Cloud DNS-based geo-routing + Cloudflare for SaaS — textbook configuration. Yet this seemingly “simple” requirement led me to fall into six distinct pitfalls and iterate through six versions of a Cloudflare Worker before finally achieving success.


Round One: Cloudflare for SaaS + custom_origin_server (Error 525)

Standard approach: Add cravatar.com as a custom hostname under the wpcy.net zone, and set custom_origin_server to cravatar.wpcy.net (a gray-clouded A record pointing to the origin IP).

Result: Error 525 (“SSL handshake failed”).

Investigation revealed that Cloudflare sent the SNI cravatar.wpcy.net during origin requests, but the origin server rejected it. We asked the Fedora DevOps team to update the nginx configuration to include server_name cravatar.wpcy.net.

Nginx worked correctly in local tests, yet external access still resulted in TCP resets. Ultimately, we discovered: The issue wasn’t nginx — it was the datacenter’s network-layer SNI filtering, which only permits pre-registered (ICP-filing-compliant) domains. Since cravatar.wpcy.net lacked ICP filing, it was blocked at the network layer.

Could we use the custom_origin_sni parameter to force Cloudflare to send cravatar.com as the SNI during origin requests? Sorry — this is an Enterprise-only feature.

Lesson learned: SNI filtering in Chinese datacenters operates at a far lower (deeper) network layer than expected — even correct nginx configurations won’t help.


Round Two: Cloudflare Worker + resolveOverride over HTTPS (Still Error 525)

Since Cloudflare for SaaS origin settings couldn’t resolve the SNI issue, we tried full control via a Worker.

Idea: The Worker fetches https://cravatar.com/path. Because the URL hostname is cravatar.com, the TLS SNI becomes cravatar.com (which passes the whitelist). Meanwhile, resolveOverride points to the gray-clouded DNS record to obtain the origin IP (avoiding DNS loopback).

Result: Still error 525.

Reason: resolveOverride changes the resolved IP address, but Cloudflare’s internal TLS layer uses the hostname from resolveOverride (cravatar.wpcy.net) — not the URL hostname — for SNI.

This behavior contradicts official documentation (which states SNI follows the URL hostname), yet real-world testing confirmed it. Likely a special behavior specific to Cloudflare for SaaS.

Lesson learned: In Cloudflare for SaaS contexts, resolveOverride may behave differently regarding SNI compared to standard zones.


Round Three: Worker Fetching Directly by IP (Error 403)

If domain names cause trouble, what about fetching directly by IP?

fetch('http://140.210.20.196/avatar/test', { headers: { Host: 'cravatar.com' } })

Result: Error 403 (“Direct IP access not allowed”).

Cloudflare Workers prohibit fetching by raw IP addresses. This is a hard security policy — no bypass exists.

Lesson learned: Cloudflare Worker fetch() must use domain names; IPs are strictly forbidden.


Round Four: HTTP Origin Fetch + cravatar.wpcy.net (Error 404 / Datacenter Blocking)

Let’s try HTTP (port 80) to bypass TLS/SNI entirely: fetch('http://cravatar.wpcy.net/path'), setting Host: cravatar.com.

Result: Error 404, with response body showing the datacenter’s interception page: “This website is temporarily inaccessible.”

Cause: The datacenter filters not only TLS SNI, but also the HTTP Host header — and more critically, the target domain name parsed from the request URL. Although we set Host: cravatar.com, the request URL itself is cravatar.wpcy.net, so the datacenter sees cravatar.wpcy.net at the network layer and blocks it outright.

Lesson learned: The Nanjing datacenter enforces domain filtering across all protocols (HTTP & HTTPS), checking the DNS-resolved target domain — not just the Host header.


Round Five: HTTP Origin Fetch + cravatar.com Hostname (301 Redirect Loop)

Try fetch('http://cravatar.com/path') (with Host: cravatar.com, passing datacenter checks), using resolveOverride to get the origin IP.

Result: 301 redirect to https://cravatar.com/path.

Cause: The wpcy.net zone has “Always Use HTTPS” enabled, and Cloudflare applies this rule even to Worker subrequests. So: Worker issues HTTP → Cloudflare internally redirects 301 → HTTPS → back to Worker → infinite loop.

Lesson learned: Cloudflare’s “Always Use HTTPS” setting applies to Worker subrequests too — even when resolveOverride is used.


Round Six: Disable “Always Use HTTPS” + HTTP Origin Fetch (Finally, HTTP 200!)

Disable “Always Use HTTPS” on the wpcy.net zone, then have the Worker fetch http://cravatar.com/path with resolveOverride.

Result: HTTP 200! Avatars delivered correctly!

Other custom hostnames (e.g., wpcy.com, wpcommunity.com) handle HTTPS redirection independently — WordPress and Discourse each enforce HTTPS at the application level, eliminating reliance on Cloudflare zone-level settings.


Final Architecture

Overseas users → HTTPS → Cloudflare Anycast → Worker intercepts  
  → HTTP fetch to cravatar.com (resolveOverride → gray-clouded DNS → origin IP)  
  → Origin port 80 (Host=cravatar.com, passes ICP-filing check)  
  → Response returned → cached at Cloudflare edge (30-day TTL for /avatar/*) → user  

Domestic users → HTTPS → Alibaba Cloud DNS (geo-routed within China) → origin port 443 (direct connection)

The final Worker code is only 55 lines — but writing those 55 lines required six full iterations.

Key Lessons Summarized

  1. Chinese datacenters (at least this Nanjing facility) inspect both TLS SNI and HTTP Host/target domain — only pre-registered (ICP-filing-compliant) domains are permitted.
  2. custom_origin_sni in Cloudflare for SaaS is an Enterprise-only feature.
  3. In Cloudflare for SaaS contexts, resolveOverride may exhibit unexpected SNI behavior.
  4. Cloudflare Workers cannot fetch resources by raw IP address.
  5. Cloudflare’s “Always Use HTTPS” setting applies to Worker subrequests — even with resolveOverride.
  6. Final working solution: HTTP origin fetch + resolveOverride + disabling zone-level “Always Use HTTPS”.

None of these pitfalls are documented — all were uncovered through hands-on testing. Hope this saves future engineers some pain.

Pitfall #7: HTTP→HTTPS 308 Redirect from Origin Server Causes Infinite Loop

After deployment, users reported encountering ERR_TOO_MANY_REDIRECTS when accessing the cravatar.com homepage while using a VPN.

Root Cause: The Worker uniformly fetches resources from the origin server over HTTP for all paths. However, the origin server responds with an HTTP 308 redirect to HTTPS for any HTTP requests targeting paths other than /avatar/. This triggers the following loop: the browser follows the 308 redirect → request re-enters Cloudflare → Worker again fetches over HTTP → origin server again issues a 308 redirect → infinite loop.

The /avatar/* paths remain unaffected because the origin server directly returns HTTP 200 (no redirect) for those paths.

Temporary Fix: The Worker now detects when the origin server returns an HTTPS redirect to the same domain and responds with a graceful degradation page (breaking the loop). API endpoints continue functioning normally.

Permanent Solution: We have contacted fedora-devops to add conditional logic in the origin server’s nginx configuration—skipping the HTTPS redirect for HTTP requests bearing the X-CF-Worker header. The Worker has already been updated to include this header in all origin-fetch requests.

Another Lesson Learned: Enforcing HTTPS redirects at the origin server combined with HTTP-origin fetching in Workers leads to redirect loops. This issue was not caught during testing of /avatar/ paths (since the avatar API does not perform redirects) and only surfaced when users accessed the homepage.

Fully Fixed

The fedora-devops team added a conditional rule in the origin site’s nginx configuration: HTTP requests bearing the X-CF-Worker header bypass the HTTPS redirect.

All endpoints now function correctly:

  • Homepage returns 200 :white_check_mark: (HTML, text/html)
  • /avatar/* returns 200 :white_check_mark: (images, cached for 30 days)
  • /favicon/* returns 200 :white_check_mark:
  • Direct domestic connections remain unaffected :white_check_mark:

The final Worker code has been streamlined to just 45 lines and is now officially live.

Complete List of Pitfalls Encountered (7 total):

  1. Cloudflare for SaaS custom_origin_server → Datacenter SNI whitelist blocking (error 525)
  2. custom_origin_sni → Enterprise plan only
  3. Worker HTTPS + resolveOverride → SNI follows the overridden hostname (error 525)
  4. Worker fetches by IP address → Blocked by Cloudflare (error 403)
  5. Worker HTTP request with non-ICP-registered domain as hostname → Datacenter Host header check blocks it (error 404)
  6. Worker HTTP request with ICP-registered domain → “Always Use HTTPS” setting causes infinite 301 redirects
  7. Origin server HTTP→HTTPS 308 redirect → Causes redirect loop when Worker fetches over HTTP (ERR_TOO_MANY_REDIRECTS)

Each issue was discovered only through hands-on testing—none are documented anywhere.

Pitfall #8: Page Rule 301 Redirects Lose the Path

When migrating cravatar.cn to Cloudflare, a Page Rule was configured to redirect cravatar.cn/* to the fixed URL https://cn.cravatar.com. It did not use $1 to preserve the path. As a result, requests like cravatar.cn/avatar/xxx were redirected to cn.cravatar.com (the homepage), causing all avatars to break.

Fix: Change the forwarding URL to https://cn.cravatar.com/$1.

Lesson Learned: In Cloudflare Page Rules, if the forwarding URL does not include $1, the original path is discarded. Though this is a basic mistake, its impact was severe — every avatar API call made via cravatar.cn failed completely.

Supplement: Origin Server Missing Cache-Control Response Header (Fixed)

While investigating the 0% cache hit rate for the cravatar.cn zone, a more fundamental issue was identified: the origin server’s avatar.php script does not set a Cache-Control header when successfully returning avatars.

Issue

The response headers from curl -I https://cn.cravatar.com/avatar/test do not include Cache-Control. Only error responses (via the c_die function) set Cache-Control: no-cache; successful avatar responses provide no caching directives whatsoever.

This means:

  • Browsers re-request avatars on every visit (no local caching).
  • The Cloudflare Worker edge cache lacks explicit TTL guidance.
  • Intermediate CDNs (e.g., the layer in front of cn.cravatar.com) also lack caching instructions.

Fix

A line was added at line 464 of /www/wwwroot/cravatar/wp-content/plugins/cravatar/pages/avatar.php (immediately after the Avatar-From header):

header( 'Cache-Control: public, max-age=2592000' );

This sets a 30-day cache duration (max-age=2592000 seconds), matching the expires 30d directive used for static files in nginx.

Verification

$ curl -sI https://cn.cravatar.com/avatar/test123 | grep cache-control
cache-control: public, max-age=2592000

$ curl -sI https://cravatar.com/avatar/test123 | grep cache-control
cache-control: public, max-age=2592000

The change is now active for both domains.

Note

The 0% cache hit rate for the cravatar.cn zone is expected — this zone only serves 301 redirects to cn.cravatar.com, and Cloudflare does not cache 301 responses. Actual avatar content is served by cn.cravatar.com, which bypasses Cloudflare and instead uses a separate CDN infrastructure.