1
0
mirror of synced 2025-12-19 18:10:59 -05:00

Upgrade Express from v4 to v5 (#56026)

This commit is contained in:
Kevin Heis
2025-06-11 13:34:44 -07:00
committed by GitHub
parent 2bb890014c
commit c86a000041
17 changed files with 506 additions and 310 deletions

706
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -269,7 +269,7 @@
"dereference-json-schema": "^0.2.1", "dereference-json-schema": "^0.2.1",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"escape-string-regexp": "5.0.0", "escape-string-regexp": "5.0.0",
"express": "4.21.2", "express": "^5.1.0",
"fastest-levenshtein": "1.0.16", "fastest-levenshtein": "1.0.16",
"file-type": "21.0.0", "file-type": "21.0.0",
"flat": "^6.0.1", "flat": "^6.0.1",
@@ -349,7 +349,7 @@
"@types/cookie": "0.6.0", "@types/cookie": "0.6.0",
"@types/cookie-parser": "1.4.8", "@types/cookie-parser": "1.4.8",
"@types/event-to-promise": "^0.7.5", "@types/event-to-promise": "^0.7.5",
"@types/express": "4.17.21", "@types/express": "^5.0.3",
"@types/imurmurhash": "^0.1.4", "@types/imurmurhash": "^0.1.4",
"@types/js-cookie": "^3.0.6", "@types/js-cookie": "^3.0.6",
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",

View File

@@ -65,10 +65,15 @@ router.get('/cookies', (req, res) => {
const cookies = { const cookies = {
isStaff: Boolean(req.cookies?.staffonly?.startsWith('yes')) || false, isStaff: Boolean(req.cookies?.staffonly?.startsWith('yes')) || false,
} }
return res.json(cookies) res.json(cookies)
}) })
router.get('*', (req, res) => { // Handle root /api requests
router.get('/', (req, res) => {
res.status(404).json({ error: `${req.path} not found` })
})
router.get('/*path', (req, res) => {
res.status(404).json({ error: `${req.path} not found` }) res.status(404).json({ error: `${req.path} not found` })
}) })

View File

@@ -8,7 +8,8 @@ export default function buildInfo(req: Request, res: Response) {
res.type('text/plain') res.type('text/plain')
noCacheControl(res) noCacheControl(res)
if (!BUILD_SHA) { if (!BUILD_SHA) {
return res.status(404).send('Not known') res.status(404).send('Not known')
return
} }
return res.send(`${BUILD_SHA}`) res.send(`${BUILD_SHA}`)
} }

View File

@@ -12,7 +12,8 @@ export default function fastHead(req: ExtendedRequest, res: Response, next: Next
// this and allow the CDN to hold on to it. // this and allow the CDN to hold on to it.
defaultCacheControl(res) defaultCacheControl(res)
return res.status(200).send('') res.status(200).send('')
return
} }
next() next()
} }

View File

@@ -12,7 +12,7 @@ import crypto from 'crypto'
const router = express.Router() const router = express.Router()
router.get('/*', function (req, res) { router.get('/*path', function (req, res) {
// If X-CacheTest-Error is set, simulate the site being down (regardless of URL) // If X-CacheTest-Error is set, simulate the site being down (regardless of URL)
if (req.get('X-CacheTest-Error')) { if (req.get('X-CacheTest-Error')) {
res.status(parseInt(req.get('X-CacheTest-Error') as string)).end() res.status(parseInt(req.get('X-CacheTest-Error') as string)).end()

View File

@@ -250,7 +250,7 @@ export default function (app: Express) {
// Specifically deal with HEAD requests before doing the slower // Specifically deal with HEAD requests before doing the slower
// full page rendering. // full page rendering.
app.head('/*', fastHead) app.head('/*path', fastHead)
// *** Preparation for render-page: contextualizers *** // *** Preparation for render-page: contextualizers ***
app.use(asyncMiddleware(secretScanning)) app.use(asyncMiddleware(secretScanning))
@@ -282,7 +282,7 @@ export default function (app: Express) {
app.use(haltOnDroppedConnection) app.use(haltOnDroppedConnection)
// *** Rendering, must go almost last *** // *** Rendering, must go almost last ***
app.get('/*', asyncMiddleware(renderPage)) app.get('/*path', asyncMiddleware(renderPage))
// *** Error handling, must go last *** // *** Error handling, must go last ***
app.use(handleErrors) app.use(handleErrors)

View File

@@ -58,7 +58,8 @@ export default function mockVaPortal(req: ExtendedRequest, res: Response, next:
if (req.url.startsWith('/iframe/docs_va')) { if (req.url.startsWith('/iframe/docs_va')) {
res.removeHeader('content-security-policy') res.removeHeader('content-security-policy')
return res.status(200).type('text/html').send(HTML) res.status(200).type('text/html').send(HTML)
return
} }
next() next()

View File

@@ -108,7 +108,7 @@ export default async function renderPage(req: ExtendedRequest, res: Response) {
// It's important to not use `src/pages/404.txt` (or `/404` as the path) // It's important to not use `src/pages/404.txt` (or `/404` as the path)
// here because then it will set the wrong Cache-Control header. // here because then it will set the wrong Cache-Control header.
tempReq.url = '/_notfound' tempReq.url = '/_notfound'
tempReq.path = '/_notfound' Object.defineProperty(tempReq, 'path', { value: '/_notfound', writable: true })
tempReq.cookies = {} tempReq.cookies = {}
tempReq.headers = {} tempReq.headers = {}
// By default, since the lookup for a `src/pages/*.tsx` file will work, // By default, since the lookup for a `src/pages/*.tsx` file will work,

View File

@@ -23,8 +23,9 @@ export default function robots(req: ExtendedRequest, res: Response, next: NextFu
req.hostname === 'docs.github.com' || req.hostname === 'docs.github.com' ||
req.hostname === '127.0.0.1' req.hostname === '127.0.0.1'
) { ) {
return res.send(defaultResponse) res.send(defaultResponse)
return
} }
return res.send(disallowAll) res.send(disallowAll)
} }

View File

@@ -1,4 +1,4 @@
import type { NextFunction, Response } from 'express' import type { NextFunction, Response, ErrorRequestHandler } from 'express'
import FailBot from '../lib/failbot.js' import FailBot from '../lib/failbot.js'
import { nextApp } from '@/frame/middleware/next.js' import { nextApp } from '@/frame/middleware/next.js'
@@ -53,7 +53,7 @@ function timedOut(req: ExtendedRequest) {
statsd.increment('middleware.timeout', 1, incrementTags) statsd.increment('middleware.timeout', 1, incrementTags)
} }
export default async function handleError( const handleError: ErrorRequestHandler = async function handleError(
error: ErrorWithCode | number, error: ErrorWithCode | number,
req: ExtendedRequest, req: ExtendedRequest,
res: Response, res: Response,
@@ -93,7 +93,8 @@ export default async function handleError(
} }
// We MUST delegate to the default Express error handler // We MUST delegate to the default Express error handler
return next(error) next(error)
return
} }
if (!req.context) { if (!req.context) {
@@ -103,7 +104,8 @@ export default async function handleError(
// Special handling for when a middleware calls `next(404)` // Special handling for when a middleware calls `next(404)`
if (error === 404) { if (error === 404) {
// Note that if this fails, it will swallow that error. // Note that if this fails, it will swallow that error.
return nextApp.render404(req, res) nextApp.render404(req, res)
return
} }
if (typeof error === 'number') { if (typeof error === 'number') {
throw new Error("Don't use next(xxx) where xxx is any other number than 404") throw new Error("Don't use next(xxx) where xxx is any other number than 404")
@@ -117,7 +119,8 @@ export default async function handleError(
// If the error contains a status code, just send that back. This is usually // If the error contains a status code, just send that back. This is usually
// from a middleware like `express.json()`. // from a middleware like `express.json()`.
if (error.statusCode) { if (error.statusCode) {
return res.sendStatus(error.statusCode) res.sendStatus(error.statusCode)
return
} }
res.statusCode = 500 res.statusCode = 500
@@ -129,7 +132,8 @@ export default async function handleError(
// it's easier to just see the full stack trace in the console // it's easier to just see the full stack trace in the console
// and in the client. // and in the client.
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
return next(error) next(error)
return
} else { } else {
nextApp.renderError(error, req, res, req.path) nextApp.renderError(error, req, res, req.path)
@@ -138,6 +142,9 @@ export default async function handleError(
} }
} catch (error) { } catch (error) {
console.error('An error occurred in the error handling middleware!', error) console.error('An error occurred in the error handling middleware!', error)
return next(error) next(error)
return
} }
} }
export default handleError

View File

@@ -22,7 +22,8 @@ export default function handleInvalidNextPaths(
// The CDN will not cache if the status code is not a success // The CDN will not cache if the status code is not a success
// and not a 404. // and not a 404.
return res.status(400).type('text').send('Invalid request headers') res.status(400).type('text').send('Invalid request headers')
return
} }
return next() return next()

View File

@@ -26,7 +26,8 @@ export default function handleInvalidNextPaths(
const tags = [`path:${req.path}`] const tags = [`path:${req.path}`]
statsd.increment(STATSD_KEY, 1, tags) statsd.increment(STATSD_KEY, 1, tags)
return res.status(404).type('text').send('Not found') res.status(404).type('text').send('Not found')
return
} }
return next() return next()

View File

@@ -80,7 +80,8 @@ export default function handleInvalidPaths(
// they're not going to suddenly work in the next deployment. // they're not going to suddenly work in the next deployment.
defaultCacheControl(res) defaultCacheControl(res)
res.setHeader('content-type', 'text/plain') res.setHeader('content-type', 'text/plain')
return res.status(404).send('Not found') res.status(404).send('Not found')
return
} }
if (req.path.endsWith('/index.md')) { if (req.path.endsWith('/index.md')) {
@@ -98,7 +99,8 @@ export default function handleInvalidPaths(
.replace(/%2F/g, '/') .replace(/%2F/g, '/')
.replace(/%40/g, '@') .replace(/%40/g, '@')
const newUrl = `/api/article/body?pathname=${encodedPath}` const newUrl = `/api/article/body?pathname=${encodedPath}`
return res.redirect(newUrl) res.redirect(newUrl)
return
} }
return next() return next()
} }

View File

@@ -55,6 +55,30 @@ export default function handleInvalidQuerystrings(
const { method, query, path } = req const { method, query, path } = req
if (method === 'GET' || method === 'HEAD') { if (method === 'GET' || method === 'HEAD') {
const originalKeys = Object.keys(query) const originalKeys = Object.keys(query)
// Check for invalid query string patterns (square brackets, etc.)
const invalidKeys = originalKeys.filter((key) => {
// Check for square brackets which are invalid
return key.includes('[') || key.includes(']')
})
if (invalidKeys.length > 0) {
noCacheControl(res)
const invalidKey = invalidKeys[0].replace(/\[.*$/, '') // Get the base key name
res.status(400).send(`Invalid query string key (${invalidKey})`)
const tags = [
'response:400',
'reason:invalid-brackets',
`url:${req.url}`,
`path:${req.path}`,
`keys:${originalKeys.length}`,
]
statsd.increment(STATSD_KEY, 1, tags)
return
}
let keys = originalKeys.filter((key) => !RECOGNIZED_KEYS_BY_ANY.has(key)) let keys = originalKeys.filter((key) => !RECOGNIZED_KEYS_BY_ANY.has(key))
if (keys.length > 0) { if (keys.length > 0) {
// Before we judge the number of query strings, strip out all the ones // Before we judge the number of query strings, strip out all the ones

View File

@@ -36,7 +36,8 @@ export default function handleOldNextDataPaths(
const requestBuildId = req.path.split('/')[3] const requestBuildId = req.path.split('/')[3]
if (requestBuildId !== getCurrentBuildID()) { if (requestBuildId !== getCurrentBuildID()) {
errorCacheControl(res) errorCacheControl(res)
return res.status(404).type('text').send('build ID mismatch') res.status(404).type('text').send('build ID mismatch')
return
} }
} }
return next() return next()

View File

@@ -12,10 +12,12 @@ const router = express.Router()
// /api/webhooks/v1?category=check_run&version=free-pro-team%40latest // /api/webhooks/v1?category=check_run&version=free-pro-team%40latest
router.get('/v1', async function webhooks(req, res) { router.get('/v1', async function webhooks(req, res) {
if (!req.query.category) { if (!req.query.category) {
return res.status(400).json({ error: "Missing 'category' in query string" }) res.status(400).json({ error: "Missing 'category' in query string" })
return
} }
if (!req.query.version) { if (!req.query.version) {
return res.status(400).json({ error: "Missing 'version' in query string" }) res.status(400).json({ error: "Missing 'version' in query string" })
return
} }
const webhookVersion = Object.values(allVersions).find( const webhookVersion = Object.values(allVersions).find(
@@ -24,7 +26,8 @@ router.get('/v1', async function webhooks(req, res) {
const notFoundError = 'No webhook found for given category and version' const notFoundError = 'No webhook found for given category and version'
if (!webhookVersion) { if (!webhookVersion) {
return res.status(404).json({ error: notFoundError }) res.status(404).json({ error: notFoundError })
return
} }
const webhook = await getWebhook(webhookVersion, req.query.category as string) const webhook = await getWebhook(webhookVersion, req.query.category as string)
@@ -33,7 +36,7 @@ router.get('/v1', async function webhooks(req, res) {
if (process.env.NODE_ENV !== 'development') { if (process.env.NODE_ENV !== 'development') {
defaultCacheControl(res) defaultCacheControl(res)
} }
return res.status(200).send(webhook) res.status(200).send(webhook)
} else { } else {
res.status(404).json({ error: notFoundError }) res.status(404).json({ error: notFoundError })
} }