Repository navigation
fix: stop the CDN caching markdown-negotiated docs pages as HTML - #701
Conversation
/docs/* requests with Accept: text/markdown (LLM/agent tooling) get rewritten by proxy.ts to the raw-markdown response, but that response shares its cache key with the HTML document since Cloudflare ignores Vary here. Whichever variant a given edge PoP sees first for a URL gets cached and served to everyone else hitting that PoP, including regular browsers, until the entry expires. This is the same bug class #642 fixed for the RSC flight payload, just never extended to the markdown content negotiation path. Mirror that fix: mark Accept: text/markdown responses private, no-store so the shared CDN never caches them.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📦 Next.js Bundle Analysis for librechat.aiThis analysis was generated by the Next.js Bundle Analysis action. 🤖 This PR introduced no changes to the JavaScript bundle! 🙌 |
|
@dustinhealy did you consider including accept in the |
I think Cloudflare only partitions its cache key on a header if there's also a Cache Rule configured for it on the zone; the origin returning
Next.js sends
That implies there's no Cache Rule for This does mean markdown-negotiated requests never get edge-cached. If a Cache Rule for |
Problem
/docs/*pages intermittently render as raw, unstyled Markdown text on a white page instead of the normal docs site, no console errors, but the response comes backContent-Type: text/markdown; charset=utf-8.Root cause
proxy.tscontent-negotiates/docs/*requests: anAccept: text/markdownrequest (sent by LLM/agent tooling, not browsers) gets rewritten to the raw Markdown response (proxy.ts:19-22,proxy.ts:68-72)./docs/*responses are cached at the shared CDN (Cloudflare, in front of Vercel) withCache-Control: public, s-maxage=86400, stale-while-revalidate=604800, keyed by URL only. The Markdown-negotiated response was never isolated into its own cache key, so whichever variant a given Cloudflare edge PoP saw first for a URL got served to everyone else hitting that PoP, including regular browsers, until the entry expired.Reproduction
curl -H "Accept: text/markdown" <docs-url>?cb=<random>-> MISS, cached as text/markdown under that URL's key.curl -H "Accept: text/html,..." <same-url>-> HIT, still text/markdown, poisoned.Reproduced 3/3 via curl and end-to-end in a real Chromium instance via Playwright, screenshots and video attached below.
Before (baseline):

Before (poisoned):

Video:
03-repro-video.mp4
After (poisoned - but still rendering properly):

Fix
Mirror #642: give the
Accept: text/markdown-negotiated response its ownCache-Control, split out from the shared/docs/*rule (next.config.mjs), so the CDN never caches it:Testing
Verified against a real
pnpm build+pnpm start(production mode;next devdisables custom cache headers entirely, so it can't be used to check this):Not yet verified: this hasn't been exercised against a real Cloudflare deployment (preview/prod), so the closed-loop CDN behavior is an open item.