Skip to content

Fix cancelled HTTP requests showing as Pending in DevTools Network tab#9685

Open
rishika0212 wants to merge 11 commits intoflutter:masterfrom
rishika0212:fix-network-cancelled-status
Open

Fix cancelled HTTP requests showing as Pending in DevTools Network tab#9685
rishika0212 wants to merge 11 commits intoflutter:masterfrom
rishika0212:fix-network-cancelled-status

Conversation

@rishika0212
Copy link
Contributor

Description

This PR fixes an issue where cancelled HTTP requests appear as "Pending" in the DevTools Network tab.

When a request is aborted (for example using HttpClientRequest.abort or Dio cancellation), DevTools keeps the request in a Pending state because no response is received. This change detects such cases and displays the request status as "Cancelled" instead.

Changes

  • Detect cancelled/aborted requests in HttpRequestData
  • Display "Cancelled" instead of "Pending" in the Network table
  • Ensure cancelled requests are no longer treated as inProgress
  • Prevent duration from remaining null for cancelled requests
  • Add a regression test to verify the behavior
  • Update CustomPointerScrollView to use cacheExtent so the project compiles with the current Flutter SDK

All existing network tests pass locally.

Fixes: #9593

Pre-launch Checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I read the Tree Hygiene wiki page, which explains my responsibilities.
  • I read the Flutter Style Guide recently and followed its advice.
  • I signed the CLA.
  • I listed at least one issue that this PR fixes in the description above.
  • I added tests to check the change I am making.

build.yaml badge

If you need help, consider asking for help on Discord.

@rishika0212 rishika0212 requested review from a team, bkonyi and kenzieschmoll as code owners March 5, 2026 16:38
Copy link
Contributor

@srawlins srawlins left a comment

Choose a reason for hiding this comment

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

This code looks great; thanks for the PR! I'd like to test manually before landing.

super.shrinkWrap,
super.padding,
super.scrollCacheExtent,
super.cacheExtent,
Copy link
Contributor

Choose a reason for hiding this comment

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

CC @elliette ; from the PR description:

Update CustomPointerScrollView to use cacheExtent so the project compiles with the current Flutter SDK

Copy link
Contributor

@bkonyi bkonyi left a comment

Choose a reason for hiding this comment

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

LGTM overall!

@srawlins
Copy link
Contributor

Hi @rishika0212 sorry for the delay. I've been manually testing this and I have a clarifying question:

I use the devtools_companion app to test the Network panel. I've got a PR that adds "cancellation" support: elliette/devtools_companion#23.

I've observed behavior that seems questionable to me, but you might shed some light. Take this series of steps:

  1. Send a GET request.
  2. The server starts to send a response, (the "Response Body" checkbox is checked).
  3. After 1 second, the request is cancelled by the client.
  4. The server never closes the response.

After these steps, I do not see the Cancelled status (it seems to remain Open). However, if the server never starts to send a response (the "Response Body checkbox is not checked), then the Cancelled status appears as expected.

I think the Cancelled status should still appear, even if the server has started to respond. But I am not an HTTP expert. WDYT?

@srawlins
Copy link
Contributor

Oh, additionally, the other problem I observed is that the table will always show the Cancelled status before a server completes its response, even if the request was not in fact cancelled. I'll double check when this behavior occurs, but I observed it in the DevTools Companion app.

@rishika0212
Copy link
Contributor Author

Hey @srawlins

thanks again for the detailed repro, this was super helpful. I ran through it carefully using devtools_companion (including your srawlins/cancel case) and your exact sequence.

Here’s what I’m seeing:

  • Completes → behaves as expected (200)
  • Does not complete → stays pending (-), as expected
  • Cancelled before completion → now correctly shows as cancelled

I also tightened up the DevTools logic a bit to avoid showing “Cancelled” too early before a response actually completes.

For the specific edge case you mentioned (response starts, then gets cancelled after ~1s):
I agree that ideally this should show as Cancelled.

However, from what I can tell, this case doesn’t always come through with a clear cancellation signal in the HTTP profile data. Sometimes it still ends up looking like a normal 200 style response, which makes it tricky for DevTools to classify it confidently based on the current fields.

So overall from what I can tell…

  • The original issue (Cancelled showing as Pending) should now be fixed
  • The “response-started then cancelled” case seems more like a limitation in the profiling signal (companion/VM service), rather than just UI logic

Let me know if you’re seeing anything different on your side!

@srawlins
Copy link
Contributor

I also tightened up the DevTools logic a bit to avoid showing “Cancelled” too early before a response actually completes.

Do you mean you've done this before today, or you will upload a commit?

@rishika0212
Copy link
Contributor Author

I also tightened up the DevTools logic a bit to avoid showing “Cancelled” too early before a response actually completes.

Do you mean you've done this before today, or you will upload a commit?

Before your comment, I hadn’t specifically tested the “cancel before complete” path in this depth, but after your note I ran those scenarios in devtools_companion, improved the classification logic, and added tests.

I can push the tested improvements now (mainly the cancelled vs pending behavior). The “response already started, then cancelled” case is still not fully reliable yet since the profiling signal is ambiguous in some runs.

@rishika0212
Copy link
Contributor Author

Right now, in the “response started, then client cancels” case, the profile payload can sometimes look like a normal 200 response. When that happens, the UI doesn’t really have a reliable way to tell the difference between:

  1. a real completion
  2. a request that got cancelled midway

So unless we get a clear signal from upstream (like wasCancelled, a dedicated cancel event, or a specific error field), we’re basically guessing. And that guess can go wrong in both directions:

  1. if we’re too aggressive → we show false Cancelled
  2. if we’re too conservative → we miss actual cancellations

@srawlins
Copy link
Contributor

I can push the tested improvements now (mainly the cancelled vs pending behavior).

That would be great!

The “response already started, then cancelled” case is still not fully reliable yet since the profiling signal is ambiguous in some runs.

This sounds OK to me.

@rishika0212
Copy link
Contributor Author

I can push the tested improvements now (mainly the cancelled vs pending behavior).

That would be great!

I've pushed the updates.

@rishika0212 rishika0212 requested a review from srawlins March 21, 2026 18:03
Copy link
Contributor

@srawlins srawlins left a comment

Choose a reason for hiding this comment

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

Just a couple of small changes; thanks!

'cancel',
'canceled',
'cancelled',
'operation canceled',
Copy link
Contributor

Choose a reason for hiding this comment

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

Since we are just searching in a String for these values, we either don't need the longer ones ("operation canceled") or we don't need the shorter ones ("canceled").

I think it is find to just keep the shorter ones.

Are these values derived from some general libraries, like the dio package and dart:io?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, they’re derived from cancellation wording observed in common Dart HTTP stacks, specifically our dart:io, package:http, and dio companion flows and the resulting VM profile payload strings.

'cancelled',
'operation canceled',
'operation cancelled',
'abort',
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should remove the shortest two in this list, "cancel" and "abort." Unless there are examples from dart:io, package:http, or package:dio, where a cancelled error/event contains "cancel" or "abort" but not "cancelled"/"canceled" or "aborted."

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the suggestions, I’ve updated things accordingly.

I kept just canceled, cancelled, and aborted, and removed the longer phrases like operation canceled / operation cancelled since we’re already doing substring matching. I also dropped the more generic terms like cancel and abort to avoid false positives.

These markers are based on the cancellation wording we’ve seen in related flows (like dart:io request.abort(), package:http client.close(), and dio cancelToken.cancel(...)) along with the profiler payload text.

One thing to note: in companion cancel mode, we can still get a 200 since the status gets set early so in those cases, classification relies on explicit cancellation text in the error/event payloads.

DateTime? get _endTime =>
_hasError ? _request.endTime : _request.response?.endTime;

static const _cancellationMarkers = [
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you add a doc comment above this. I'd be nice if it included how the list is to be used, and how it was derived.

DateTime? get _endTime =>
_hasError ? _request.endTime : _request.response?.endTime;

static const _cancellationMarkers = [
Copy link
Contributor

Choose a reason for hiding this comment

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

Since this is a private static field used only one time, by a private method, it might be nice to just inline this list into the method itself.


bool _matchesCancellationMarker(String? value) {
final normalized = value?.toLowerCase();
if (normalized == null) return false;
Copy link
Contributor

Choose a reason for hiding this comment

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

The only way that normalized can be null is if value is null, so we can check that before the toLowerCase code.

return end?.difference(start);
}

// Cancelled request
Copy link
Contributor

Choose a reason for hiding this comment

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

This comment doesn't really add any information.

## Network profiler updates

- Added a filter setting to hide HTTP-profiler socket data. [#9698](https://github.com/flutter/devtools/pull/9698)
- Improve HTTP request status classification in the Network tab to better distinguish cancelled, completed, and in-flight requests (for example, avoiding some cases where cancelled requests appeared as pending). (#9683)
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't replace the "filter setting" release note; just add yours below it, as a second bullet point.


## App size tool updates

- Added documentation links and improved handling for null files. [#9689](https://github.com/flutter/devtools/pull/9689)
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't remove this text either.

expect(httpMethods.contains(request.method), true);
expect(request.status, request.inProgress ? isNull : isNotNull);
if (request.inProgress) {
expect(request.status, isNull);
Copy link
Contributor

Choose a reason for hiding this comment

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

This is clearer than the old code, thanks!

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.

Cancelled requests are shown as "Pending" in Devtools Network Tab

4 participants