Skip to content

Commit 9f596ec

Browse files
committed
fix: normalize trailing slashes in middleware redirect
Paths like /about/ produced /about/.md instead of /about.md. Strip trailing slashes before inspecting or constructing the redirect URL. Also reorders checks to parse the URL only after confirming the Accept header matches.
1 parent 603393b commit 9f596ec

2 files changed

Lines changed: 43 additions & 6 deletions

File tree

functions/_middleware.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,27 @@ describe("markdown content negotiation middleware", () => {
107107
expect(response.headers.get("Location")).toBe("/contributing.md?ref=footer")
108108
})
109109

110+
it("strips trailing slash before redirecting", async () => {
111+
const context = createContext("https://example.com/contributing/", {
112+
headers: { Accept: "text/markdown" },
113+
})
114+
115+
const response = await onRequest(context)
116+
117+
expect(response.status).toBe(302)
118+
expect(response.headers.get("Location")).toBe("/contributing.md")
119+
})
120+
121+
it("passes through when URL with trailing slash has a file extension", async () => {
122+
const context = createContext("https://example.com/styles/main.css/", {
123+
headers: { Accept: "text/markdown" },
124+
})
125+
126+
await onRequest(context)
127+
128+
expect(context.next).toHaveBeenCalled()
129+
})
130+
110131
it("passes through when no Accept header is present", async () => {
111132
const context = createContext("https://example.com/contributing")
112133

functions/_middleware.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,36 @@ export async function onRequest(context: Context): Promise<Response> {
1010
return context.next()
1111
}
1212

13-
const url = new URL(request.url)
14-
const lastSegment = url.pathname.split("/").pop() ?? ""
15-
if (lastSegment.includes(".")) {
13+
const accept = request.headers.get("Accept") ?? ""
14+
if (!accept.includes("text/markdown")) {
1615
return context.next()
1716
}
1817

19-
const accept = request.headers.get("Accept") ?? ""
20-
if (!accept.includes("text/markdown")) {
18+
const url = new URL(request.url)
19+
const pathname = cutSuffix(url.pathname, "/")
20+
const lastSegment = cutEnd(pathname, "/")
21+
if (lastSegment.includes(".")) {
2122
return context.next()
2223
}
2324

24-
const path = url.pathname === "/" ? "/index" : url.pathname
25+
const path = pathname === "" ? "/index" : pathname
2526
return new Response(null, {
2627
status: 302,
2728
headers: { Location: `${path}.md${url.search}` },
2829
})
2930
}
31+
32+
function cutSuffix(path: string, suffix: string): string {
33+
if (path.endsWith(suffix)) {
34+
return path.slice(0, -suffix.length)
35+
}
36+
return path
37+
}
38+
39+
function cutEnd(str: string, sep: string): string {
40+
const index = str.lastIndexOf(sep)
41+
if (index === -1) {
42+
return str
43+
}
44+
return str.slice(index)
45+
}

0 commit comments

Comments
 (0)