MCPcopy Create free account
hub / github.com/Noumena-Network/code / wrapFetchWithTimeout

Function wrapFetchWithTimeout

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

Source from the content-addressed store, hash-verified

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

Callers 1

client.tsFile · 0.85

Calls 3

setMethod · 0.80
cleanupFunction · 0.70
hasMethod · 0.45

Tested by

no test coverage detected