Skip to content

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

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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions