From a633b79aa5a0f9109237e7be2cdb010f8be092ad Mon Sep 17 00:00:00 2001 From: Cameron Pfiffer Date: Thu, 27 Aug 2026 17:49:39 -0700 Subject: [PATCH] Preserve HTTPS across slash redirects. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use relative canonical locations so the Fly reverse proxy cannot leak its internal HTTP scheme into public redirects. 👾 Generated with [Letta Code](https://letta.com) Co-Authored-By: Letta Code --- src/index.tsx | 4 ++-- src/knowledge.test.ts | 12 ++++++++---- src/trailing-slash.ts | 13 +++++++++++++ 3 files changed, 23 insertions(+), 6 deletions(-) create mode 100644 src/trailing-slash.ts diff --git a/src/index.tsx b/src/index.tsx index 04e3067..9f42d14 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,7 +1,6 @@ import "./env.ts"; import { Hono } from "hono"; import { renderToReadableStream } from "hono/jsx/streaming"; -import { trimTrailingSlash } from "hono/trailing-slash"; import { serveStatic } from "@hono/node-server/serve-static"; import { serve } from "@hono/node-server"; import { Site } from "./components/site.tsx"; @@ -41,10 +40,11 @@ import { knowledgeRoute, loadKnowledgeGraph, } from "./knowledge.ts"; +import { canonicalizeTrailingSlash } from "./trailing-slash.ts"; const app = new Hono(); -app.use(trimTrailingSlash({ alwaysRedirect: true })); +app.use(canonicalizeTrailingSlash); // --- Static files --- app.use( diff --git a/src/knowledge.test.ts b/src/knowledge.test.ts index afb91ae..dfc4ff4 100644 --- a/src/knowledge.test.ts +++ b/src/knowledge.test.ts @@ -5,7 +5,6 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import test from "node:test"; import { Hono } from "hono"; -import { trimTrailingSlash } from "hono/trailing-slash"; import { KnowledgeVideo } from "./components/knowledge-entry.tsx"; import { knowledgeContentDigest, @@ -14,6 +13,7 @@ import { loadKnowledgeGraph, parseKnowledgeDocument, } from "./knowledge.ts"; +import { canonicalizeTrailingSlash } from "./trailing-slash.ts"; function draftSource(slug: string, route?: string): string { return `--- @@ -180,17 +180,21 @@ test("captures multiple knowledge path segments in Hono", async () => { test("redirects trailing slashes to canonical Knowledge URLs", async () => { const app = new Hono(); - app.use(trimTrailingSlash({ alwaysRedirect: true })); + app.use(canonicalizeTrailingSlash); app.get("/knowledge", (context) => context.text("knowledge")); app.get("/knowledge/:route{.+}", (context) => context.text(context.req.param("route"))); const landing = await app.request("https://cameron.stream/knowledge/?view=map"); assert.equal(landing.status, 301); - assert.equal(landing.headers.get("location"), "https://cameron.stream/knowledge?view=map"); + assert.equal(landing.headers.get("location"), "/knowledge?view=map"); const entry = await app.request("https://cameron.stream/knowledge/agent-memory/"); assert.equal(entry.status, 301); - assert.equal(entry.headers.get("location"), "https://cameron.stream/knowledge/agent-memory"); + assert.equal(entry.headers.get("location"), "/knowledge/agent-memory"); + + const head = await app.request("https://cameron.stream/knowledge/", { method: "HEAD" }); + assert.equal(head.status, 301); + assert.equal(head.headers.get("location"), "/knowledge"); }); test("indexes canonical nested routes and rejects route collisions", async () => { diff --git a/src/trailing-slash.ts b/src/trailing-slash.ts new file mode 100644 index 0000000..89614d9 --- /dev/null +++ b/src/trailing-slash.ts @@ -0,0 +1,13 @@ +import type { MiddlewareHandler } from "hono"; + +export const canonicalizeTrailingSlash: MiddlewareHandler = async (context, next) => { + const path = context.req.path; + const method = context.req.method; + + if ((method === "GET" || method === "HEAD") && path !== "/" && path.endsWith("/")) { + const { search } = new URL(context.req.url); + return context.redirect(`${path.slice(0, -1)}${search}`, 301); + } + + await next(); +}; -- 2.51.2