Skip to content

Commit c2ff4d6

Browse files
authored
Suppress 404 noise from Next.js assets on archived enterprise versions (#56269)
1 parent 96a5914 commit c2ff4d6

File tree

2 files changed

+123
-4
lines changed

2 files changed

+123
-4
lines changed

src/archives/middleware/archived-enterprise-versions-assets.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,22 @@ export default async function archivedEnterpriseVersionsAssets(
4949
const { isArchived, requestedVersion } = isArchivedVersion(req)
5050
if (!isArchived || !requestedVersion) return next()
5151

52+
// If this looks like a Next.js chunk or build manifest request from an archived page,
53+
// just return 204 No Content instead of trying to proxy it.
54+
// This suppresses noise from hydration requests that don't affect
55+
// content viewing since archived pages render fine server-side.
56+
// Only target specific problematic asset types, not all _next/static assets.
57+
if (
58+
(req.path.includes('/_next/static/chunks/') ||
59+
req.path.includes('/_buildManifest.js') ||
60+
req.path.includes('/_ssgManifest.js')) &&
61+
(req.get('referrer') || '').match(/enterprise(-server@|\/)[\d.]+/)
62+
) {
63+
archivedCacheControl(res)
64+
setFastlySurrogateKey(res, SURROGATE_ENUMS.MANUAL)
65+
return res.sendStatus(204) // No Content - silently ignore
66+
}
67+
5268
// In all of the `docs-ghes-<relase number` repos, the asset directories
5369
// are at the root. This removes the version and release number from the
5470
// asset path so that we can proxy the request to the correct location.

src/assets/tests/static-assets.ts

Lines changed: 107 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ type MockResponse = {
3838
statusCode: number
3939
json?: (payload: any) => void
4040
send?: (body: any) => void
41+
sendStatus?: (statusCode: number) => void
42+
end?: () => void
4143
_json?: string
4244
_send?: string
4345
headers: Record<string, string>
@@ -48,8 +50,8 @@ type MockResponse = {
4850

4951
const mockResponse = () => {
5052
const res: MockResponse = {
51-
status: 404,
52-
statusCode: 404,
53+
status: undefined as any,
54+
statusCode: undefined as any,
5355
headers: {},
5456
}
5557
res.json = (payload) => {
@@ -60,6 +62,14 @@ const mockResponse = () => {
6062
res.statusCode = 200
6163
res._send = body
6264
}
65+
res.end = () => {
66+
// Mock end method
67+
}
68+
res.sendStatus = (statusCode) => {
69+
res.status = statusCode
70+
res.statusCode = statusCode
71+
// Mock sendStatus method
72+
}
6373
res.set = (key, value) => {
6474
if (typeof key === 'string') {
6575
res.headers[key.toLowerCase()] = value
@@ -75,6 +85,12 @@ const mockResponse = () => {
7585
res.hasHeader = (key) => {
7686
return key in res.headers
7787
}
88+
// Add Express-style status method that supports chaining
89+
;(res as any).status = (code: number) => {
90+
res.status = code
91+
res.statusCode = code
92+
return res
93+
}
7894
return res
7995
}
8096

@@ -178,7 +194,12 @@ describe('archived enterprise static assets', () => {
178194
})
179195
nock('https://github.github.com')
180196
.get('/docs-ghes-2.3/_next/static/fourofour.css')
181-
.reply(404, 'Not found', {
197+
.reply(404, 'not found', {
198+
'content-type': 'text/plain',
199+
})
200+
nock('https://github.github.com')
201+
.get('/docs-ghes-3.5/assets/images/some-image.png')
202+
.reply(404, 'not found', {
182203
'content-type': 'text/plain',
183204
})
184205
nock('https://github.github.com')
@@ -251,7 +272,6 @@ describe('archived enterprise static assets', () => {
251272
}
252273
setDefaultFastlySurrogateKey(req, res, next)
253274
await archivedEnterpriseVersionsAssets(req as any, res as any, next)
254-
expect(res.statusCode).toBe(404)
255275
// It didn't exit in that middleware but called next() to move on
256276
// with any other middlewares.
257277
expect(nexted).toBe(true)
@@ -274,4 +294,87 @@ describe('archived enterprise static assets', () => {
274294
// tried "our disk" and it's eventually there.
275295
expect(nexted).toBe(true)
276296
})
297+
298+
describe.each([
299+
{
300+
name: 'Next.js chunk assets from archived enterprise referrer (legacy format)',
301+
path: '/_next/static/chunks/9589-81283b60820a85f5.js',
302+
referrer: '/en/enterprise/3.5/authentication/connecting-to-github-with-ssh',
303+
expectStatus: 204,
304+
shouldCallNext: false,
305+
},
306+
{
307+
name: 'Next.js chunk assets from archived enterprise referrer (new format)',
308+
path: '/_next/static/chunks/pages/[versionId]-40812da083876691.js',
309+
referrer: '/en/[email protected]/authentication/connecting-to-github-with-ssh',
310+
expectStatus: 204,
311+
shouldCallNext: false,
312+
},
313+
{
314+
name: 'Next.js build manifest from archived enterprise referrer',
315+
path: '/_next/static/NkhGE2zLVuDHVh7pXdtVC/_buildManifest.js',
316+
referrer: '/[email protected]/admin/configuration',
317+
expectStatus: 204,
318+
shouldCallNext: false,
319+
},
320+
])(
321+
'should return $expectStatus for $name',
322+
({ name, path, referrer, expectStatus, shouldCallNext }) => {
323+
test(name, async () => {
324+
const req = mockRequest(path, {
325+
headers: {
326+
Referrer: referrer,
327+
},
328+
})
329+
const res = mockResponse()
330+
let nexted = false
331+
const next = () => {
332+
if (!shouldCallNext) {
333+
throw new Error('should not call next() for suppressed assets')
334+
}
335+
nexted = true
336+
}
337+
setDefaultFastlySurrogateKey(req, res, () => {})
338+
await archivedEnterpriseVersionsAssets(req as any, res as any, next)
339+
expect(res.statusCode).toBe(expectStatus)
340+
if (shouldCallNext) {
341+
expect(nexted).toBe(true)
342+
}
343+
})
344+
},
345+
)
346+
347+
describe.each([
348+
{
349+
name: 'Next.js assets from non-enterprise referrer',
350+
path: '/_next/static/chunks/main-abc123.js',
351+
referrer: '/en/actions/using-workflows',
352+
expectStatus: undefined,
353+
shouldCallNext: true,
354+
},
355+
{
356+
name: 'non-Next.js assets from archived enterprise referrer',
357+
path: '/assets/images/some-image.png',
358+
referrer: '/en/[email protected]/some/page',
359+
expectStatus: undefined,
360+
shouldCallNext: true,
361+
},
362+
])('should not suppress $name', ({ name, path, referrer, expectStatus, shouldCallNext }) => {
363+
test(name, async () => {
364+
const req = mockRequest(path, {
365+
headers: {
366+
Referrer: referrer,
367+
},
368+
})
369+
const res = mockResponse()
370+
let nexted = false
371+
const next = () => {
372+
nexted = true
373+
}
374+
setDefaultFastlySurrogateKey(req, res, () => {})
375+
await archivedEnterpriseVersionsAssets(req as any, res as any, next)
376+
expect(nexted).toBe(shouldCallNext)
377+
expect(res.statusCode).toBe(expectStatus)
378+
})
379+
})
277380
})

0 commit comments

Comments
 (0)