From a84c7814de2605d0e2f3e9a167b33b489460ffbe Mon Sep 17 00:00:00 2001 From: Mantra <87142457+mantrakp04@users.noreply.github.com> Date: Wed, 20 May 2026 13:15:13 -0700 Subject: [PATCH] Use Accept header for skills HTML/markdown negotiation (#1454) ## Summary Follow-up to #1452. `Sec-Fetch-Mode` / `Sec-Fetch-Dest` didn't reliably split HTML vs. markdown at the CDN edge, so curl could still get the HTML landing page. Switch to the `Accept` header: - Browsers send `Accept: text/html,...` on top-level navigations. - `curl`, `fetch()`, and agent fetchers send `*/*` or omit `Accept`. - Serve HTML only when `text/html` is explicitly listed; everything else gets `SKILL.md`. - `Vary` updated to `Accept` to match. ## Test plan - [ ] Deploy preview - [ ] `curl -sSL https://skill.stack-auth.com/ | head -3` returns markdown frontmatter - [ ] Browser load of `https://skill.stack-auth.com/` still shows the HTML landing page - [ ] Purge Vercel cache if stale variants persist ## Summary by CodeRabbit * **Bug Fixes** * Improved content format negotiation for skill resources to correctly serve HTML or markdown based on client requests. * **Chores** * Optimized caching behavior for edge and CDN services to enhance global content distribution efficiency. [![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/hexclave/stack-auth/pull/1454?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) --- apps/skills/src/app/route.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/apps/skills/src/app/route.ts b/apps/skills/src/app/route.ts index 0ae26a2a6..6da8fff40 100644 --- a/apps/skills/src/app/route.ts +++ b/apps/skills/src/app/route.ts @@ -211,7 +211,7 @@ For the full, current flag list and any commands added after this skill was gene const COMMON_HEADERS = { "Cache-Control": "public, max-age=3600, s-maxage=3600", // CDN must cache markdown (curl/agents) and HTML (browser navigate) separately. - "Vary": "Sec-Fetch-Mode, Sec-Fetch-Dest", + "Vary": "Accept", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, HEAD, OPTIONS", "Access-Control-Allow-Headers": "*", @@ -431,12 +431,18 @@ function renderHtml(): string { `; } +const MARKDOWN_PREFERRING_TYPES = new Set(["*/*", "text/plain", "text/markdown", "text/x-markdown"]); + function wantsHtml(req: Request): boolean { - // Browsers navigating to a top-level URL send Sec-Fetch-Mode: navigate. - // curl, fetch(), and agent fetchers do not, so they keep getting markdown. - if (req.headers.get("sec-fetch-mode") === "navigate") return true; - if (req.headers.get("sec-fetch-dest") === "document") return true; - return false; + // Browsers send `Accept: text/html,...` before `*/*`; curl/fetch/agents send + // `*/*` (or omit Accept). Serve HTML only when text/html appears AND is + // listed before any markdown-preferring type that would otherwise win. + const accept = req.headers.get("accept") ?? ""; + const types = accept.split(",").map((part) => part.trim().split(";")[0].trim().toLowerCase()); + const htmlIndex = types.indexOf("text/html"); + if (htmlIndex === -1) return false; + const competitorIndex = types.findIndex((t) => MARKDOWN_PREFERRING_TYPES.has(t)); + return competitorIndex === -1 || htmlIndex < competitorIndex; } export function GET(req: Request) {