Skip to content

Commit b7d6ee0

Browse files
authored
Cache language redirects (github#33028)
1 parent 8ee9c11 commit b7d6ee0

9 files changed

+66
-53
lines changed

lib/languages.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
// see also languages-schema.js
1+
// See also languages-schema.js
2+
// Nota bene: If you are adding a new language,
3+
// change accept-language handling in CDN config as well.
4+
25
import dotenv from 'dotenv'
36

47
import { TRANSLATIONS_ROOT } from './constants.js'

middleware/archived-enterprise-versions.js

+7-9
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import isArchivedVersion from '../lib/is-archived-version.js'
1313
import { setFastlySurrogateKey, SURROGATE_ENUMS } from './set-fastly-surrogate-key.js'
1414
import got from 'got'
1515
import { readCompressedJsonFileFallbackLazily } from '../lib/read-json-file.js'
16-
import { archivedCacheControl, noCacheControl } from './cache-control.js'
16+
import { archivedCacheControl, languageCacheControl } from './cache-control.js'
1717
import { pathLanguagePrefixed, languagePrefixPathRegex } from '../lib/languages.js'
1818
import getRedirect, { splitPathByLanguage } from '../lib/get-redirect.js'
1919

@@ -100,11 +100,10 @@ export default async function archivedEnterpriseVersions(req, res, next) {
100100
if (deprecatedWithFunctionalRedirects.includes(requestedVersion)) {
101101
const redirectTo = getRedirect(req.path, req.context)
102102
if (redirectTo) {
103-
if (redirectCode === 301) {
104-
archivedCacheControl(res)
105-
} else {
106-
noCacheControl(res)
103+
if (redirectCode === 302) {
104+
languageCacheControl(res) // call first to get `vary`
107105
}
106+
archivedCacheControl(res) // call second to extend duration
108107
return res.redirect(redirectCode, redirectTo)
109108
}
110109

@@ -120,11 +119,10 @@ export default async function archivedEnterpriseVersions(req, res, next) {
120119
const [language, withoutLanguage] = splitPathByLanguage(req.path, req.context.userLanguage)
121120
const newRedirectTo = redirectJson[withoutLanguage]
122121
if (newRedirectTo) {
123-
if (redirectCode === 301) {
124-
archivedCacheControl(res)
125-
} else {
126-
noCacheControl(res)
122+
if (redirectCode === 302) {
123+
languageCacheControl(res) // call first to get `vary`
127124
}
125+
archivedCacheControl(res) // call second to extend duration
128126
return res.redirect(redirectCode, `/${language}${newRedirectTo}`)
129127
}
130128
}

middleware/cache-control.js

+9
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,12 @@ export const noCacheControl = cacheControlFactory(0)
5555

5656
// Long caching for archived pages and assets
5757
export const archivedCacheControl = cacheControlFactory(60 * 60 * 24 * 365)
58+
59+
// Vary on language when needed
60+
// x-user-language is a custom request header derived from req.cookie:user_language
61+
// accept-language is truncated to one of our available languages
62+
// https://bit.ly/3u5UeRN
63+
export function languageCacheControl(res) {
64+
defaultCacheControl(res)
65+
res.set('vary', 'accept-language, x-user-language')
66+
}

middleware/fast-root-redirect.js

-21
This file was deleted.

middleware/index.js

-2
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ import favicons from './favicons.js'
5656
import setStaticAssetCaching from './static-asset-caching.js'
5757
import fastHead from './fast-head.js'
5858
import fastlyCacheTest from './fastly-cache-test.js'
59-
import fastRootRedirect from './fast-root-redirect.js'
6059
import trailingSlashes from './trailing-slashes.js'
6160
import fastlyBehavior from './fastly-behavior.js'
6261

@@ -189,7 +188,6 @@ export default function (app) {
189188
}
190189

191190
// *** Early exits ***
192-
app.get('/', fastRootRedirect)
193191
app.use(instrument(handleInvalidPaths, './handle-invalid-paths'))
194192
app.use(instrument(handleNextDataPath, './handle-next-data-path'))
195193

middleware/redirects/handle-redirects.js

+4-7
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,7 @@ import { URL } from 'url'
33
import { pathLanguagePrefixed } from '../../lib/languages.js'
44
import { deprecatedWithFunctionalRedirects } from '../../lib/enterprise-server-releases.js'
55
import getRedirect from '../../lib/get-redirect.js'
6-
import { cacheControlFactory } from '../cache-control.js'
7-
8-
const cacheControl = cacheControlFactory(60 * 60 * 24) // one day
9-
const noCacheControl = cacheControlFactory(0)
6+
import { defaultCacheControl, languageCacheControl } from '../cache-control.js'
107

118
export default function handleRedirects(req, res, next) {
129
// never redirect assets
@@ -20,7 +17,7 @@ export default function handleRedirects(req, res, next) {
2017
// blanket redirects for languageless homepage
2118
if (req.path === '/') {
2219
const language = getLanguage(req)
23-
noCacheControl(res)
20+
languageCacheControl(res)
2421
return res.redirect(302, `/${language}`)
2522
}
2623

@@ -154,9 +151,9 @@ export default function handleRedirects(req, res, next) {
154151

155152
// do the redirect if the from-URL already had a language in it
156153
if (pathLanguagePrefixed(req.path) || redirect.includes('://')) {
157-
cacheControl(res)
154+
defaultCacheControl(res)
158155
} else {
159-
noCacheControl(res)
156+
languageCacheControl(res)
160157
}
161158

162159
const permanent = redirect.includes('://') || usePermanentRedirect(req)

tests/rendering/server.js

+25-7
Original file line numberDiff line numberDiff line change
@@ -609,8 +609,11 @@ describe('server', () => {
609609
const res = await get('/articles/deleting-a-team', { followRedirects: false })
610610
expect(res.statusCode).toBe(302)
611611
expect(res.headers['set-cookie']).toBeUndefined()
612-
// no cache control because a language prefix had to be injected
613-
expect(res.headers['cache-control']).toBe('private, no-store')
612+
// language specific caching
613+
expect(res.headers['cache-control']).toContain('public')
614+
expect(res.headers['cache-control']).toMatch(/max-age=[1-9]/)
615+
expect(res.headers.vary).toContain('accept-language')
616+
expect(res.headers.vary).toContain('x-user-language')
614617
})
615618

616619
test('redirects old articles to their slugified URL', async () => {
@@ -625,8 +628,12 @@ describe('server', () => {
625628
const res = await get('/')
626629
expect(res.statusCode).toBe(302)
627630
expect(res.headers.location).toBe('/en')
628-
expect(res.headers['cache-control']).toBe('private, no-store')
629631
expect(res.headers['set-cookie']).toBeUndefined()
632+
// language specific caching
633+
expect(res.headers['cache-control']).toContain('public')
634+
expect(res.headers['cache-control']).toMatch(/max-age=[1-9]/)
635+
expect(res.headers.vary).toContain('accept-language')
636+
expect(res.headers.vary).toContain('x-user-language')
630637
})
631638

632639
// This test exists because in a previous life, our NextJS used to
@@ -644,8 +651,12 @@ describe('server', () => {
644651

645652
expect(res.statusCode).toBe(302)
646653
expect(res.headers.location).toBe('/en')
647-
expect(res.headers['cache-control']).toBe('private, no-store')
648654
expect(res.headers['set-cookie']).toBeUndefined()
655+
// language specific caching
656+
expect(res.headers['cache-control']).toContain('public')
657+
expect(res.headers['cache-control']).toMatch(/max-age=[1-9]/)
658+
expect(res.headers.vary).toContain('accept-language')
659+
expect(res.headers.vary).toContain('x-user-language')
649660
})
650661

651662
test('redirects / to /en when unsupported language preference is specified', async () => {
@@ -658,17 +669,24 @@ describe('server', () => {
658669
})
659670
expect(res.statusCode).toBe(302)
660671
expect(res.headers.location).toBe('/en')
661-
expect(res.headers['cache-control']).toBe('private, no-store')
662672
expect(res.headers['set-cookie']).toBeUndefined()
673+
// language specific caching
674+
expect(res.headers['cache-control']).toContain('public')
675+
expect(res.headers['cache-control']).toMatch(/max-age=[1-9]/)
676+
expect(res.headers.vary).toContain('accept-language')
677+
expect(res.headers.vary).toContain('x-user-language')
663678
})
664679

665680
test('adds English prefix to old article URLs', async () => {
666681
const res = await get('/articles/deleting-a-team')
667682
expect(res.statusCode).toBe(302)
668683
expect(res.headers.location.startsWith('/en/')).toBe(true)
669684
expect(res.headers['set-cookie']).toBeUndefined()
670-
// no cache control because a language prefix had to be injected
671-
expect(res.headers['cache-control']).toBe('private, no-store')
685+
// language specific caching
686+
expect(res.headers['cache-control']).toContain('public')
687+
expect(res.headers['cache-control']).toMatch(/max-age=[1-9]/)
688+
expect(res.headers.vary).toContain('accept-language')
689+
expect(res.headers.vary).toContain('x-user-language')
672690
})
673691

674692
test('redirects that not only injects /en/ should have cache-control', async () => {

tests/routing/deprecated-enterprise-versions.js

+11-4
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,11 @@ describe('recently deprecated redirects', () => {
9595
expect(res.statusCode).toBe(302)
9696
expect(res.headers.location).toBe('/en/[email protected]')
9797
expect(res.headers['set-cookie']).toBeUndefined()
98-
// Deliberately no cache control because it is user-dependent
99-
expect(res.headers['cache-control']).toBe('private, no-store')
98+
// language specific caching
99+
expect(res.headers['cache-control']).toContain('public')
100+
expect(res.headers['cache-control']).toMatch(/max-age=[1-9]/)
101+
expect(res.headers.vary).toContain('accept-language')
102+
expect(res.headers.vary).toContain('x-user-language')
100103
})
101104

102105
test('already languaged enterprise 3.0 redirects', async () => {
@@ -108,14 +111,18 @@ describe('recently deprecated redirects', () => {
108111
expect(res.headers['cache-control']).toContain('public')
109112
expect(res.headers['cache-control']).toMatch(/max-age=[1-9]/)
110113
})
114+
111115
test('redirects enterprise-server 3.0 with actual redirect without language', async () => {
112116
const res = await get(
113117
'/[email protected]/github/getting-started-with-github/githubs-products'
114118
)
115119
expect(res.statusCode).toBe(302)
116120
expect(res.headers['set-cookie']).toBeUndefined()
117-
// Deliberately no cache control because it is user-dependent
118-
expect(res.headers['cache-control']).toBe('private, no-store')
121+
// language specific caching
122+
expect(res.headers['cache-control']).toContain('public')
123+
expect(res.headers['cache-control']).toMatch(/max-age=[1-9]/)
124+
expect(res.headers.vary).toContain('accept-language')
125+
expect(res.headers.vary).toContain('x-user-language')
119126
// This is based on
120127
// https://github.com/github/help-docs-archived-enterprise-versions/blob/master/3.0/redirects.json
121128
expect(res.headers.location).toBe(

tests/routing/redirects.js

+6-2
Original file line numberDiff line numberDiff line change
@@ -126,10 +126,14 @@ describe('redirects', () => {
126126
const res = await get('/')
127127
expect(res.statusCode).toBe(302)
128128
expect(res.headers.location).toBe('/en')
129-
expect(res.headers['cache-control']).toBe('private, no-store')
129+
// language specific caching
130+
expect(res.headers['cache-control']).toContain('public')
131+
expect(res.headers['cache-control']).toMatch(/max-age=\d+/)
132+
expect(res.headers.vary).toContain('accept-language')
133+
expect(res.headers.vary).toContain('x-user-language')
130134
})
131135

132-
test('trailing slash on languaged homepage should permantently redirect', async () => {
136+
test('trailing slash on languaged homepage should permanently redirect', async () => {
133137
const res = await get('/en/')
134138
expect(res.statusCode).toBe(301)
135139
expect(res.headers.location).toBe('/en')

0 commit comments

Comments
 (0)