Skip to content

Commit ac11a11

Browse files
fix: add AbortController timeout to fetchApi to prevent connection pool exhaustion
Without a timeout, a hung fetch occupies one of the browser's ~6 HTTP/1.1 connection slots indefinitely after long sessions, silently blocking all subsequent requests to the same host (including POST /prompt). - 60 s AbortController timeout on every fetchApi() call - Caller-supplied signals are preserved via AbortSignal.any() - Emits a Sentry breadcrumb and Mixpanel FETCH_TIMEOUT event before aborting so we can confirm the hypothesis in production and verify the fix Fixes #14389
1 parent c814b8e commit ac11a11

3 files changed

Lines changed: 55 additions & 3 deletions

File tree

src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type {
1515
CreditTopupMetadata,
1616
DefaultViewSetMetadata,
1717
EnterLinearMetadata,
18+
FetchTimeoutMetadata,
1819
HelpCenterClosedMetadata,
1920
HelpCenterOpenedMetadata,
2021
HelpResourceClickedMetadata,
@@ -422,4 +423,8 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
422423
trackUiButtonClicked(metadata: UiButtonClickMetadata): void {
423424
this.trackEvent(TelemetryEvents.UI_BUTTON_CLICKED, metadata)
424425
}
426+
427+
trackFetchTimeout(metadata: FetchTimeoutMetadata): void {
428+
this.trackEvent(TelemetryEvents.FETCH_TIMEOUT, metadata)
429+
}
425430
}

src/platform/telemetry/types.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -895,6 +895,13 @@ export function getBillingTelemetryEventPayload(event: BillingTelemetryEvent) {
895895
}
896896
}
897897

898+
export interface FetchTimeoutMetadata {
899+
route: string
900+
method: string
901+
duration_ms: number
902+
timeout_ms: number
903+
}
904+
898905
/**
899906
* Telemetry provider interface for individual providers.
900907
* All methods are optional - providers only implement what they need.
@@ -1022,6 +1029,9 @@ export interface TelemetryProvider {
10221029

10231030
// Page view tracking
10241031
trackPageView?(pageName: string, properties?: PageViewMetadata): void
1032+
1033+
// Network error events
1034+
trackFetchTimeout?(metadata: FetchTimeoutMetadata): void
10251035
}
10261036

10271037
/**
@@ -1166,7 +1176,10 @@ export const TelemetryEvents = {
11661176
NAMED_VALUES_SHADOW_DIFF_SUMMARY: 'app:named_values_shadow_diff_summary',
11671177

11681178
// Page View
1169-
PAGE_VIEW: 'app:page_view'
1179+
PAGE_VIEW: 'app:page_view',
1180+
1181+
// Network
1182+
FETCH_TIMEOUT: 'app:fetch_timeout'
11701183
} as const
11711184

11721185
export type TelemetryEventName =
@@ -1257,3 +1270,4 @@ export type TelemetryEventProperties =
12571270
| SubscriptionSuccessMetadata
12581271
| WorkspaceInviteFailedMetadata
12591272
| BillingTelemetryEvent
1273+
| FetchTimeoutMetadata

src/scripts/api.ts

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import type {
1717
ModelFolderInfo
1818
} from '@/platform/assets/schemas/assetSchema'
1919
import { isCloud } from '@/platform/distribution/types'
20+
import * as Sentry from '@sentry/vue'
21+
import { useTelemetry } from '@/platform/telemetry'
2022
import { useToastStore } from '@/platform/updates/common/toastStore'
2123
import type { ShareableAssetsResponse } from '@/schemas/apiSchema'
2224
import { zShareableAssetsResponse } from '@/schemas/apiSchema'
@@ -498,11 +500,42 @@ export class ComfyApi extends EventTarget {
498500
}
499501

500502
addHeaderEntry(headers, 'Comfy-User', this.user)
503+
504+
// AbortController timeout prevents connection pool exhaustion after long sessions.
505+
// See: https://github.com/Comfy-Org/ComfyUI_frontend/issues/14389
506+
const FETCH_TIMEOUT_MS = 60_000
507+
const controller = new AbortController()
508+
const startTime = Date.now()
509+
const timeoutId = setTimeout(() => {
510+
const duration_ms = Date.now() - startTime
511+
const method = (options?.method ?? 'GET').toUpperCase()
512+
513+
Sentry.addBreadcrumb({
514+
category: 'fetch',
515+
message: `Timeout on ${method} ${route}`,
516+
level: 'warning',
517+
data: { duration_ms, timeout_ms: FETCH_TIMEOUT_MS }
518+
})
519+
520+
useTelemetry()?.trackFetchTimeout?.({
521+
route,
522+
method,
523+
duration_ms,
524+
timeout_ms: FETCH_TIMEOUT_MS
525+
})
526+
527+
controller.abort()
528+
}, FETCH_TIMEOUT_MS)
529+
530+
const signal = options?.signal
531+
? AbortSignal.any([options.signal, controller.signal])
532+
: controller.signal
533+
501534
return fetchWithUnifiedRemint(
502535
this.apiURL(route),
503-
{ cache: 'no-cache', ...options, headers },
536+
{ cache: 'no-cache', ...options, headers, signal },
504537
unifiedRetryOn401
505-
)
538+
).finally(() => clearTimeout(timeoutId))
506539
}
507540

508541
/**

0 commit comments

Comments
 (0)