-
-
Notifications
You must be signed in to change notification settings - Fork 0
ref: Simplify proxy handler and remove caching layer #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1dbfb17
7b93ea7
8ea1f43
628e094
981668c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,108 +1,66 @@ | ||
| 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 { 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'; | ||
| 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<string, CacheEntry>(); | ||
| 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; | ||
| } | ||
| if (segments[0] === "skills") { | ||
| 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<string, string> = { | ||
| '/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<Response> { | ||
| 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.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`)); | ||
| 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); | ||
| }); | ||
cursor[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| app.get('*', async (c) => { | ||
| let path = c.req.path; | ||
| if (path.length > 1 && path.endsWith('/')) { | ||
| path = path.slice(0, -1); | ||
| } | ||
|
|
||
cursor[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // Redirect /skills/... to /... (canonical URLs don't include the prefix) | ||
| if (path.startsWith('/skills/')) { | ||
| return c.redirect(path.slice('/skills'.length), 301); | ||
| } | ||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing route for
|
||
| const url = mapPath(path); | ||
| app.get("/:skill/*", (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); | ||
| } | ||
|
|
||
| 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); | ||
| } | ||
|
|
||
| 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}`); | ||
| }); | ||


Uh oh!
There was an error while loading. Please reload this page.