Skip to content

Page caches store Accept-header 303 redirect and serve it to all visitors #30

@TBarregren

Description

@TBarregren

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

  1. Enable a full-page cache plugin (e.g., LiteSpeed Cache)
  2. Clear the cache
  3. Send a request with Accept: text/markdown:
    curl -sI -H "Accept: text/markdown" https://example.com/some-page/
  4. The plugin returns a 303 See Other redirect to /some-page.md with Vary: Accept
  5. LiteSpeed Cache (and most WordPress page caches) ignores the Vary: Accept header and caches the 303 response keyed by URL alone
  6. 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 main branch

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions