From cbbe07195c00a490556d0f57b7637bda9aaf1d36 Mon Sep 17 00:00:00 2001 From: mantrakp04 Date: Wed, 20 May 2026 12:47:45 -0700 Subject: [PATCH 1/2] Use Accept header for skills HTML/markdown negotiation Sec-Fetch-Mode/Sec-Fetch-Dest weren't reliable for distinguishing browsers from curl/agents on the CDN edge. Switch to the Accept header: browsers send `text/html,...` while curl/fetch/agents send `*/*` or omit it, so we serve HTML only when text/html is explicitly listed. Vary header updated to match. --- apps/skills/src/app/route.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/apps/skills/src/app/route.ts b/apps/skills/src/app/route.ts index 0ae26a2a60..c9c25c5bd4 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": "*", @@ -432,11 +432,10 @@ function renderHtml(): string { } 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 is explicitly listed. + const accept = req.headers.get("accept") ?? ""; + return accept.split(",").some((part) => part.trim().split(";")[0] === "text/html"); } export function GET(req: Request) { From 5f6509e18a37af60e24a1bbe457af944d583fa23 Mon Sep 17 00:00:00 2001 From: mantrakp04 Date: Wed, 20 May 2026 13:13:23 -0700 Subject: [PATCH 2/2] Harden Accept parsing in skills wantsHtml - Trim + lowercase each media-type token so `text/html ;q=0.9` and mixed casing still match. - Require text/html to appear before */*, text/plain, text/markdown, or text/x-markdown so a client that prefers markdown still gets it. --- apps/skills/src/app/route.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/skills/src/app/route.ts b/apps/skills/src/app/route.ts index c9c25c5bd4..6da8fff408 100644 --- a/apps/skills/src/app/route.ts +++ b/apps/skills/src/app/route.ts @@ -431,11 +431,18 @@ function renderHtml(): string { `; } +const MARKDOWN_PREFERRING_TYPES = new Set(["*/*", "text/plain", "text/markdown", "text/x-markdown"]); + function wantsHtml(req: Request): boolean { // Browsers send `Accept: text/html,...` before `*/*`; curl/fetch/agents send - // `*/*` (or omit Accept). Serve HTML only when text/html is explicitly listed. + // `*/*` (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") ?? ""; - return accept.split(",").some((part) => part.trim().split(";")[0] === "text/html"); + 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) {