Keep query strings when redirecting URLs with double slashes (#64098) · github/docs@b3dbab9 · GitHub
Skip to content

Commit b3dbab9

Browse files
heiskrCopilot
andauthored
Keep query strings when redirecting URLs with double slashes (#64098)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: bf5940de-658e-4a1a-bffa-3018fb78b138
1 parent 30a9496 commit b3dbab9

5 files changed

Lines changed: 126 additions & 4 deletions

File tree

‎src/frame/middleware/trailing-slashes.ts‎

Lines changed: 4 additions & 3 deletions
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { describe, expect, test } from 'vitest'
2+
import type { Response } from 'express'
3+
4+
import trailingSlashes from '@/frame/middleware/trailing-slashes'
5+
import { safeRedirectUrl } from '@/frame/middleware/safe-redirect'
6+
import type { ExtendedRequest } from '@/types'
7+
8+
function run(url: string) {
9+
const req = { method: 'GET', url } as ExtendedRequest
10+
const headers: Record<string, string> = {}
11+
const result: { status?: number; location?: string; nextCalled: boolean } = {
12+
nextCalled: false,
13+
}
14+
const res = {
15+
set(key: string, value: string) {
16+
headers[key.toLowerCase()] = value
17+
return this
18+
},
19+
hasHeader(key: string) {
20+
return key.toLowerCase() in headers
21+
},
22+
safeRedirect(status: number, location: string) {
23+
result.status = status
24+
result.location = safeRedirectUrl(location)
25+
},
26+
} as unknown as Response
27+
trailingSlashes(req, res, () => {
28+
result.nextCalled = true
29+
})
30+
return { ...result, headers }
31+
}
32+
33+
describe('trailingSlashes middleware', () => {
34+
test('redirects a path of only slashes to the homepage', () => {
35+
const result = run('//')
36+
expect(result.status).toBe(301)
37+
expect(result.location).toBe('/')
38+
expect(result.headers['cache-control']).toMatch(/max-age=\d+/)
39+
})
40+
41+
test('keeps the query string when redirecting a path of only slashes', () => {
42+
const result = run('//?foo=1')
43+
expect(result.status).toBe(301)
44+
expect(result.location).toBe('/?foo=1')
45+
})
46+
47+
test('keeps double slashes inside query values', () => {
48+
expect(run('//?next=https://github.com').location).toBe('/?next=https://github.com')
49+
expect(run('/en//search/?query=a//b').location).toBe('/en/search?query=a//b')
50+
})
51+
52+
test('removes a trailing slash and keeps the query string', () => {
53+
const result = run('/foo/?a=1')
54+
expect(result.status).toBe(301)
55+
expect(result.location).toBe('/foo?a=1')
56+
})
57+
58+
test('removes trailing double slashes', () => {
59+
const result = run('/en//')
60+
expect(result.status).toBe(301)
61+
expect(result.location).toBe('/en')
62+
})
63+
64+
test('calls next for the homepage', () => {
65+
const result = run('/')
66+
expect(result.nextCalled).toBe(true)
67+
expect(result.status).toBeUndefined()
68+
})
69+
})

‎src/redirects/middleware/handle-redirects.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,11 @@ export default function handleRedirects(req: ExtendedRequest, res: Response, nex
2727

2828
// Collapse duplicate slashes before patterns.assetPaths, so //example.com cannot bypass handling.
2929
if (req.path.includes('//')) {
30-
return res.safeRedirect(301, req.path.replace(/\/+/g, '/'))
30+
// Take only the query from req.url, because an absolute-form req.url also holds the scheme and host.
31+
const queryIndex = req.url.indexOf('?')
32+
const query = queryIndex === -1 ? '' : req.url.slice(queryIndex)
33+
defaultCacheControl(res)
34+
return res.safeRedirect(301, req.path.replace(/\/+/g, '/') + query)
3135
}
3236

3337
if (patterns.assetPaths.test(req.path)) return next()

‎src/redirects/tests/redirects.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,13 @@ describe('redirects', () => {
456456
expect(res.headers.location).toBe(`/en/rest`)
457457
})
458458

459+
test('double-slash in the URL keeps the query string', async () => {
460+
const res = await get(`/en//search?query=git`)
461+
expect(res.statusCode).toBe(301)
462+
expect(res.headers.location).toBe(`/en/search?query=git`)
463+
expect(res.headers['cache-control']).toMatch(/max-age=\d+/)
464+
})
465+
459466
test('double-slash trailing in the URL', async () => {
460467
const res = await get(`/en//`)
461468
expect(res.statusCode).toBe(301)

‎src/redirects/tests/unit/handle-redirects.ts‎

Lines changed: 41 additions & 0 deletions

0 commit comments

Comments
 (0)