You move your static assets to a CDN subdomain like cdn.example.com, refresh your site, and your clean typography instantly drops back to Times New Roman or a generic sans-serif fallback. Meanwhile, the browser console is screaming red: font loading blocked because there’s no Access-Control-Allow-Origin header on the resource.
The frustrating part? Images, scripts, and CSS from the exact same CDN domain load without a hitch. Here is how to fix CORS font errors across your origin server and CDN edge, verify the headers with curl, and avoid the cache-poisoning trap that keeps broken responses stuck at the edge.

Why Browsers Enforce CORS on Web Fonts
Browsers treat font files (.woff, .woff2, .ttf, .otf, .eot) with much stricter rules than images or scripts. Under the W3C CSS font specification, user agents are required to enforce Cross-Origin Resource Sharing (CORS) when fetching fonts via @font-face.
When your HTML page at https://example.com pulls a stylesheet from https://cdn.example.com/css/main.css, the browser handles that fetch without complaint. But once that CSS file tries to load https://cdn.example.com/fonts/inter-var.woff2, the browser treats it as a cross-origin font request and attaches an Origin: https://example.com header.
If the response coming back from the CDN does not include an explicit Access-Control-Allow-Origin header, the browser simply refuses to load the font. The Network tab will ironically display 200 OK, but the rendering engine throws a DOMException and renders fallback glyphs instead.
Diagnosing the Failure with DevTools and cURL
Don’t guess what your CDN is sending back. Check the raw HTTP response headers directly so you can see whether an Origin header changes anything.
Fire off a curl command against your broken CDN asset, simulating a cross-origin request from your root domain:
curl -sI -H "Origin: https://example.com" https://cdn.example.com/fonts/inter-bold.woff2Look through the output. If things are broken, you’ll see a 200 OK status code, but you won’t see an access-control-allow-origin header anywhere. It usually looks like this:
HTTP/2 200
server: cloud-edge
date: Wed, 12 Apr 2026 14:22:01 GMT
content-type: font/woff2
content-length: 34512
cache-control: public, max-age=31536000
x-cache: HITNext, bypass the CDN completely and run the same check against your origin server. That tells you immediately which layer is dropping the ball:
curl -sI -H "Origin: https://example.com" https://origin.example.com/fonts/inter-bold.woff2If your origin server doesn’t send the header, that’s where you start. If your origin does send it but your CDN doesn’t, your CDN edge configuration or its caching behavior is stripping the header.
Fixing Origin Headers in Nginx
If you’re running Nginx on the origin, open your virtual host file (typically in /etc/nginx/sites-available/) or your main nginx.conf.
You want a dedicated location block for font assets that attaches the CORS header and sets proper cache controls. Don’t throw wild CORS headers across your entire site—scope them specifically to static fonts and media files.
Add this block inside your server { ... } configuration:
# Match web font extensions
location ~* .(?:woff2?|eot|ttf|otf)$ { add_header Access-Control-Allow-Origin "*" always; add_header Access-Control-Allow-Methods "GET, OPTIONS" always; add_header Access-Control-Allow-Headers "*" always; add_header Vary "Origin" always; add_header Cache-Control "public, max-age=31536000, immutable"; # Handle preflight OPTIONS requests cleanly if ($request_method = 'OPTIONS') { add_header Access-Control-Allow-Origin "*" always; add_header Access-Control-Max-Age 86400; add_header Content-Length 0; add_header Content-Type "text/plain; charset=utf-8"; return 204; } try_files $uri =404;
}Don’t skip that always flag. By default, Nginx’s add_header directive only fires on 200, 204, 301, 302, 304, or 307 responses. If you leave always out, error responses or edge-case redirects won’t include the CORS headers, turning minor issues into frustrating debugging sessions.
Check your syntax and reload Nginx:
sudo nginx -t && sudo systemctl reload nginxIf you’re handling long-term caching headers on these same files, take a look at our guide on Nginx Cache-Control immutable headers for static assets to ensure your directives aren’t stomping on each other.
Fixing Origin Headers in Apache and .htaccess
If you run an Apache origin (common on shared hosting and traditional WordPress stacks), you can send the headers via mod_headers in your root .htaccess file.
First, make sure mod_headers is actually enabled. On an Ubuntu/Debian host, run:
sudo a2enmod headers && sudo systemctl restart apache2Next, open your .htaccess file and drop this block near the top, before any URL rewrite rules:
Header set Access-Control-Allow-Origin "*" Header set Access-Control-Allow-Methods "GET, OPTIONS" Header set Access-Control-Allow-Headers "*" Header append Vary "Origin"
If your compliance setup prohibits a wildcard *, you can restrict access strictly to your primary domain:
SetEnvIf Origin "^https?://(www.)?(example.com)$" AccessControlAllowOrigin=$0
Header set Access-Control-Allow-Origin "%{AccessControlAllowOrigin}e" env=AccessControlAllowOrigin
Header append Vary "Origin"
If you run WordPress alongside caching plugins, check our breakdown on configuring Cache-Control headers in WordPress .htaccess for CDN setups so your font directives don’t collide with media expiration rules.
CDN Edge Configuration and Header Forwarding
Getting your origin right is only half the battle. Plenty of CDNs strip origin CORS headers by default or cache a generic, non-CORS response, then serve that cached file to every cross-origin visitor that follows.
Check these three areas on your CDN:
- Forward the Origin Header: Ensure your CDN distribution forwards the incoming
Originheader to your origin server. If the edge strips the header before forwarding the request, your origin web server won’t know it needs to return CORS headers. - Vary by Origin: Make sure the CDN respects the
Vary: Originheader. Without this, a direct browser navigation or internal script fetch (which sends noOriginheader) can populate the edge cache first, locking in a headerless response for everyone else. - Inject Headers at the Edge: If you don’t control the origin server, inject the header directly using CDN edge rules or edge workers targeting
*.woff2,*.woff, and*.ttfextensions.
If you’re wiring up an external CDN, check our guide on connecting DNS to CDN with an external origin so DNS routing doesn’t trip up SSL handshakes or asset paths.
Also confirm your static CDN rule doesn’t pass session cookies. Take a look at how to strip Set-Cookie headers for static assets to keep your cache hit ratio high.
The Cache-Key Trap: Why Fonts Stay Broken After the Fix
Here is a gotcha that burned me during a production migration: I fixed Nginx, reloaded the server, verified with curl that the origin was returning CORS headers, and Chrome still threw the exact same font errors on the live site.
Why? The CDN edge node had already cached the font file before the origin had CORS configured. That edge node was sitting on a copy of inter-bold.woff2 with a 30-day TTL and zero CORS headers.
On top of that, modern browsers cache fonts aggressively on local disk. Even if the CDN recovers, your local browser might keep serving the stale, broken response from disk.
Here’s how to clear the pipeline properly:
- Purge the CDN Edge: Run a purge targeting your font paths or run a wildcard purge (e.g.,
/fonts/*or*.woff2) across all edge regions. - Bust the Cache on Test Requests: Use a query parameter to force a complete cache miss at the edge node:
curl -sI -H "Origin: https://example.com" "https://cdn.example.com/fonts/inter-bold.woff2?bust=001"If that request with ?bust=001 returns access-control-allow-origin: *, your origin and edge rules are working properly. Any remaining failures on your machine are just stale local browser cache.
Testing the Entire Pipeline End-to-End
Before calling it resolved, run an end-to-end check in your browser.
Open Chrome or Firefox, launch DevTools (F12 or Cmd+Option+I), switch to the Network tab, and filter by Font.
Perform a hard reload (Cmd+Shift+R or Ctrl+F5). Click on any .woff2 asset and look at the Headers tab:
- Request Headers: Confirm
Origin: https://example.comwas dispatched. - Response Headers: Look for
Access-Control-Allow-Origin: *(or your specific origin). - Status Code: You should see
200 OKor304 Not Modifiedwith zero console warnings.
Once those headers line up, the console errors will vanish, and your custom web fonts will render properly.
For the underlying standards and directives, consult the MDN Web Docs: Cross-Origin Resource Sharing (CORS) and the official Apache mod_headers directive guide.
Frequently Asked Questions About CDN Font CORS
Can I just set Access-Control-Allow-Origin to * for fonts?
Yes. For public static fonts, setting Access-Control-Allow-Origin: * is standard practice and completely safe. Font files don’t expose user data, sessions, or private tokens. Restricting font files to specific domains adds maintenance headaches without improving your security posture.
Why do images load fine from the CDN while fonts fail?
The HTML standard allows cross-origin loading for images (via ) by default without requiring CORS headers. Web fonts loaded through CSS @font-face follow stricter specifications that mandate CORS checks to prevent unauthorized font hotlinking and cross-origin tracking.
Do I need to handle OPTIONS requests for fonts?
Usually, no. Fonts fetched via standard CSS @font-face rules issue simple GET requests without preflight checks. However, if you load fonts dynamically via JavaScript using the CSS Font Loading API (like new FontFace()), the browser may dispatch an OPTIONS preflight request first depending on the fetch mode. Having an OPTIONS handler in place prevents surprises there.
What is the difference between CORS for fonts and CORS for APIs?
APIs often handle authentication, cookies, and sensitive payloads, requiring explicit origin allowlists and headers like Access-Control-Allow-Credentials: true. Fonts don’t need any of that. It’s purely a static file delivery issue where a wildcard * header gives the browser what it needs to render the glyphs.
Next Steps
Now that your fonts load cleanly, check the rest of your asset pipeline for edge-caching quirks. Make sure you use content hashes in your asset filenames so you can safely set edge cache lifetimes to a full year without ever serving stale UI assets to your users.

