Skip to content

Prevent page caches from storing content-negotiation redirect#31

Open
TBarregren wants to merge 1 commit intoProgressPlanner:mainfrom
TBarregren:fix/cache-negotiation-redirect
Open

Prevent page caches from storing content-negotiation redirect#31
TBarregren wants to merge 1 commit intoProgressPlanner:mainfrom
TBarregren:fix/cache-negotiation-redirect

Conversation

@TBarregren
Copy link

@TBarregren TBarregren commented Feb 26, 2026

Summary

  • Full-page caches (LiteSpeed Cache, WP Super Cache, W3TC, etc.) ignore the Vary: Accept header and store the 303 redirect from handle_accept_negotiation() keyed by URL alone
  • All subsequent visitors get redirected to the .md URL instead of seeing the HTML page
  • This adds three cache-prevention layers before the redirect to cover the entire WordPress caching ecosystem

The Fix

Three complementary layers added before the redirect in handle_accept_negotiation():

Layer Target Coverage
DONOTCACHEPAGE constant WP Super Cache, W3TC, LiteSpeed Cache, and most WP 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 inactive

The .md URLs themselves remain fully cacheable — only the content-negotiation redirect (which depends on the Accept header) is excluded from caching.

Test Plan

  • Normal HTML request returns 200 (no redirect):
    curl -sI https://example.com/some-page/
  • Accept: text/markdown returns 303 with Cache-Control: private, no-store:
    curl -sI -H "Accept: text/markdown" https://example.com/some-page/
  • .md URL returns 200 with text/markdown content type:
    curl -sI https://example.com/some-page.md

Fixes #30
Relates to PITFALLS.md Pitfall #5 ("Content Negotiation Interferes with Caching")

🤖 Generated with Claude Code

@jdevalk
Copy link
Member

jdevalk commented Feb 27, 2026

Hmm this solution would prevent caching everywhere, including in solutions that do understand they should... vary cache, by Vary.

@TBarregren TBarregren force-pushed the fix/cache-negotiation-redirect branch from 25e5ecd to 3d68a48 Compare March 19, 2026 13:44
Many full-page caches do not vary their cache key by the Accept
request header. When they store the 303 redirect from content
negotiation, all subsequent visitors are redirected to the .md
URL instead of seeing the HTML page.

Two complementary layers prevent this:

- Cache-Control: private header prevents shared caches (CDNs,
  reverse proxies) from storing the redirect while allowing
  browser caching.

- DONOTCACHEPAGE constant and LiteSpeed API call tell WP-level
  page caches not to store the response. This is behind a filter
  (markdown_alternate_disable_page_cache_on_redirect) so sites
  with Vary-aware caching can opt out.

The .md URLs themselves remain fully cacheable.

Fixes ProgressPlanner#30

Co-Authored-By: Claude <noreply@anthropic.com>
@TBarregren TBarregren force-pushed the fix/cache-negotiation-redirect branch from 3d68a48 to ec59c33 Compare March 19, 2026 13:55
@TBarregren
Copy link
Author

Thanks for the feedback @jdevalk — fair point. I've reworked the fix:

What changed:

  • Cache-Control changed from private, no-store to just private — only prevents shared caches from storing the redirect; browser caching is unaffected
  • DONOTCACHEPAGE and the LiteSpeed action are now behind a filter, so sites with Vary-aware caching (e.g. a properly configured Varnish) can opt out:
add_filter( 'markdown_alternate_disable_page_cache_on_redirect', '__return_false' );

The idea is that Cache-Control: private is always safe (it just tells shared caches to stay out of responses that vary by request header), while the WP-level prevention is a safety net for the many page-cache plugins that cache at the PHP level without considering Vary. But if your stack handles Vary: Accept correctly, the filter lets you skip that layer entirely.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants