-
Notifications
You must be signed in to change notification settings - Fork 7
Description
Summary
Full-page caches (LiteSpeed Cache, WP Super Cache, W3 Total Cache, etc.) store the 303 redirect from handle_accept_negotiation() and serve it to all subsequent visitors — regardless of their Accept header. This breaks the site for normal browsers: they get redirected to the .md URL instead of seeing the HTML page.
Steps to Reproduce
- Enable a full-page cache plugin (e.g., LiteSpeed Cache)
- Clear the cache
- Send a request with
Accept: text/markdown:curl -sI -H "Accept: text/markdown" https://example.com/some-page/ - The plugin returns a
303 See Otherredirect to/some-page.mdwithVary: Accept - LiteSpeed Cache (and most WordPress page caches) ignores the
Vary: Acceptheader and caches the 303 response keyed by URL alone - A normal browser visits
https://example.com/some-page/— the cache serves the cached 303 redirect, sending the browser to/some-page.md
Root Cause
handle_accept_negotiation() in RewriteHandler.php sends Vary: Accept but does nothing else to prevent caching. Most WordPress full-page caches do not respect Vary headers — they cache based on URL alone. The Vary: Accept header is correct per HTTP spec but insufficient for the WordPress caching ecosystem.
This is documented as a known risk in PITFALLS.md (Pitfall #5: "Content Negotiation Interferes with Caching"), but no protection has been implemented.
Recommended Fix
Add three layers of cache protection before the redirect, covering the WordPress ecosystem, HTTP standard, and LiteSpeed-specific API:
// Prevent page caches from storing this content-negotiation redirect.
if (!defined('DONOTCACHEPAGE')) {
define('DONOTCACHEPAGE', true);
}
header('Cache-Control: private, no-store');
do_action('litespeed_control_set_nocache', 'Content negotiation redirect');Why three layers:
| Layer | Target | Coverage |
|---|---|---|
DONOTCACHEPAGE |
WP Super Cache, W3TC, LiteSpeed Cache, and most WordPress page cache plugins | Universal WordPress convention |
Cache-Control: private, no-store |
CDNs, reverse proxies (Varnish, Nginx), browser cache | HTTP standard |
do_action('litespeed_control_set_nocache') |
LiteSpeed Cache specifically | LSCWP's own API; safe no-op if plugin is not active |
The .md URLs themselves remain fully cacheable — only the content-negotiation redirect (which depends on the Accept header) is excluded from caching.
Environment
- WordPress 6.x
- LiteSpeed Cache plugin (confirmed), but affects any full-page cache that ignores
Vary - Markdown Alternate latest version from
mainbranch