Skip to content

Commit fb65978

Browse files
authored
Merge branch 'main' into ruby-guide-patch
2 parents 1689a07 + b0379c5 commit fb65978

File tree

4 files changed

+154
-20
lines changed

4 files changed

+154
-20
lines changed

middleware/archived-enterprise-versions-assets.js

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,37 @@ const cacheControl = cacheControlFactory(60 * 60 * 24)
1515
// See also ./archived-enterprise-versions.js for non-CSS/JS paths
1616

1717
export default async function archivedEnterpriseVersionsAssets(req, res, next) {
18-
const { isArchived, requestedVersion } = isArchivedVersion(req)
19-
if (!isArchived) return next()
20-
2118
// Only match asset paths
19+
// This can be true on /enterprise/2.22/_next/static/foo.css
20+
// or /_next/static/foo.css
2221
if (!patterns.assetPaths.test(req.path)) return next()
2322

23+
// We now know the URL is either /enterprise/2.22/_next/static/foo.css
24+
// or the regular /_next/static/foo.css. But we're only going to
25+
// bother looking it up on https://github.github.com/help-docs-archived-enterprise-versions
26+
// if the URL has the enterprise bit in it, or if the path was
27+
// /_next/static/foo.css *and* its Referrer had the enterprise
28+
// bit in it.
29+
if (
30+
!(
31+
patterns.getEnterpriseVersionNumber.test(req.path) ||
32+
patterns.getEnterpriseServerNumber.test(req.path) ||
33+
patterns.getEnterpriseVersionNumber.test(req.get('referrer')) ||
34+
patterns.getEnterpriseServerNumber.test(req.get('referrer'))
35+
)
36+
) {
37+
return next()
38+
}
39+
40+
// Now we know the URL is definitely not /_next/static/foo.css
41+
// So it's probably /enterprise/2.22/_next/static/foo.css and we
42+
// should see if we might find this in the proxied backend.
43+
// But `isArchivedVersion()` will only return truthy if the
44+
// Referrer header also indicates that the request for this static
45+
// asset came from a page
46+
const { isArchived, requestedVersion } = isArchivedVersion(req)
47+
if (!isArchived) return next()
48+
2449
const assetPath = req.path.replace(`/enterprise/${requestedVersion}`, '')
2550
const proxyPath = path.join('/', requestedVersion, assetPath)
2651

@@ -37,6 +62,28 @@ export default async function archivedEnterpriseVersionsAssets(req, res, next) {
3762
cacheControl(res)
3863
return res.send(r.body)
3964
} catch (err) {
40-
return next(404)
65+
// Primarly for the developers working on tests that mock
66+
// requests. If you don't set up `nock` correctly, you might
67+
// not realize that and think it failed for other reasons.
68+
if (err.toString().includes('Nock: No match for request')) {
69+
throw err
70+
}
71+
72+
// It's important that we don't give up on this by returning a 404
73+
// here. It's better to let this through in case the asset exists
74+
// beyond the realm of archived enterprise versions.
75+
// For example, image you load
76+
// /[email protected]/en/DOES/NOT/EXIST in your browser.
77+
// Quickly, we discover that the proxying is failing because
78+
// it didn't find a page called `/en/DOES/NOT/EXIST` over there.
79+
// So, we proceed to render *our* 404 HTML page.
80+
// Now, on that 404 page, it will reference static assets too.
81+
// E.g. <link href="/_next/static/styles.css">
82+
// These will thus be requested, with a Referrer header that
83+
// forces us to give it a chance, but it'll find it can't find it
84+
// but we mustn't return a 404 yet, because that
85+
// /_next/static/styles.css will probably still succeed because the 404
86+
// page is not that of the archived enterprise version.
87+
return next()
4188
}
4289
}

middleware/handle-errors.js

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -41,21 +41,9 @@ export default async function handleError(error, req, res, next) {
4141
// By default, Fastly will cache 404 responses unless otherwise
4242
// told not to.
4343
// See https://docs.fastly.com/en/guides/how-caching-and-cdns-work#http-status-codes-cached-by-default
44-
// Most of the time, that's a good thing! Especially, if bombarded
45-
// for some static asset that we don't have.
46-
// E.g. `ab -n 10000 https://docs.github.com/assets/doesnotexist.png`
47-
// But due to potential timing issue related to how the servers start,
48-
// what might happen is that a new insteance comes up that
49-
// contains `<script src="/_next/static/foo.1234.css">` in the HTML.
50-
// The browser then proceeds to request /_next/static/foo.1234.css
51-
// but this time it could be unluckily routed to a different instance
52-
// that hasn't yet been upgraded, so they get a 404. And the CDN will
53-
// notice this and cache it.
54-
// Setting a tiny cache gets us a good compromise. It protects us
55-
// against most stamping herds on 404s (thank you CDN!) but it also
56-
// clears itself if you get that unlucky timing issue with rolling
57-
// instances in a new deployment.
58-
// For more background see issue 1553.
44+
// Let's cache our 404'ing assets conservatively.
45+
// The Cache-Control is short, and let's use the default surrogate
46+
// key just in case it was a mistake.
5947
cacheControl(res)
6048
// Undo the cookie setting that CSRF sets.
6149
res.removeHeader('set-cookie')

tests/rendering/events.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ describe('POST /events', () => {
3131
delete process.env.HYDRO_SECRET
3232
delete process.env.HYDRO_ENDPOINT
3333
csrfToken = ''
34+
35+
nock.cleanAll()
3436
})
3537

3638
async function checkEvent(data, code) {

tests/rendering/static-assets.js

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import fs from 'fs'
22
import path from 'path'
33

4-
import { expect } from '@jest/globals'
4+
import nock from 'nock'
5+
import { expect, jest } from '@jest/globals'
56

67
import { SURROGATE_ENUMS } from '../../middleware/set-fastly-surrogate-key.js'
78
import { get } from '../helpers/supertest.js'
@@ -69,3 +70,99 @@ describe('static assets', () => {
6970
checkCachingHeaders(res, true, 60)
7071
})
7172
})
73+
74+
describe('archived enterprise static assets', () => {
75+
// Sometimes static assets are proxied. The URL for the static asset
76+
// might not indicate it's based on archived enterprise version.
77+
78+
jest.setTimeout(60 * 1000)
79+
80+
beforeAll(async () => {
81+
// The first page load takes a long time so let's get it out of the way in
82+
// advance to call out that problem specifically rather than misleadingly
83+
// attributing it to the first test
84+
// await get('/')
85+
86+
const sampleCSS = '/* nice CSS */'
87+
88+
nock('https://github.github.com')
89+
.get('/help-docs-archived-enterprise-versions/2.21/_next/static/foo.css')
90+
.reply(200, sampleCSS, {
91+
'content-type': 'text/css',
92+
'content-length': sampleCSS.length,
93+
})
94+
nock('https://github.github.com')
95+
.get('/help-docs-archived-enterprise-versions/2.21/_next/static/only-on-proxy.css')
96+
.reply(200, sampleCSS, {
97+
'content-type': 'text/css',
98+
'content-length': sampleCSS.length,
99+
})
100+
nock('https://github.github.com')
101+
.get('/help-docs-archived-enterprise-versions/2.3/_next/static/only-on-2.3.css')
102+
.reply(200, sampleCSS, {
103+
'content-type': 'text/css',
104+
'content-length': sampleCSS.length,
105+
})
106+
nock('https://github.github.com')
107+
.get('/help-docs-archived-enterprise-versions/2.3/_next/static/fourofour.css')
108+
.reply(404, 'Not found', {
109+
'content-type': 'text/plain',
110+
})
111+
nock('https://github.github.com')
112+
.get('/help-docs-archived-enterprise-versions/2.3/assets/images/site/logo.png')
113+
.reply(404, 'Not found', {
114+
'content-type': 'text/plain',
115+
})
116+
})
117+
118+
afterAll(() => nock.cleanAll())
119+
120+
it('should proxy if the static asset is prefixed', async () => {
121+
const res = await get('/enterprise/2.21/_next/static/foo.css', {
122+
headers: {
123+
Referrer: '/enterprise/2.21',
124+
},
125+
})
126+
expect(res.statusCode).toBe(200)
127+
checkCachingHeaders(res, true, 60)
128+
})
129+
it('should proxy if the Referrer header indicates so', async () => {
130+
const res = await get('/_next/static/only-on-proxy.css', {
131+
headers: {
132+
Referrer: '/enterprise/2.21',
133+
},
134+
})
135+
expect(res.statusCode).toBe(200)
136+
checkCachingHeaders(res, true, 60)
137+
})
138+
it('should proxy if the Referrer header indicates so', async () => {
139+
const res = await get('/_next/static/only-on-2.3.css', {
140+
headers: {
141+
Referrer: '/en/[email protected]/some/page',
142+
},
143+
})
144+
expect(res.statusCode).toBe(200)
145+
checkCachingHeaders(res, true, 60)
146+
})
147+
it('might still 404 even with the right referrer', async () => {
148+
const res = await get('/_next/static/fourofour.css', {
149+
headers: {
150+
Referrer: '/en/[email protected]/some/page',
151+
},
152+
})
153+
expect(res.statusCode).toBe(404)
154+
checkCachingHeaders(res, true, 60)
155+
})
156+
157+
it('404 on the proxy but actually present here', async () => {
158+
const res = await get('/assets/images/site/logo.png', {
159+
headers: {
160+
Referrer: '/en/[email protected]/some/page',
161+
},
162+
})
163+
// It tried to go via the proxy, but it wasn't there, but then it
164+
// tried "our disk" and it's eventually there.
165+
expect(res.statusCode).toBe(200)
166+
checkCachingHeaders(res, true, 60)
167+
})
168+
})

0 commit comments

Comments
 (0)