Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
45b1c2a
Fix cancelled HTTP requests showing as Pending in DevTools Network tab
rishika0212 Mar 5, 2026
3ecd00f
Simplify inProgress getter logic
rishika0212 Mar 10, 2026
d74f363
Merge branch 'flutter:master' into fix-network-cancelled-status
rishika0212 Mar 10, 2026
f5204f6
added release notes and fixed analyzer warnings
rishika0212 Mar 10, 2026
39babfa
added release notes
rishika0212 Mar 11, 2026
2ad05f8
Restored @visibleForTesting annotations
rishika0212 Mar 13, 2026
54eafea
Improve network request status classification
rishika0212 Mar 18, 2026
0503e54
psuhing json
rishika0212 Mar 18, 2026
d9162b6
Merge branch 'master' into fix-network-cancelled-status
rishika0212 Mar 18, 2026
0f7a854
Improved cancellation status detection in Network profiler
rishika0212 Mar 23, 2026
76cd92d
Fix network cancellation test lints and typing
rishika0212 Mar 23, 2026
8a50293
Align network status test with cancellation behavior; stabilize eval …
rishika0212 Mar 27, 2026
5de8e82
Refine HTTP cancellation/status handling and network timing graph nul…
rishika0212 Mar 31, 2026
d3d1ae7
Refine network cancellation logic and related tests.
rishika0212 Mar 31, 2026
8f8ed54
Fix network pending expectations and analyzer infos
rishika0212 Apr 8, 2026
59d02dc
Remove unused matcher import in diff pane test
rishika0212 Apr 8, 2026
686699f
Restore diff pane golden assertions
rishika0212 Apr 8, 2026
f5c8639
fix asyncEval integration test failures
rishika0212 Apr 8, 2026
eafbdc7
Merge upstream/master into fix-network-cancelled-status
rishika0212 Apr 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -707,21 +707,31 @@ class NetworkRequestOverviewView extends StatelessWidget {
];
}

Widget _buildTimingRow(Color color, String label, Duration duration) {
final flex = (duration.inMicroseconds / data.duration!.inMicroseconds * 100)
.round();
Duration? get _totalDuration => (data as DartIOHttpRequestData).duration;

Widget _buildTimingRow(
Color color,
String segmentLabel,
Duration segmentDuration,
) {
final totalDuration = _totalDuration!;
final flex =
(segmentDuration.inMicroseconds / totalDuration.inMicroseconds * 100)
.round();
return Flexible(
flex: flex,
child: DevToolsTooltip(
message: '$label - ${durationText(duration)}',
message: '$segmentLabel - ${durationText(segmentDuration)}',
child: Container(height: _timingGraphHeight, color: color),
),
);
}

Widget _buildHttpTimeGraph() {
final data = this.data as DartIOHttpRequestData;
if (data.duration == null || data.instantEvents.isEmpty) {
if (_totalDuration == null ||
_totalDuration!.inMicroseconds == 0 ||
data.instantEvents.isEmpty) {
return Container(
key: httpTimingGraphKey,
height: 18.0,
Expand Down
58 changes: 47 additions & 11 deletions packages/devtools_app/lib/src/shared/http/http_request_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -135,14 +135,38 @@ class DartIOHttpRequestData extends NetworkRequest {

bool get _hasError => _request.request?.hasError ?? false;

DateTime? get _endTime =>
_hasError ? _request.endTime : _request.response?.endTime;
DateTime? get _endTime => (_hasError || _isCancelled)
? _request.endTime
: _request.response?.endTime;

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

// Markers used for substring matching against request / response errors
// and request event names to classify cancelled requests.
//
// Derived from observed cancellation wording in HTTP profiler payloads,
// keeping specific terms to reduce false positives.
const cancellationMarkers = ['canceled', 'cancelled', 'aborted'];

return cancellationMarkers.any(normalized.contains);
}

bool get _hasCancellationError {
final requestError = _request.request?.error;
final responseError = _request.response?.error;
return _matchesCancellationMarker(requestError) ||
_matchesCancellationMarker(responseError);
}

bool get _hasCancellationEvent =>
_request.events.any((event) => _matchesCancellationMarker(event.event));

@override
Duration? get duration {
if (inProgress || !isValid) return null;
// Timestamps are in microseconds
return _endTime!.difference(_request.startTime);
return _endTime?.difference(_request.startTime);
}

/// Whether the request is safe to display in the UI.
Expand All @@ -156,7 +180,7 @@ class DartIOHttpRequestData extends NetworkRequest {
return {
'method': _request.method,
'uri': _request.uri.toString(),
if (!didFail) ...{
if (!didFail && !_isCancelled) ...{
'connectionInfo': _request.request?.connectionInfo,
'contentLength': _request.request?.contentLength,
},
Expand Down Expand Up @@ -227,11 +251,15 @@ class DartIOHttpRequestData extends NetworkRequest {
return connectionInfo != null ? connectionInfo[_localPortKey] : null;
}

/// True if the HTTP request hasn't completed yet, determined by the lack of
/// an end time in the response data.
/// True if the HTTP request hasn't completed yet, determined by
/// `isRequestComplete` / `isResponseComplete` from the profile data.
@override
Comment thread
srawlins marked this conversation as resolved.
bool get inProgress =>
_hasError ? !_request.isRequestComplete : !_request.isResponseComplete;
bool get inProgress {
if (_isCancelled) return false;
return _hasError
? !_request.isRequestComplete
: !_request.isResponseComplete;
}

/// All instant events logged to the timeline for this HTTP request.
List<DartIOHttpInstantEvent> get instantEvents {
Expand Down Expand Up @@ -273,6 +301,7 @@ class DartIOHttpRequestData extends NetworkRequest {
bool get didFail {
if (status == null) return false;
if (status == 'Error') return true;
if (status == 'Cancelled') return false;

try {
final code = int.parse(status!);
Expand Down Expand Up @@ -301,12 +330,19 @@ class DartIOHttpRequestData extends NetworkRequest {
DateTime get startTimestamp => _request.startTime;

@override
String? get status =>
_hasError ? 'Error' : _request.response?.statusCode.toString();
String? get status {
if (_isCancelled) return 'Cancelled';

if (_hasError) return 'Error';

return _request.response?.statusCode.toString();
}

@override
String get uri => _request.uri.toString();

bool get _isCancelled => _hasCancellationError || _hasCancellationEvent;

String? get responseBody {
if (_request is! HttpProfileRequest) {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ TODO: Remove this section if there are not any updates.

## Network profiler updates

TODO: Remove this section if there are not any updates.
- Improved 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](https://github.com/flutter/devtools/pull/9683)

## Logging updates

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,17 @@ void main() {
expect(requests.length, numRequests);
final httpRequests = requests.whereType<DartIOHttpRequestData>().toList();
for (final request in httpRequests) {
expect(request.duration, request.inProgress ? isNull : isNotNull);
expect(
request.duration,
request.inProgress || request.endTimestamp == null
? isNull
: isNotNull,
);
expect(request.general.length, greaterThan(0));
expect(httpMethods.contains(request.method), true);
expect(request.status, request.inProgress ? isNull : isNotNull);
if (request.inProgress) {
expect(request.status, isNull);
Comment thread
srawlins marked this conversation as resolved.
}
}

// Finally, call `clear()` and ensure the requests have been cleared.
Expand Down Expand Up @@ -205,15 +212,31 @@ void main() {

controller.setActiveFilter(query: 'status:Error');
expect(profile, hasLength(numRequests));
expect(controller.filteredData.value, hasLength(1));

controller.setActiveFilter(query: 's:101');
final errorCount = profile
.whereType<DartIOHttpRequestData>()
.where((request) => request.status == 'Error')
.length;
expect(controller.filteredData.value, hasLength(errorCount));

final firstStatus = profile
.whereType<DartIOHttpRequestData>()
.map((request) => request.status)
.whereType<String>()
.first;
final firstStatusCount = profile
.whereType<DartIOHttpRequestData>()
.where((request) => request.status == firstStatus)
.length;
controller.setActiveFilter(query: 's:$firstStatus');
expect(profile, hasLength(numRequests));
expect(controller.filteredData.value, hasLength(1));
expect(controller.filteredData.value, hasLength(firstStatusCount));

controller.setActiveFilter(query: '-s:Error');
expect(profile, hasLength(numRequests));
expect(controller.filteredData.value, hasLength(8));
expect(
controller.filteredData.value,
hasLength(numRequests - errorCount),
);

controller.setActiveFilter(query: 'type:json');
expect(profile, hasLength(numRequests));
Expand Down Expand Up @@ -253,11 +276,28 @@ void main() {

controller.setActiveFilter(query: '-status:error method:get');
expect(profile, hasLength(numRequests));
expect(controller.filteredData.value, hasLength(3));
final nonErrorGetCount = profile
.whereType<DartIOHttpRequestData>()
.where(
(request) =>
request.method.toLowerCase() == 'get' &&
request.status?.toLowerCase() != 'error',
)
.length;
expect(controller.filteredData.value, hasLength(nonErrorGetCount));

controller.setActiveFilter(query: '-status:error method:get t:http');
expect(profile, hasLength(numRequests));
expect(controller.filteredData.value, hasLength(2));
final nonErrorGetHttpCount = profile
.whereType<DartIOHttpRequestData>()
.where(
(request) =>
request.method.toLowerCase() == 'get' &&
request.status?.toLowerCase() != 'error' &&
request.type.toLowerCase() == 'http',
)
.length;
expect(controller.filteredData.value, hasLength(nonErrorGetHttpCount));
});

test('filterData hides tcp sockets via setting filter', () async {
Expand Down Expand Up @@ -341,6 +381,21 @@ void main() {
'statusCode': 200,
},
})!;
final request1CancelledWithStatusCode = HttpProfileRequest.parse({
...httpBaseObject,
'events': [
{
'timestamp': startTime + 100,
'event': 'Request cancelled by client',
},
],
'response': {
'startTime': startTime,
'endTime': null,
'redirects': [],
'statusCode': 200,
},
})!;
final request2Pending = HttpProfileRequest.parse({
...httpBaseObject,
'id': '102',
Expand Down Expand Up @@ -403,6 +458,30 @@ void main() {
},
);

test('latest request update wins over stale status for same id', () {
currentNetworkRequests.updateOrAddAll(
requests: [request1Done],
sockets: const [],
timelineMicrosOffset: 0,
);

final initialRequest =
currentNetworkRequests.getRequest('101')! as DartIOHttpRequestData;
expect(initialRequest.status, '200');
expect(initialRequest.status, isNot('Cancelled'));

currentNetworkRequests.updateOrAddAll(
requests: [request1CancelledWithStatusCode],
sockets: const [],
timelineMicrosOffset: 0,
);

final updatedRequest =
currentNetworkRequests.getRequest('101')! as DartIOHttpRequestData;
expect(updatedRequest.status, 'Cancelled');
expect(updatedRequest.inProgress, false);
});

test('clear', () {
final reqs = [request1Pending, request2Pending];
final sockets = [socketStats1Pending, socketStats2Pending];
Expand Down
Loading
Loading