Skip to content

Commit d0aac8e

Browse files
heiskrCopilotCopilot
authored
🧹 Remove connect-timeout and add per-fetch timeouts (#61086)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent a44c190 commit d0aac8e

10 files changed

Lines changed: 54 additions & 175 deletions

File tree

‎package-lock.json‎

Lines changed: 0 additions & 90 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,6 @@
198198
"cheerio": "^1.2.0",
199199
"classnames": "^2.5.1",
200200
"clsx": "^2.1.1",
201-
"connect-timeout": "1.9.1",
202201
"cookie-parser": "^1.4.7",
203202
"cuss": "2.2.0",
204203
"dayjs": "^1.11.19",
@@ -282,7 +281,6 @@
282281
"@octokit/rest": "22.0.0",
283282
"@playwright/test": "^1.58.2",
284283
"@types/accept-language-parser": "1.5.7",
285-
"@types/connect-timeout": "1.9.0",
286284
"@types/cookie": "0.6.0",
287285
"@types/cookie-parser": "1.4.8",
288286
"@types/eslint-plugin-jsx-a11y": "^6.10.1",

‎src/ai-tools/lib/spaces-utils.ts‎

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
/**
22
* Copilot Space API response types
33
*/
4+
import { fetchWithRetry } from '@/frame/lib/fetch-utils'
5+
46
export interface SpaceResource {
57
id: number
68
resource_type: string
@@ -48,14 +50,28 @@ export function parseSpaceUrl(url: string): { org: string; id: string } {
4850
export async function fetchCopilotSpace(spaceUrl: string): Promise<SpaceData> {
4951
const { org, id } = parseSpaceUrl(spaceUrl)
5052
const apiUrl = `https://api.github.com/orgs/${org}/copilot-spaces/${id}`
53+
const githubToken = process.env.GITHUB_TOKEN
54+
55+
if (!githubToken) {
56+
throw new Error('GITHUB_TOKEN environment variable is required to fetch Copilot Spaces.')
57+
}
5158

52-
const response = await fetch(apiUrl, {
53-
headers: {
54-
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
55-
Accept: 'application/vnd.github+json',
56-
'X-GitHub-Api-Version': '2022-11-28',
59+
const response = await fetchWithRetry(
60+
apiUrl,
61+
{
62+
headers: {
63+
Authorization: `Bearer ${githubToken}`,
64+
Accept: 'application/vnd.github+json',
65+
'X-GitHub-Api-Version': '2022-11-28',
66+
},
67+
},
68+
{
69+
retries: 0,
70+
timeout: 30_000,
71+
// We handle non-2xx responses below with status-specific messages.
72+
throwHttpErrors: false,
5773
},
58-
})
74+
)
5975

6076
if (!response.ok) {
6177
if (response.status === 404) {

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ export default async function archivedEnterpriseVersionsAssets(
9494
{
9595
retries: 0,
9696
throwHttpErrors: true,
97+
// Stay safely under MAX_REQUEST_TIMEOUT (10s) so the upstream
98+
// can't push us past the request budget.
99+
timeout: 8_000,
97100
},
98101
)
99102

‎src/frame/lib/fetch-utils.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
* Utility functions for fetch with retry and timeout functionality
33
* to replace got library functionality
44
*/
5+
import statsd from '@/observability/lib/statsd'
6+
7+
const STATSD_FETCH_TIMEOUT = 'fetch.timeout'
58

69
export interface FetchWithRetryOptions {
710
retries?: number
@@ -52,6 +55,14 @@ async function fetchWithTimeout(
5255
} catch (error) {
5356
clearTimeout(timeoutId)
5457
if (error instanceof Error && error.name === 'AbortError') {
58+
const host = (() => {
59+
try {
60+
return new URL(typeof url === 'string' ? url : url.toString()).host
61+
} catch {
62+
return 'unknown'
63+
}
64+
})()
65+
statsd.increment(STATSD_FETCH_TIMEOUT, 1, [`host:${host}`])
5566
throw new Error(`Request timed out after ${timeout}ms`)
5667
}
5768
throw error

‎src/frame/middleware/halt-on-dropped-connection.ts‎

Lines changed: 0 additions & 21 deletions
This file was deleted.

‎src/frame/middleware/index.ts‎

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@ import path from 'path'
33

44
import express from 'express'
55
import type { NextFunction, Request, Response, Express } from 'express'
6-
import timeout from 'connect-timeout'
76

8-
import { haltOnDroppedConnection } from './halt-on-dropped-connection'
97
import abort from './abort'
108
import helmet from './helmet'
119
import cookieParser from './cookie-parser'
@@ -65,14 +63,10 @@ import dynamicAssets from '@/assets/middleware/dynamic-assets'
6563
import generalSearchMiddleware from '@/search/middleware/general-search-middleware'
6664
import shielding from '@/shielding/middleware'
6765
import safeRedirect from './safe-redirect'
68-
import { MAX_REQUEST_TIMEOUT } from '@/frame/lib/constants'
6966
import { initLoggerContext } from '@/observability/logger/lib/logger-context'
7067
import { getAutomaticRequestLogger } from '@/observability/logger/middleware/get-automatic-request-logger'
7168
import urlDecode from './url-decode'
7269

73-
const { NODE_ENV } = process.env
74-
const isTest = NODE_ENV === 'test' || process.env.GITHUB_ACTIONS === 'true'
75-
7670
const ENABLE_FASTLY_TESTING = JSON.parse(process.env.ENABLE_FASTLY_TESTING || 'false')
7771

7872
// Catch unhandled promise rejections and passing them to Express's error handler
@@ -90,8 +84,6 @@ const asyncMiddleware =
9084
}
9185

9286
export default function index(app: Express) {
93-
// *** Request connection management ***
94-
if (!isTest) app.use(timeout(MAX_REQUEST_TIMEOUT))
9587
app.use(abort)
9688

9789
// Don't use the proxy's IP, use the requester's for rate limiting or
@@ -237,9 +229,6 @@ export default function index(app: Express) {
237229
app.use(asyncMiddleware(findPage)) // Must come before archived-enterprise-versions, breadcrumbs, featured-links, products, render-page
238230
app.use(blockRobots)
239231

240-
// Check for a dropped connection before proceeding
241-
app.use(haltOnDroppedConnection)
242-
243232
// *** Rendering, 2xx responses ***
244233
app.use('/api', api)
245234
app.use('/llms.txt', llmsTxt)
@@ -251,17 +240,11 @@ export default function index(app: Express) {
251240
// Now that the `req.language` is known, set it for the remaining endpoints
252241
app.use(setLanguageFastlySurrogateKey)
253242

254-
// Check for a dropped connection before proceeding (again)
255-
app.use(haltOnDroppedConnection)
256-
257243
app.use(robots)
258244
app.use(earlyAccessLinks)
259245
app.use('/categories.json', asyncMiddleware(categoriesForSupport))
260246
app.get('/_500', asyncMiddleware(triggerError))
261247

262-
// Check for a dropped connection before proceeding (again)
263-
app.use(haltOnDroppedConnection)
264-
265248
// Specifically deal with HEAD requests before doing the slower
266249
// full page rendering.
267250
app.head('/*path', fastHead)
@@ -292,9 +275,6 @@ export default function index(app: Express) {
292275
// handle serving NextJS bundled code (/_next/*)
293276
app.use(next)
294277

295-
// Check for a dropped connection before proceeding (again)
296-
app.use(haltOnDroppedConnection)
297-
298278
// *** Rendering, must go almost last ***
299279
app.get('/*path', asyncMiddleware(renderPage))
300280

‎src/frame/middleware/render-page.ts‎

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import { allVersions } from '@/versions/lib/all-versions'
1313
import { transformerRegistry } from '@/article-api/transformers'
1414
import { minimumNotFoundHtml } from '../lib/constants'
1515
import { contentTypeCacheControl, defaultCacheControl } from './cache-control'
16-
import { isConnectionDropped } from './halt-on-dropped-connection'
1716
import { nextHandleRequest } from './next'
1817

1918
const logger = createLogger(import.meta.url)
@@ -98,9 +97,6 @@ export default async function renderPage(req: ExtendedRequest, res: Response) {
9897
res.setHeader('Last-Modified', new Date(page.effectiveDate).toUTCString())
9998
}
10099

101-
// Stop processing if the connection was already dropped
102-
if (isConnectionDropped(req, res)) return
103-
104100
// Content negotiation: serve markdown when the client prefers it over HTML.
105101
// Agents like Claude Code send Accept headers that omit text/html.
106102
if (req.accepts(['text/html', 'text/markdown']) === 'text/markdown') {
@@ -123,9 +119,6 @@ export default async function renderPage(req: ExtendedRequest, res: Response) {
123119
req.context.miniTocItems = buildMiniTocItems(req)
124120
}
125121

126-
// Stop processing if the connection was already dropped
127-
if (isConnectionDropped(req, res)) return
128-
129122
// Create string for <title> tag
130123
page.fullTitle = page.title
131124

‎src/observability/middleware/handle-errors.ts‎

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { nextApp } from '@/frame/middleware/next'
55
import { minimumNotFoundHtml } from '@/frame/lib/constants'
66
import { setFastlySurrogateKey, SURROGATE_ENUMS } from '@/frame/middleware/set-fastly-surrogate-key'
77
import { errorCacheControl } from '@/frame/middleware/cache-control'
8-
import statsd from '@/observability/lib/statsd'
98
import { toError } from '@/observability/lib/to-error'
109
import { ExtendedRequest } from '@/types'
1110
import { createLogger } from '@/observability/logger'
@@ -43,33 +42,12 @@ async function logException(error: ErrorWithCode, req: ExtendedRequest) {
4342
}
4443
}
4544

46-
function timedOut(req: ExtendedRequest) {
47-
// The `req.pagePath` can come later so it's not guaranteed to always
48-
// be present. It's added by the `handle-next-data-path.ts` middleware
49-
// we translates those "cryptic" `/_next/data/...` URLs from
50-
// client-side routing.
51-
const incrementTags = [`path:${req.pagePath || req.path}`]
52-
if (req.context?.currentCategory) {
53-
incrementTags.push(`product:${req.context.currentCategory}`)
54-
}
55-
statsd.increment('middleware.timeout', 1, incrementTags)
56-
logger.warn('Request timed out', {
57-
path: req.pagePath || req.path,
58-
method: req.method,
59-
})
60-
}
61-
6245
async function handleError(
6346
error: ErrorWithCode | number,
6447
req: ExtendedRequest,
6548
res: Response,
6649
next: NextFunction,
6750
) {
68-
// Potentially set by the `connect-timeout` middleware.
69-
if (req.timedout) {
70-
timedOut(req)
71-
}
72-
7351
const responseDone = res.headersSent || req.aborted
7452

7553
if (req.path.startsWith('/assets') || req.path.startsWith('/_next/static')) {

0 commit comments

Comments
 (0)