Skip to content

Commit 5fc846c

Browse files
author
Peter Bengtsson
authored
Plain text 404 page for non language urls (github#37752)
1 parent d7709e8 commit 5fc846c

File tree

8 files changed

+50
-16
lines changed

8 files changed

+50
-16
lines changed

middleware/find-page.js

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@ import { existsSync } from 'fs'
33

44
import { ROOT } from '../lib/constants.js'
55
import Page from '../lib/page.js'
6-
import { languageKeys } from '../lib/languages.js'
6+
import { languagePrefixPathRegex } from '../lib/languages.js'
77

8-
const languagePrefixRegex = new RegExp(`^/(${languageKeys.join('|')})(/|$)`)
98
const englishPrefixRegex = /^\/en(\/|$)/
109
const CONTENT_ROOT = path.join(ROOT, 'content')
1110

@@ -18,7 +17,7 @@ export default async function findPage(
1817
{ isDev = process.env.NODE_ENV === 'development', contentRoot = CONTENT_ROOT } = {}
1918
) {
2019
// Filter out things like `/will/redirect` or `/_next/data/...`
21-
if (!languagePrefixRegex.test(req.pagePath)) {
20+
if (!languagePrefixPathRegex.test(req.pagePath)) {
2221
return next()
2322
}
2423

@@ -58,8 +57,8 @@ export default async function findPage(
5857
}
5958

6059
async function rereadByPath(uri, contentRoot, currentVersion) {
61-
const languageCode = uri.match(languagePrefixRegex)[1]
62-
const withoutLanguage = uri.replace(languagePrefixRegex, '/')
60+
const languageCode = uri.match(languagePrefixPathRegex)[1]
61+
const withoutLanguage = uri.replace(languagePrefixPathRegex, '/')
6362
const withoutVersion = withoutLanguage.replace(`/${currentVersion}`, '')
6463
// TODO: Support loading translations the same way.
6564
// NOTE: No one is going to test translations like this in development

middleware/next.js

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,6 @@ export const nextHandleRequest = nextApp.getRequestHandler()
88
await nextApp.prepare()
99

1010
function renderPageWithNext(req, res, next) {
11-
// We currently don't use next/image for any images.
12-
// We don't even have `sharp` installed.
13-
// This could change in the future but right now can just 404 on these
14-
// so we don't have to deal with any other errors.
15-
if (req.path.startsWith('/_next/image')) {
16-
return next(404)
17-
}
18-
1911
const isNextDataRequest = req.path.startsWith('/_next') && !req.path.startsWith('/_next/data')
2012

2113
if (isNextDataRequest) {

middleware/render-page.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import FailBot from '#src/observability/lib/failbot.js'
44
import patterns from '../lib/patterns.js'
55
import getMiniTocItems from '../lib/get-mini-toc-items.js'
66
import Page from '../lib/page.js'
7+
import { pathLanguagePrefixed } from '../lib/languages.js'
78
import statsd from '#src/observability/lib/statsd.js'
89
import { allVersions } from '../lib/all-versions.js'
910
import { isConnectionDropped } from './halt-on-dropped-connection.js'
@@ -55,6 +56,14 @@ export default async function renderPage(req, res) {
5556
)
5657
}
5758

59+
if (!pathLanguagePrefixed(req.path)) {
60+
defaultCacheControl(res)
61+
return res.status(404).type('text').send('Not found')
62+
}
63+
64+
// The rest is "unhandled" requests where we don't have the page
65+
// but the URL looks like a real page.
66+
5867
statsd.increment(STATSD_KEY_404, 1, [
5968
`url:${req.url}`,
6069
`ip:${req.ip}`,

src/shielding/middleware/handle-invalid-paths.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ function isJunkPath(path) {
3131
return true
3232
}
3333

34+
// We currently don't use next/image for any images.
35+
// This could change in the future but right now can just 404 on these
36+
// so we don't have to deal with any other errors.
37+
if (path.startsWith('/_next/image')) {
38+
return true
39+
}
40+
3441
return false
3542
}
3643

src/shielding/tests/shielding.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,26 @@ describe('rate limiting', () => {
7474
expect(res.headers['ratelimit-remaining']).toBeUndefined()
7575
})
7676
})
77+
78+
describe('404 pages and their content-type', () => {
79+
const exampleNonLanguage404s = [
80+
'/_next/image/foo',
81+
'/wp-content/themes/seotheme/db.php?u',
82+
'/enterprise/3.1/_next/static/chunks/616-910d0397bafa52e0.js',
83+
'/~root',
84+
]
85+
test.each(exampleNonLanguage404s)(
86+
'non-language 404 response is plain text and cacheable: %s',
87+
async (pathname) => {
88+
const res = await get(pathname)
89+
expect(res.statusCode).toBe(404)
90+
expect(res.headers['content-type']).toMatch('text/plain')
91+
expect(res.headers['cache-control']).toMatch('public')
92+
}
93+
)
94+
test('valid language prefix 404 response is HTML', async () => {
95+
const res = await get('/en/something-that-doesnt-existent')
96+
expect(res.statusCode).toBe(404)
97+
expect(res.headers['content-type']).toMatch('text/html')
98+
})
99+
})

tests/rendering-fixtures/footer.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ describe('footer', () => {
1515
})
1616

1717
test('leads to support on 404 pages', async () => {
18-
const $ = await getDOM('/delicious-snacks/donuts.php', { allow404: true })
18+
// Important to use the prefix /en/ on the failing URL or else
19+
// it will render a very basic plain text 404 response.
20+
const $ = await getDOM('/en/delicious-snacks/donuts.php', { allow404: true })
1921
expect($('a#support').attr('href')).toBe('https://support.github.com')
2022
})
2123
})

tests/rendering/server.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,9 @@ describe('server', () => {
156156
})
157157

158158
test('renders a 404 page', async () => {
159-
const $ = await getDOM('/not-a-real-page', { allow404: true })
159+
// Important to use the prefix /en/ on the failing URL or else
160+
// it will render a very basic plain text 404 response.
161+
const $ = await getDOM('/en/not-a-real-page', { allow404: true })
160162
expect($('h1').first().text()).toBe('Ooops!')
161163
expect($.text().includes("It looks like this page doesn't exist.")).toBe(true)
162164
expect(

tests/routing/next.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ describe('bad requests', () => {
88
test('any _next/image request should 404', async () => {
99
const res = await get('/_next/image?what=ever')
1010
expect(res.statusCode).toBe(404)
11-
expect(res.headers['content-type']).toMatch('text/html')
11+
expect(res.headers['content-type']).toMatch('text/plain')
1212
})
1313

1414
test('any _next.* request should 404', async () => {

0 commit comments

Comments
 (0)