Skip to content

Fix/web fetch ctrl c abort#24320

Open
ProthamD wants to merge 5 commits intogoogle-gemini:mainfrom
ProthamD:fix/web-fetch-ctrl-c-abort
Open

Fix/web fetch ctrl c abort#24320
ProthamD wants to merge 5 commits intogoogle-gemini:mainfrom
ProthamD:fix/web-fetch-ctrl-c-abort

Conversation

@ProthamD
Copy link
Copy Markdown
Contributor

Problem

When web_fetch is actively loading a URL and the user presses Ctrl+C, the CLI does not cancel immediately. Instead:

  1. Silent retries happen - The request is retried up to 3 times with exponential backoff
  2. Long hang time - Backoff delays total ~35 seconds (5s + 10s + 20s)
  3. No feedback - No error messages or retry indicators shown to the user
  4. Wrong telemetry - Logs report ETIMEDOUT instead of user cancellation

Root Cause

In fetchWithTimeout, when the abort controller fires:

  • Both the internal timeout timer and external cancellation signal trigger the same AbortController
  • The resulting AbortError is unconditionally converted to FetchError with code ETIMEDOUT
  • Because ETIMEDOUT is in RETRYABLE_NETWORK_CODES, retryWithBackoff treats user cancellation as a transient network failure
  • This causes automatic retry loops totaling ~35 seconds of delay

Solution

The fix distinguishes between user-initiated cancellation and internal timeouts:

  1. In fetchWithTimeout: Check if the external signal was aborted

    • If yes → throw a plain AbortError (user cancellation)
    • If no → throw FetchError with ETIMEDOUT (timeout)
  2. In retryWithBackoff: Immediately re-throw AbortError without retrying

    • This prevents user cancellation from being treated as a retryable error
  3. Result: Ctrl+C cancels immediately without retry delays

Changes Made

packages/core/src/utils/fetch.ts

  1. Added logic in fetchWithTimeout to detect when the caller's signal was aborted
  2. When external signal is aborted, throw a plain AbortError instead of FetchError with ETIMEDOUT
  3. This ensures the retry layer correctly identifies user cancellation

packages/core/src/utils/fetch.test.ts

  • Updated existing test name for clarity: "should handle timeouts" → "should throw FetchError with ETIMEDOUT on an internal timeout"
  • Added new test: "should throw an AbortError (not ETIMEDOUT) when the caller signal is aborted"
    • Verifies that user-initiated cancellation throws AbortError (not timeout)
    • Confirms no retry behavior occurs for user cancellation

fixes:

fixes #21546

Testing

  • ✅ Unit tests verify correct error types are thrown
  • ✅ Existing timeout behavior unchanged
  • ✅ Ctrl+C now exits immediately without retry delays

Affected Scenarios

Fixed: Pressing Ctrl+C during active web_fetch request
Fixed: Agent abort signals are now properly handled
Fixed: Telemetry now correctly reports user cancellation (not timeout)

@ProthamD ProthamD requested a review from a team as a code owner March 31, 2026 09:37
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses an issue where user-initiated cancellations (such as Ctrl+C) during a web fetch request were incorrectly handled as transient network timeouts. By refining the error handling logic to identify user-triggered aborts, the system now cancels immediately instead of attempting multiple retries, significantly improving user experience and telemetry accuracy.

Highlights

  • Cancellation Logic: Updated fetchWithTimeout to distinguish between user-initiated cancellation and internal timeouts by checking the AbortSignal state.
  • Retry Prevention: Ensured that user-initiated cancellations throw a plain AbortError instead of a FetchError with ETIMEDOUT, preventing unnecessary and lengthy retry loops.
  • Test Coverage: Added a new test case to verify that user-initiated aborts do not trigger retry behavior and updated existing test descriptions for clarity.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@ProthamD
Copy link
Copy Markdown
Contributor Author

/gemini review

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a comprehensive implementation plan for silencing passing test output to improve the cleanliness of the preflight process. It also includes a specific fix in the core fetch utility to correctly distinguish between internal timeouts and user-initiated cancellations. A review comment suggests improving error handling by re-throwing the original abort reason or the caught error instead of creating a generic Error object, which helps preserve the stack trace and debugging information.

Comment thread packages/core/src/utils/fetch.ts Outdated
@gemini-cli gemini-cli Bot added priority/p1 Important and should be addressed in the near term. area/core Issues related to User Interface, OS Support, Core Functionality labels Mar 31, 2026
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a comprehensive implementation plan for silencing passing test output and includes a specific fix for handling aborted fetch requests. The fetchWithTimeout utility was updated to distinguish between internal timeouts and user-initiated cancellations, ensuring that aborted signals are not incorrectly treated as retryable timeouts. A review comment suggests improving the error handling in fetch.ts by rethrowing the original abort reason or the error itself to preserve the stack trace and context, rather than creating a new generic Error object.

Comment thread packages/core/src/utils/fetch.ts Outdated
Comment on lines +189 to +191
const abortError = new Error('The operation was aborted.');
abortError.name = 'AbortError';
throw abortError;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Instead of creating a new generic Error object, it is better to rethrow the original error or the reason from the AbortSignal. This preserves the original stack trace and any custom abort reason (e.g., a specific cancellation message) provided by the caller. Since the project targets Node.js >= 20, options.signal.reason is available and is the standard way to propagate the cause of an abortion.

Suggested change
const abortError = new Error('The operation was aborted.');
abortError.name = 'AbortError';
throw abortError;
throw options.signal.reason ?? error;
References
  1. Asynchronous operations that can be cancelled by the user should accept and propagate an AbortSignal to ensure cancellability and prevent dangling processes or network requests.
  2. When catching exceptions, log the detailed error for debugging instead of providing only a generic error message.

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/core Issues related to User Interface, OS Support, Core Functionality help wanted We will accept PRs from all issues marked as "help wanted". Thanks for your support! priority/p1 Important and should be addressed in the near term.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

web_fetch: pressing Ctrl+C while a URL is loading causes silent retries instead of immediate cancellation

1 participant