MCPcopy Index your code
hub / github.com/codeaashu/claude-code / wrapFetchWithTimeout

Function wrapFetchWithTimeout

src/services/mcp/client.ts:492–550  ·  view source on GitHub ↗
(baseFetch: FetchLike)

Source from the content-addressed store, hash-verified

490 * @param baseFetch - The fetch function to wrap
491 */
492export function wrapFetchWithTimeout(baseFetch: FetchLike): FetchLike {
493 return async (url: string | URL, init?: RequestInit) => {
494 const method = (init?.method ?? 'GET').toUpperCase()
495
496 // Skip timeout for GET requests - in MCP transports, these are long-lived SSE streams.
497 // (OAuth discovery GETs in auth.ts use a separate createAuthFetch() with its own timeout.)
498 if (method === 'GET') {
499 return baseFetch(url, init)
500 }
501
502 // Normalize headers and guarantee the Streamable-HTTP Accept value. new Headers()
503 // accepts HeadersInit | undefined and copies from plain objects, tuple arrays,
504 // and existing Headers instances — so whatever shape the SDK handed us, the
505 // Accept value survives the spread below as an own property of a concrete object.
506 // eslint-disable-next-line eslint-plugin-n/no-unsupported-features/node-builtins
507 const headers = new Headers(init?.headers)
508 if (!headers.has('accept')) {
509 headers.set('accept', MCP_STREAMABLE_HTTP_ACCEPT)
510 }
511
512 // Use setTimeout instead of AbortSignal.timeout() so we can clearTimeout on
513 // completion. AbortSignal.timeout's internal timer is only released when the
514 // signal is GC'd, which in Bun is lazy — ~2.4KB of native memory per request
515 // lingers for the full 60s even when the request completes in milliseconds.
516 const controller = new AbortController()
517 const timer = setTimeout(
518 c =>
519 c.abort(new DOMException('The operation timed out.', 'TimeoutError')),
520 MCP_REQUEST_TIMEOUT_MS,
521 controller,
522 )
523 timer.unref?.()
524
525 const parentSignal = init?.signal
526 const abort = () => controller.abort(parentSignal?.reason)
527 parentSignal?.addEventListener('abort', abort)
528 if (parentSignal?.aborted) {
529 controller.abort(parentSignal.reason)
530 }
531
532 const cleanup = () => {
533 clearTimeout(timer)
534 parentSignal?.removeEventListener('abort', abort)
535 }
536
537 try {
538 const response = await baseFetch(url, {
539 ...init,
540 headers,
541 signal: controller.signal,
542 })
543 cleanup()
544 return response
545 } catch (error) {
546 cleanup()
547 throw error
548 }
549 }

Callers 1

client.tsFile · 0.85

Calls 3

cleanupFunction · 0.70
hasMethod · 0.45
setMethod · 0.45

Tested by

no test coverage detected