From 1dbfb1712e6ea4969ae9ee7a55e80e521d7ef6f3 Mon Sep 17 00:00:00 2001 From: Michael Hoffmann Date: Mon, 9 Mar 2026 14:39:22 +0100 Subject: [PATCH 1/4] ref: Simplify proxy handler and remove caching layer Replace catch-all route with explicit route handlers for each endpoint. Remove in-memory cache and stale-while-error logic in favor of direct proxy pass-through. Add typed Context and ContentfulStatusCode imports. Co-Authored-By: Claude Agent transcript: https://claudescope.sentry.dev/share/4rxSgfqf_8e03QyKF2z9P75vink8hZ2eN_MRVyURJa4 --- src/index.ts | 125 ++++++++++++++++----------------------------------- 1 file changed, 39 insertions(+), 86 deletions(-) diff --git a/src/index.ts b/src/index.ts index d715428..ced64ce 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,108 +1,61 @@ -import { Hono } from 'hono'; -import { serve } from '@hono/node-server'; +import { Hono } from "hono"; +import type { Context } from "hono"; +import { serve } from "@hono/node-server"; +import type { ContentfulStatusCode } from "hono/utils/http-status"; -const BASE = 'https://raw.githubusercontent.com/getsentry/sentry-for-ai/refs/heads/main'; +const BASE = "https://raw.githubusercontent.com/getsentry/sentry-for-ai/refs/heads/main"; -// Cache -interface CacheEntry { - body: string; - status: number; - fetchedAt: number; -} - -const cache = new Map(); +function buildSkillUrl(pathname: string): string | null { + if (!pathname.startsWith("/")) { + return null; + } -function getCache(key: string): CacheEntry | null { - const entry = cache.get(key); - if (!entry) return null; - const ttl = entry.status === 200 ? 5 * 60 * 1000 : 1 * 60 * 1000; - if (Date.now() - entry.fetchedAt > ttl) return null; - return entry; -} + const segments = pathname.slice(1).split("/"); + if (segments.some((segment) => segment.length === 0 || segment === "." || segment === "..")) { + return null; + } -function setCache(key: string, body: string, status: number): void { - cache.set(key, { body, status, fetchedAt: Date.now() }); + return `${BASE}/skills${pathname}`; } -// Category shortcuts — direct entry points for router skills -const CATEGORY_ROUTES: Record = { - '/sdks': '/sentry-sdk-setup/SKILL.md', - '/workflows': '/sentry-workflow/SKILL.md', - '/features': '/sentry-feature-setup/SKILL.md', -}; - -// URL mapper -function mapPath(path: string): string | null { - if (path.includes('..')) return null; - if (path === '/' || path === '/SKILL_TREE.md') { - return `${BASE}/SKILL_TREE.md`; - } - // Category shortcuts resolve to their router skill - const redirect = CATEGORY_ROUTES[path]; - if (redirect) { - return `${BASE}/skills${redirect}`; +async function proxyText(c: Context, url: string): Promise { + try { + const res = await fetch(url, { signal: AbortSignal.timeout(5000) }); + const body = await res.text(); + return c.text(body, res.status as ContentfulStatusCode, { + "Content-Type": "text/plain; charset=utf-8", + }); + } catch { + return c.text("Bad Gateway", 502); } - return `${BASE}/skills${path}`; } // App const app = new Hono(); -app.get('*', async (c) => { - let path = c.req.path; - if (path.length > 1 && path.endsWith('/')) { - path = path.slice(0, -1); - } - - // Redirect /skills/... to /... (canonical URLs don't include the prefix) - if (path.startsWith('/skills/')) { - return c.redirect(path.slice('/skills'.length), 301); - } +app.get("/", (c) => proxyText(c, `${BASE}/SKILL_TREE.md`)); +app.get("/SKILL_TREE.md", (c) => proxyText(c, `${BASE}/SKILL_TREE.md`)); +app.get("/sdks", (c) => proxyText(c, `${BASE}/skills/sentry-sdk-setup/SKILL.md`)); +app.get("/workflows", (c) => proxyText(c, `${BASE}/skills/sentry-workflow/SKILL.md`)); +app.get("/features", (c) => proxyText(c, `${BASE}/skills/sentry-feature-setup/SKILL.md`)); - const url = mapPath(path); +app.get("/:skill/SKILL.md", (c) => { + const url = buildSkillUrl(c.req.path); if (!url) { - return c.text('Bad Request', 400); - } - - const cached = getCache(url); - if (cached) { - return c.text(cached.body, cached.status as any, { - 'Content-Type': 'text/plain; charset=utf-8', - }); + return c.text("Bad Request", 400); } + return proxyText(c, url); +}); - let body: string; - let status: number; - - try { - const res = await fetch(url, { signal: AbortSignal.timeout(5000) }); - body = await res.text(); - status = res.status; - if (status >= 500) { - const stale = cache.get(url); - if (stale) { - return c.text(stale.body, stale.status as any, { - 'Content-Type': 'text/plain; charset=utf-8', - }); - } - } - setCache(url, body, status); - } catch (err) { - const stale = cache.get(url); - if (stale) { - return c.text(stale.body, stale.status as any, { - 'Content-Type': 'text/plain; charset=utf-8', - }); - } - return c.text('Bad Gateway', 502); +app.get("/:skill/*", (c) => { + const url = buildSkillUrl(c.req.path); + if (!url) { + return c.text("Bad Request", 400); } - - return c.text(body, status as any, { - 'Content-Type': 'text/plain; charset=utf-8', - }); + return proxyText(c, url); }); const port = Number(process.env.PORT) || 3000; serve({ fetch: app.fetch, port }, () => { - console.log(`Listening on port ${port}`); + console.log(`Listening on http://localhost:${port}`); }); From 7b93ea74d6a167d52080ef3b15c079821982ca5c Mon Sep 17 00:00:00 2001 From: Michael Hoffmann Date: Mon, 9 Mar 2026 15:05:36 +0100 Subject: [PATCH 2/4] fix redirect handling for /skills-prefixed routes --- src/index.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/index.ts b/src/index.ts index ced64ce..77c2482 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,6 +14,9 @@ function buildSkillUrl(pathname: string): string | null { if (segments.some((segment) => segment.length === 0 || segment === "." || segment === "..")) { return null; } + if (segments[0] === "skills") { + return null; + } return `${BASE}/skills${pathname}`; } @@ -38,6 +41,15 @@ app.get("/SKILL_TREE.md", (c) => proxyText(c, `${BASE}/SKILL_TREE.md`)); app.get("/sdks", (c) => proxyText(c, `${BASE}/skills/sentry-sdk-setup/SKILL.md`)); app.get("/workflows", (c) => proxyText(c, `${BASE}/skills/sentry-workflow/SKILL.md`)); app.get("/features", (c) => proxyText(c, `${BASE}/skills/sentry-feature-setup/SKILL.md`)); +app.get("/skills", (c) => { + const url = new URL(c.req.url); + return c.redirect(`/${url.search}`, 301); +}); +app.get("/skills/*", (c) => { + const url = new URL(c.req.url); + const canonicalPath = c.req.path.slice("/skills".length) || "/"; + return c.redirect(`${canonicalPath}${url.search}`, 301); +}); app.get("/:skill/SKILL.md", (c) => { const url = buildSkillUrl(c.req.path); From 8ea1f4328c8bd0c2828deff912807d211a49a106 Mon Sep 17 00:00:00 2001 From: Michael Hoffmann Date: Mon, 9 Mar 2026 15:08:02 +0100 Subject: [PATCH 3/4] refactor simplify proxy route handling --- src/index.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/index.ts b/src/index.ts index 77c2482..44dc83f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -37,7 +37,6 @@ async function proxyText(c: Context, url: string): Promise { const app = new Hono(); app.get("/", (c) => proxyText(c, `${BASE}/SKILL_TREE.md`)); -app.get("/SKILL_TREE.md", (c) => proxyText(c, `${BASE}/SKILL_TREE.md`)); app.get("/sdks", (c) => proxyText(c, `${BASE}/skills/sentry-sdk-setup/SKILL.md`)); app.get("/workflows", (c) => proxyText(c, `${BASE}/skills/sentry-workflow/SKILL.md`)); app.get("/features", (c) => proxyText(c, `${BASE}/skills/sentry-feature-setup/SKILL.md`)); @@ -51,14 +50,6 @@ app.get("/skills/*", (c) => { return c.redirect(`${canonicalPath}${url.search}`, 301); }); -app.get("/:skill/SKILL.md", (c) => { - const url = buildSkillUrl(c.req.path); - if (!url) { - return c.text("Bad Request", 400); - } - return proxyText(c, url); -}); - app.get("/:skill/*", (c) => { const url = buildSkillUrl(c.req.path); if (!url) { From 628e094dd4b0b645c868f12585606bd0b664d8dd Mon Sep 17 00:00:00 2001 From: Michael Hoffmann Date: Mon, 9 Mar 2026 15:09:34 +0100 Subject: [PATCH 4/4] feat normalize trailing slashes --- src/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/index.ts b/src/index.ts index 44dc83f..77b29f2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ import { Hono } from "hono"; import type { Context } from "hono"; import { serve } from "@hono/node-server"; +import { trimTrailingSlash } from "hono/trailing-slash"; import type { ContentfulStatusCode } from "hono/utils/http-status"; const BASE = "https://raw.githubusercontent.com/getsentry/sentry-for-ai/refs/heads/main"; @@ -35,6 +36,7 @@ async function proxyText(c: Context, url: string): Promise { // App const app = new Hono(); +app.use(trimTrailingSlash({ alwaysRedirect: true })); app.get("/", (c) => proxyText(c, `${BASE}/SKILL_TREE.md`)); app.get("/sdks", (c) => proxyText(c, `${BASE}/skills/sentry-sdk-setup/SKILL.md`));