Try to prevent late/duplicate response errors (#20077)
* Ensure all Express 'next' calls include 'return's * Add headersSent checks during archived versions flow * Return a 404 earlier if archived resource fetch fails * Short-circuit responses for archived stuff * Be more careful about responding to and short-circuiting after search requests * Fix tests
This commit is contained in:
@@ -32,6 +32,6 @@ module.exports = async function archivedEnterpriseVersionsAssets (req, res, next
|
|||||||
res.set('cache-control', `public, max-age=${ONE_DAY}`)
|
res.set('cache-control', `public, max-age=${ONE_DAY}`)
|
||||||
return res.send(r.body)
|
return res.send(r.body)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return next()
|
return next(404)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ const versionSatisfiesRange = require('../lib/version-satisfies-range')
|
|||||||
const isArchivedVersion = require('../lib/is-archived-version')
|
const isArchivedVersion = require('../lib/is-archived-version')
|
||||||
const got = require('got')
|
const got = require('got')
|
||||||
const readJsonFile = require('../lib/read-json-file')
|
const readJsonFile = require('../lib/read-json-file')
|
||||||
const archvivedRedirects = readJsonFile('./lib/redirects/static/archived-redirects-from-213-to-217.json')
|
const archivedRedirects = readJsonFile('./lib/redirects/static/archived-redirects-from-213-to-217.json')
|
||||||
const archivedFrontmatterFallbacks = readJsonFile('./lib/redirects/static/archived-frontmatter-fallbacks.json')
|
const archivedFrontmatterFallbacks = readJsonFile('./lib/redirects/static/archived-frontmatter-fallbacks.json')
|
||||||
|
|
||||||
// This module handles requests for deprecated GitHub Enterprise versions
|
// This module handles requests for deprecated GitHub Enterprise versions
|
||||||
@@ -29,54 +29,49 @@ module.exports = async function archivedEnterpriseVersions (req, res, next) {
|
|||||||
// starting with 2.18, we updated the archival script to create a redirects.json file
|
// starting with 2.18, we updated the archival script to create a redirects.json file
|
||||||
if (versionSatisfiesRange(requestedVersion, `>=${firstVersionDeprecatedOnNewSite}`) &&
|
if (versionSatisfiesRange(requestedVersion, `>=${firstVersionDeprecatedOnNewSite}`) &&
|
||||||
versionSatisfiesRange(requestedVersion, `<=${lastVersionWithoutArchivedRedirectsFile}`)) {
|
versionSatisfiesRange(requestedVersion, `<=${lastVersionWithoutArchivedRedirectsFile}`)) {
|
||||||
const redirect = archvivedRedirects[req.path]
|
const redirect = archivedRedirects[req.path]
|
||||||
if (redirect && redirect !== req.path) {
|
if (redirect && redirect !== req.path) {
|
||||||
return res.redirect(301, redirect)
|
return res.redirect(301, redirect)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let reqPath = req.path
|
|
||||||
let isRedirect = false
|
|
||||||
if (versionSatisfiesRange(requestedVersion, `>${lastVersionWithoutArchivedRedirectsFile}`)) {
|
if (versionSatisfiesRange(requestedVersion, `>${lastVersionWithoutArchivedRedirectsFile}`)) {
|
||||||
try {
|
try {
|
||||||
const res = await got(getProxyPath('redirects.json', requestedVersion))
|
const r = await got(getProxyPath('redirects.json', requestedVersion))
|
||||||
const redirectJson = JSON.parse(res.body)
|
const redirectJson = JSON.parse(r.body)
|
||||||
|
|
||||||
|
// make redirects found via redirects.json redirect with a 301
|
||||||
if (redirectJson[req.path]) {
|
if (redirectJson[req.path]) {
|
||||||
isRedirect = true
|
res.set('x-robots-tag', 'noindex')
|
||||||
|
return res.redirect(301, redirectJson[req.path])
|
||||||
}
|
}
|
||||||
reqPath = redirectJson[req.path] || req.path
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// nooop
|
// noop
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const r = await got(getProxyPath(reqPath, requestedVersion))
|
const r = await got(getProxyPath(req.path, requestedVersion))
|
||||||
res.set('content-type', r.headers['content-type'])
|
|
||||||
res.set('x-robots-tag', 'noindex')
|
res.set('x-robots-tag', 'noindex')
|
||||||
|
|
||||||
// make redirects found via redirects.json return 301 instead of 200
|
// make stubbed redirect files (which exist in versions <2.13) redirect with a 301
|
||||||
if (isRedirect) {
|
|
||||||
res.status(301)
|
|
||||||
res.set('location', reqPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// make stubbed redirect files (which exist in versions <2.13) return 301 instead of 200
|
|
||||||
const staticRedirect = r.body.match(patterns.staticRedirect)
|
const staticRedirect = r.body.match(patterns.staticRedirect)
|
||||||
if (staticRedirect) {
|
if (staticRedirect) {
|
||||||
res.status(301)
|
return res.redirect(301, staticRedirect[1])
|
||||||
res.set('location', staticRedirect[1])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
res.set('content-type', r.headers['content-type'])
|
||||||
return res.send(r.body)
|
return res.send(r.body)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
for (const fallbackRedirect of getFallbackRedirects(req, requestedVersion) || []) {
|
for (const fallbackRedirect of getFallbackRedirects(req, requestedVersion) || []) {
|
||||||
try {
|
try {
|
||||||
await got(getProxyPath(fallbackRedirect, requestedVersion))
|
await got(getProxyPath(fallbackRedirect, requestedVersion))
|
||||||
return res.redirect(301, fallbackRedirect)
|
return res.redirect(301, fallbackRedirect)
|
||||||
} catch (err) { } // noop
|
} catch (err) {
|
||||||
|
// noop
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return next()
|
return next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,5 +15,5 @@ module.exports = async function handleNextDataPath (req, res, next) {
|
|||||||
req.pagePath = req.path
|
req.pagePath = req.path
|
||||||
}
|
}
|
||||||
|
|
||||||
next()
|
return next()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -160,6 +160,10 @@ module.exports = function (app) {
|
|||||||
}))
|
}))
|
||||||
app.use('/events', asyncMiddleware(instrument(events, './events')))
|
app.use('/events', asyncMiddleware(instrument(events, './events')))
|
||||||
app.use('/search', asyncMiddleware(instrument(search, './search')))
|
app.use('/search', asyncMiddleware(instrument(search, './search')))
|
||||||
|
|
||||||
|
// Check for a dropped connection before proceeding (again)
|
||||||
|
app.use(haltOnDroppedConnection)
|
||||||
|
|
||||||
app.use(asyncMiddleware(instrument(archivedEnterpriseVersions, './archived-enterprise-versions')))
|
app.use(asyncMiddleware(instrument(archivedEnterpriseVersions, './archived-enterprise-versions')))
|
||||||
app.use(instrument(robots, './robots'))
|
app.use(instrument(robots, './robots'))
|
||||||
app.use(/(\/.*)?\/early-access$/, instrument(earlyAccessLinks, './contextualizers/early-access-links'))
|
app.use(/(\/.*)?\/early-access$/, instrument(earlyAccessLinks, './contextualizers/early-access-links'))
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
const { FEATURE_NEXTJS } = process.env;
|
const { FEATURE_NEXTJS } = process.env
|
||||||
|
|
||||||
module.exports = function isNextRequest (req, res, next) {
|
module.exports = function isNextRequest (req, res, next) {
|
||||||
req.renderWithNextjs = false;
|
req.renderWithNextjs = false
|
||||||
|
|
||||||
if (FEATURE_NEXTJS) {
|
if (FEATURE_NEXTJS) {
|
||||||
req.renderWithNextjs = true;
|
req.renderWithNextjs = true
|
||||||
}
|
}
|
||||||
|
|
||||||
next();
|
return next()
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ function renderPageWithNext (req, res, next) {
|
|||||||
return nextHandleRequest(req, res)
|
return nextHandleRequest(req, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
next()
|
return next()
|
||||||
}
|
}
|
||||||
|
|
||||||
renderPageWithNext.nextHandleRequest = nextHandleRequest
|
renderPageWithNext.nextHandleRequest = nextHandleRequest
|
||||||
|
|||||||
@@ -27,11 +27,18 @@ router.get('/', async function postSearch (req, res, next) {
|
|||||||
const results = process.env.AIRGAP || req.cookies.AIRGAP
|
const results = process.env.AIRGAP || req.cookies.AIRGAP
|
||||||
? await loadLunrResults({ version, language, query: `${query} ${filters || ''}`, limit })
|
? await loadLunrResults({ version, language, query: `${query} ${filters || ''}`, limit })
|
||||||
: await loadAlgoliaResults({ version, language, query, filters, limit })
|
: await loadAlgoliaResults({ version, language, query, filters, limit })
|
||||||
|
|
||||||
|
// Only reply if the headers have not been sent and the request was not aborted...
|
||||||
|
if (!res.headersSent && !req.aborted) {
|
||||||
return res.status(200).json(results)
|
return res.status(200).json(results)
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
|
// Only reply if the headers have not been sent and the request was not aborted...
|
||||||
|
if (!res.headersSent && !req.aborted) {
|
||||||
return res.status(400).json([])
|
return res.status(400).json([])
|
||||||
}
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ describe('redirects', () => {
|
|||||||
test('frontmatter redirect', async () => {
|
test('frontmatter redirect', async () => {
|
||||||
const res = await get('/enterprise/2.12/user/articles/github-flavored-markdown')
|
const res = await get('/enterprise/2.12/user/articles/github-flavored-markdown')
|
||||||
expect(res.statusCode).toBe(301)
|
expect(res.statusCode).toBe(301)
|
||||||
expect(res.text).toContain('location=\'/enterprise/2.12/user/categories/writing-on-github/\'')
|
expect(res.headers.location).toBe('/enterprise/2.12/user/categories/writing-on-github/')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user