Commitf1ffd50unknown_key
perf(edge): ETag middleware + Cache-Control policy for Crontech edge layer
perf(edge): ETag middleware + Cache-Control policy for Crontech edge layer
Two pieces of caching infrastructure that lay the groundwork for
Crontech to act as Gluecron's edge layer (owner's call — keeping
the value capture in-house instead of using Cloudflare).
1. ETAG middleware on every non-streaming response. Hono's built-in
hono/etag. Skipped on git protocol (.git/), live-events SSE,
deploy event firehose, and /admin/status (all streaming). Repeat
visits get 304 Not Modified — ~95% bandwidth saving when nothing
changed.
2. CACHE-CONTROL middleware sets sensible defaults so any edge proxy
in front (Crontech or otherwise) and downstream browsers know what
to cache for how long:
- /admin, /settings, /dashboard, /notifications, /connect/* and
any request with a session cookie: private, no-store
(never cache responses tied to a session)
- Marketing pages (/, /features, /pricing, /about, /vs-github,
/explore, /help, /changelog, /legal/*, /terms, /privacy,
/acceptable-use, /docs/*):
public, max-age=60, s-maxage=300, stale-while-revalidate=86400
(instant render from cache, 5 min at edge, 1 day SWR)
- Public repo browse pages (GET /:owner/:repo and below):
public, max-age=30, s-maxage=120, stale-while-revalidate=600
(Crontech surge-purge on push hook is a follow-up)
- Everything else: private, no-store (opt-in only).
The ETag middleware sets its own 'private, no-cache, must-revalidate'
default which we treat as 'no policy chosen' so our Cache-Control
wins. Routes that set Cache-Control explicitly (package downloads,
sitemaps, etc.) keep theirs.
Verified live: marketing pages get the public,SWR header; admin gets
private,no-store; 304 revalidation path still returns correctly. Full
test suite 2050/2050.1 file changed+86−0f1ffd5088c6580bddf1c6399f7abf80182547401
1 changed file+86−0
Modifiedsrc/app.tsx+86−0View fileUnifiedSplit
@@ -2,6 +2,7 @@ import { Hono } from "hono";
22import { logger } from "hono/logger";
33import { cors } from "hono/cors";
44import { compress } from "hono/compress";
5import { etag } from "hono/etag";
56// BLOCK O2 — shared error-page surface (404 / 500 / 403).
67import { NotFoundPage, ServerErrorPage } from "./views/error-page";
78import { reportError } from "./lib/observability";
@@ -141,6 +142,91 @@ const app = new Hono<AuthEnv>();
141142app.use("*", requestContext);
142143// Middleware — compression first (wraps all responses)
143144app.use("*", compress());
145
146// ETag middleware — returns 304 Not Modified on unchanged responses.
147// Saves ~95% bandwidth on repeat visits. Skipped on git protocol +
148// SSE + the API surface where it would interfere with streaming.
149app.use("*", async (c, next) => {
150 const p = c.req.path;
151 if (
152 p.includes(".git/") ||
153 p.startsWith("/live-events") ||
154 p.startsWith("/api/events/deploy") ||
155 p.startsWith("/admin/status")
156 ) {
157 return next();
158 }
159 return etag()(c, next);
160});
161
162// Cache-Control middleware — sets sensible defaults on public, anonymous
163// requests. Crontech (when wired as our edge layer) and downstream
164// browsers will honor these. Auth'd users get private,no-store
165// automatically — we never cache responses tied to a session.
166app.use("*", async (c, next) => {
167 await next();
168 // Don't overwrite explicit headers set by route handlers — BUT the
169 // ETag middleware unconditionally sets "private, no-cache, must-
170 // revalidate" so we have to treat that specific value as "no
171 // policy chosen yet" and apply ours. Any route that explicitly set
172 // a different cache-control wins.
173 const existing = c.res.headers.get("cache-control");
174 const etagDefault =
175 existing === "private, no-cache, must-revalidate" ||
176 existing === "no-cache";
177 if (existing && !etagDefault) return;
178 // Anything past auth: private + no-store (avoid leaking session
179 // content into shared caches). softAuth runs before this on the
180 // request, but the response side is what we're stamping.
181 const hasSession = c.req.header("cookie")?.includes("session=") ?? false;
182 const p = c.req.path;
183 // Always private for known-authed paths regardless of cookie.
184 if (
185 p.startsWith("/admin") ||
186 p.startsWith("/settings") ||
187 p.startsWith("/dashboard") ||
188 p.startsWith("/notifications") ||
189 p.startsWith("/connect/") ||
190 hasSession
191 ) {
192 c.res.headers.set("cache-control", "private, no-store");
193 return;
194 }
195 // Public marketing surfaces — short edge cache, longer browser cache,
196 // stale-while-revalidate so the user never waits on a stale fetch.
197 const isMarketing =
198 p === "/" ||
199 p === "/features" ||
200 p === "/pricing" ||
201 p === "/about" ||
202 p === "/vs-github" ||
203 p === "/explore" ||
204 p === "/help" ||
205 p === "/changelog" ||
206 p.startsWith("/legal/") ||
207 p === "/terms" ||
208 p === "/privacy" ||
209 p === "/acceptable-use" ||
210 p.startsWith("/docs/");
211 if (isMarketing) {
212 c.res.headers.set(
213 "cache-control",
214 "public, max-age=60, s-maxage=300, stale-while-revalidate=86400"
215 );
216 return;
217 }
218 // Public repo browse pages — cache aggressively. Edge invalidates
219 // on push via the post-receive hook (future Crontech surge purge).
220 if (/^\/[^/]+\/[^/]+(\/.*)?$/.test(p) && c.req.method === "GET") {
221 c.res.headers.set(
222 "cache-control",
223 "public, max-age=30, s-maxage=120, stale-while-revalidate=600"
224 );
225 return;
226 }
227 // Default: don't cache anything we haven't explicitly opted in.
228 c.res.headers.set("cache-control", "private, no-store");
229});
144230// Logger only on non-git routes to avoid overhead on clone/push
145231app.use("*", async (c, next) => {
146232 if (c.req.path.includes(".git/")) return next();
147233