Cravatar Overseas CF Acceleration: A Complete Pitfall Log — From 525 to 403 to 301 to 404 and Finally to 200

Background

cravatar.com is an avatar service handling 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: route overseas traffic through Cloudflare CDN for acceleration, 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 separate pitfalls and iterate through six versions of a Cloudflare Worker before finally solving it.


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

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

Result: Error 525 — SSL handshake failed.

Investigation revealed that Cloudflare sends SNI=cravatar.wpcy.net when connecting to the origin, but the origin server rejects that SNI. We asked DevOps to update the Nginx config to include server_name cravatar.wpcy.net.

Local Nginx testing passed, yet external access still resulted in TCP resets. Ultimately we discovered: this wasn’t an Nginx issue — the data center’s network layer enforces an SNI allowlist, permitting only registered (ICP-filing-compliant) domains. Since cravatar.wpcy.net wasn’t registered, it was dropped at the network layer.

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

Lesson: SNI filtering in Chinese data centers operates at a far lower network layer than expected — even correct Nginx configuration won’t help.


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

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

Idea: The Worker fetches https://cravatar.com/path. Because the URL’s hostname is cravatar.com, the TLS SNI becomes cravatar.com (which passes the allowlist); resolveOverride points to the gray-clouded DNS record to resolve the origin IP (avoiding DNS loops).

Result: Still 525.

Cause: Cloudflare Workers’ resolveOverride changes DNS resolution, but Cloudflare’s internal TLS layer uses the resolveOverride hostname (cravatar.wpcy.net) — not the URL’s hostname — for SNI.

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

Lesson: In Cloudflare for SaaS contexts, resolveOverride may cause unexpected SNI behavior, differing from standard zones.


Round Three: Worker Fetches Origin IP Directly (403)

If domain-based routing fails, can we just fetch the origin by its IP?

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

Result: 403 — “Direct IP access not allowed.”

Cloudflare forbids Workers from fetching raw IPs. This is a hard security policy with no known bypass.

Lesson: Cloudflare Workers’ fetch() must use domain names — IPs are strictly prohibited.


Round Four: Worker HTTP Origin Fetch + cravatar.wpcy.net (404 / Data Center Block)

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

Result: 404, returning the data center’s interception page: “This website is currently inaccessible.”

Cause: The data center filters both TLS SNI and HTTP Host headers — and crucially, checks the target domain resolved from the URL, not the Host header. Though we sent Host: cravatar.com, the request’s destination domain (from DNS resolution) was cravatar.wpcy.net, triggering immediate network-layer blocking.

Lesson: Nanjing’s data center performs domain filtering across all protocols (HTTP & HTTPS), checking the DNS-resolved target domain — not the Host header.


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

Try fetch('http://cravatar.com/path') (with Host: cravatar.com, passing data center checks), using resolveOverride to resolve the gray-clouded DNS record to 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 fetch → Cloudflare internally redirects to HTTPS → request returns to Worker → infinite loop.

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


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

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

Result: 200! Avatars delivered correctly!

Other custom hostnames (wpcy.com, wpcommunity.com) handle their own HTTPS redirects (WordPress and Discourse both enforce HTTPS natively), so they don’t rely on Cloudflare’s zone-level “Always Use HTTPS”.


Final Architecture

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

Domestic users → HTTPS → Alibaba Cloud DNS (mainland China routing) → origin port 443 (direct)

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

Key Lessons Learned

  1. Chinese data centers (at least this one in Nanjing) inspect both TLS SNI and HTTP Host/target domain, allowing only ICP-registered domains.
  2. custom_origin_sni in Cloudflare for SaaS is an Enterprise-only feature.
  3. In Cloudflare for SaaS contexts, resolveOverride may cause unexpected SNI behavior.
  4. Cloudflare Workers cannot fetch raw IPs — domain names are mandatory.
  5. Cloudflare’s “Always Use HTTPS” applies to Worker subrequests, even with resolveOverride.
  6. Final solution: HTTP origin fetch + resolveOverride + disabling zone-level “Always Use HTTPS”.

None of these pitfalls were documented — every insight came from 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 ERR_TOO_MANY_REDIRECTS when accessing cravatar.com’s homepage while using a VPN.

Root Cause: The Worker forwards all requests to the origin server over HTTP. However, the origin server responds with an HTTP 308 redirect to HTTPS for any request path other than /avatar/. The browser follows the 308 → lands back at Cloudflare → the Worker again issues an HTTP request to the origin → the origin again returns 308 → and so on, creating an infinite loop.

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

Temporary Fix: When the Worker detects that the origin server returns an HTTPS redirect to the same domain, it now serves a graceful degradation page instead of following the redirect — thereby breaking the loop. API endpoints continue to function normally.

Permanent Fix: We’ve contacted DevOps to add a conditional rule in the origin’s nginx configuration: skip the HTTPS redirect for HTTP requests bearing the X-CF-Worker header. The Worker already includes this header in all outbound origin requests.

Another Lesson Learned: Enforcing HTTPS redirects at the origin server combined with HTTP-origin fetches from Workers leads to redirect loops. This issue went undetected during testing of /avatar/ paths (since the avatar API doesn’t redirect), and only surfaced when users accessed the homepage.

Fully Fixed

DevOps added a conditional check in the origin site’s nginx configuration: HTTP requests with the X-CF-Worker header bypass the HTTPS redirect.

All routes now work 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 connections from within mainland China remain unaffected :white_check_mark:

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

Complete list of pitfalls encountered (7 total):

  1. Cloudflare for SaaS custom_origin_server → Data center SNI whitelist blocking (error 525)
  2. custom_origin_sni → Enterprise-only feature
  3. Worker HTTPS + resolveOverride → SNI follows the overridden hostname (error 525)
  4. Worker fetching by IP address → Blocked by Cloudflare (error 403)
  5. Worker HTTP requests + non-ICP-registered domain hostname → Data center Host header validation blocking (error 404)
  6. Worker HTTP requests + ICP-registered domain → “Always Use HTTPS” rule causing infinite 301 redirects
  7. Origin server HTTP → HTTPS 308 redirect → Worker HTTP re-origination causing redirect loop (ERR_TOO_MANY_REDIRECTS)

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

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.