Skip to content

Conversation

@decobot
Copy link
Contributor

@decobot decobot commented Jan 20, 2026

Summary

This pull request implements a solution for #1034.

Issue Reference

#1034

Implementation

Automated implementation by Ralph Bot using Claude Code.


This PR was created automatically by Ralph.

Summary by CodeRabbit

  • New Features
    • Added automatic timeout protection for requests and fetch operations with configurable limits
    • Improved request cancellation and resource cleanup handling

✏️ Tip: You can customize this high-level summary in your review settings.

Automated implementation by Ralph Bot.
This commit addresses the requirements specified in #1034.
@github-actions
Copy link
Contributor

Tagging Options

Should a new tag be published when this PR is merged?

  • 👍 for Patch 1.133.7 update
  • 🎉 for Minor 1.134.0 update
  • 🚀 for Major 2.0.0 update

@coderabbitai
Copy link

coderabbitai bot commented Jan 20, 2026

📝 Walkthrough

Walkthrough

The PR introduces dual-level timeout mechanisms: a request-wide timeout in the runtime preparation layer and a per-fetch timeout in the patched fetch utility. Both mechanisms leverage AbortController and AbortSignal.any to combine timeout signals with existing abort signals, ensuring coordinated cancellation across the request lifecycle and individual fetch operations.

Changes

Cohort / File(s) Summary
Request-wide Timeout Mechanism
runtime/mod.ts
Adds AbortController-based timeout (configurable via REQUEST_TIMEOUT_MS, default 30s) at request entry point. Combines timeout signal with incoming request signal and propagates merged signal through RequestContext to loaders. Clears timeout on successful request completion.
Fetch-level Timeout Mechanism
utils/patched_fetch.ts
Patches global fetch to apply per-request timeout (configurable via FETCH_TIMEOUT_MS, default 30s). Uses AbortSignal.any to merge fetch-specific timeout with provided or inherited RequestContext signal. Cleans up timeout timer after fetch settlement.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

  • deco-cx/deco#1034: Request- and fetch-level timeout mechanisms introduced in this PR directly address loader connection timeout requirements.

Poem

🐰 A timeout for this, a timeout for that,
No endless waits when we put on a hat!
Thirty seconds to fetch, thirty more for the request,
The rabbit's precise timing puts timing to test! ⏱️✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title references a specific issue (#1034) about connection timeouts and includes a Portuguese URL, but mixes languages and lacks clarity about the actual technical solution implemented (request-wide and fetch timeouts).
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
utils/patched_fetch.ts (1)

22-52: Sanitize FETCH_TIMEOUT_MS and clear timers on early abort.

Line 22 can parse to NaN, which makes timeouts fire immediately. Also, signal.throwIfAborted() can throw before fetchPromise is created, leaving the timeout pending. Consider guarding the timeout value and clearing the timer in a catch path.

🔧 Proposed fix
-const FETCH_TIMEOUT_MS = parseInt(Deno.env.get("FETCH_TIMEOUT_MS") ?? "30000"); // 30 seconds default
+const parsedFetchTimeout = Number(Deno.env.get("FETCH_TIMEOUT_MS"));
+const FETCH_TIMEOUT_MS = Number.isFinite(parsedFetchTimeout) ? parsedFetchTimeout : 30000; // 30 seconds default
@@
-  signal?.throwIfAborted?.();
-
-  // Clean up timeout when fetch completes
-  const fetchPromise = fetcher(input, { signal, ...init });
-  fetchPromise.finally(() => clearTimeout(fetchTimeoutId));
-
-  return fetchPromise;
+  try {
+    signal?.throwIfAborted?.();
+
+    // Clean up timeout when fetch completes
+    const fetchPromise = fetcher(input, { signal, ...init });
+    fetchPromise.finally(() => clearTimeout(fetchTimeoutId));
+
+    return fetchPromise;
+  } catch (err) {
+    clearTimeout(fetchTimeoutId);
+    throw err;
+  }
🤖 Fix all issues with AI agents
In `@runtime/mod.ts`:
- Around line 237-239: REQUEST_TIMEOUT_MS currently uses parseInt which can
return NaN and make setTimeout treat the delay as 0, causing immediate aborts;
change the parsing logic for REQUEST_TIMEOUT_MS so it produces a safe positive
integer fallback (e.g., parse then if Number.isNaN or <=0 use 30000) before
passing it to setTimeout/AbortController; update the symbol usages
REQUEST_TIMEOUT_MS, abortController, and timeoutId accordingly so the timeoutId
is set with a validated numeric timeout value.

In `@utils/patched_fetch.ts`:
- Around line 51-52: The current call to fetcher creates the request with `{
signal, ...init }` which lets init.signal overwrite the timeout `signal`,
disabling the timeout when a caller passes their own signal; change this to
create a combined AbortController (e.g., combinedController) that forwards both
the timeout abort and the caller's `init.signal` (attach an `abort` listener on
`init.signal` to call `combinedController.abort()`), pass
`combinedController.signal` to `fetcher` (instead of `{ signal, ...init }`), and
ensure you remove the `init.signal` listener and clear `fetchTimeoutId` in the
finally/cleanup so no listeners or timers leak (refer to symbols: `fetcher`,
`input`, `init`, `signal`, `fetchPromise`, `fetchTimeoutId`).
🧹 Nitpick comments (1)
runtime/mod.ts (1)

255-262: Verify Context.setDefault per request is safe for concurrent traffic.

Line 256 mutates the global default context for each request. If any work runs outside AsyncLocalStorage, this risks cross-request context bleed (including abort signals). Please confirm this is safe, or switch to a request-scoped bind (e.g., Context.bind / RequestContext.bind) or restore the prior default once the request finishes.

Comment on lines +237 to +239
const REQUEST_TIMEOUT_MS = parseInt(Deno.env.get("REQUEST_TIMEOUT_MS") ?? "30000"); // 30 seconds default
const abortController = new AbortController();
const timeoutId = setTimeout(() => abortController.abort(), REQUEST_TIMEOUT_MS);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Harden REQUEST_TIMEOUT_MS parsing to avoid accidental immediate aborts.

Line 237 uses parseInt, which can yield NaN if the env var is mis-set. setTimeout(NaN) effectively becomes 0, causing immediate aborts. Consider a safe numeric parse with a fallback.

🔧 Proposed fix
-    const REQUEST_TIMEOUT_MS = parseInt(Deno.env.get("REQUEST_TIMEOUT_MS") ?? "30000"); // 30 seconds default
+    const parsedTimeout = Number(Deno.env.get("REQUEST_TIMEOUT_MS"));
+    const REQUEST_TIMEOUT_MS = Number.isFinite(parsedTimeout) ? parsedTimeout : 30000; // 30 seconds default
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const REQUEST_TIMEOUT_MS = parseInt(Deno.env.get("REQUEST_TIMEOUT_MS") ?? "30000"); // 30 seconds default
const abortController = new AbortController();
const timeoutId = setTimeout(() => abortController.abort(), REQUEST_TIMEOUT_MS);
const parsedTimeout = Number(Deno.env.get("REQUEST_TIMEOUT_MS") ?? "30000");
const REQUEST_TIMEOUT_MS = Number.isFinite(parsedTimeout) ? parsedTimeout : 30000; // 30 seconds default
const abortController = new AbortController();
const timeoutId = setTimeout(() => abortController.abort(), REQUEST_TIMEOUT_MS);
🤖 Prompt for AI Agents
In `@runtime/mod.ts` around lines 237 - 239, REQUEST_TIMEOUT_MS currently uses
parseInt which can return NaN and make setTimeout treat the delay as 0, causing
immediate aborts; change the parsing logic for REQUEST_TIMEOUT_MS so it produces
a safe positive integer fallback (e.g., parse then if Number.isNaN or <=0 use
30000) before passing it to setTimeout/AbortController; update the symbol usages
REQUEST_TIMEOUT_MS, abortController, and timeoutId accordingly so the timeoutId
is set with a validated numeric timeout value.

Comment on lines +51 to +52
const fetchPromise = fetcher(input, { signal, ...init });
fetchPromise.finally(() => clearTimeout(fetchTimeoutId));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Combined timeout signal is lost when init.signal is present.

Line 51 uses { signal, ...init }, so init.signal overwrites the combined signal. That bypasses the timeout for callers who pass a signal, defeating the feature.

🐛 Proposed fix
-  const fetchPromise = fetcher(input, { signal, ...init });
+  const fetchPromise = fetcher(input, { ...init, signal });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const fetchPromise = fetcher(input, { signal, ...init });
fetchPromise.finally(() => clearTimeout(fetchTimeoutId));
const fetchPromise = fetcher(input, { ...init, signal });
fetchPromise.finally(() => clearTimeout(fetchTimeoutId));
🤖 Prompt for AI Agents
In `@utils/patched_fetch.ts` around lines 51 - 52, The current call to fetcher
creates the request with `{ signal, ...init }` which lets init.signal overwrite
the timeout `signal`, disabling the timeout when a caller passes their own
signal; change this to create a combined AbortController (e.g.,
combinedController) that forwards both the timeout abort and the caller's
`init.signal` (attach an `abort` listener on `init.signal` to call
`combinedController.abort()`), pass `combinedController.signal` to `fetcher`
(instead of `{ signal, ...init }`), and ensure you remove the `init.signal`
listener and clear `fetchTimeoutId` in the finally/cleanup so no listeners or
timers leak (refer to symbols: `fetcher`, `input`, `init`, `signal`,
`fetchPromise`, `fetchTimeoutId`).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants