Added size support to Network tab (#9744)
## Overview
This PR adds support for displaying response payload size in the Network tab
and fixes #6165.
It introduces a new **"Size"** column in the network requests table and displays response size in the **Overview panel** of the request inspector.
---
## Changes
### 1. Data Model Updates
**File:**
`packages/devtools_app/lib/src/screens/network/network_model.dart`
* Added two new getters to the `NetworkRequest` base class:
* `requestBytes`
* `responseBytes`
* Implemented these getters in the `Socket` class using:
* `writeBytes` : request size
* `readBytes` : response size
**Purpose:**
Expose byte-level data in a unified way for all network request types.
---
### 2. HTTP Data Handling
**File:**
`packages/devtools_app/lib/src/shared/http/http_request_data.dart`
* Added logic to extract response size using the `content-length` header
* Handles both `String` and `List<String>` header formats
**Purpose:**
Provide response size for HTTP requests when available, without requiring changes to the Dart VM.
---
### 3. Shared Utility
**File:**
`packages/devtools_app/lib/src/screens/network/utils/http_utils.dart`
* Moved `formatBytes` into a reusable utility function
* Uses **decimal (base-10) units** (`kB`, `MB`) to align with Chrome DevTools
* Handles null and negative values safely
**Purpose:**
Ensure consistent formatting across the network table and inspector views.
---
### 4. Network Table UI
**File:**
`packages/devtools_app/lib/src/screens/network/network_screen.dart`
* Added a new column: **"Size"**
* Displays formatted response size
* Shows `-` when size is unavailable
---
### 5. Request Inspector (Overview Panel)
**File:**
`packages/devtools_app/lib/src/screens/network/network_request_inspector_views.dart`
* Added a new row:
* **Response Size**
* Uses shared `formatBytes` utility
---
### 6. Tests
* Added unit tests for:
* `formatBytes` utility in `http_utils_test.dart`
* `responseBytes` parsing logic in `http_request_data.dart`
* Covers edge cases including:
* string and list headers
* invalid values
* null handling
---
## Why request size is not included
Request size is not reliably available for HTTP requests due to limitations in the current DevTools and VM service APIs:
* The Dart VM does not expose request payload size in `HttpProfileRequest`
* HTTP request bodies are not always accessible or fully captured
* Headers such as `content-length` are often absent for outgoing requests
* Streaming and chunked requests complicate accurate measurement
While socket-level request size (`writeBytes`) is available, it is not consistently applicable to HTTP requests.
Therefore, including request size would require changes in the Dart SDK / VM layer.
This PR focuses on **response size**, which can be reliably determined using:
* Socket `readBytes`
* HTTP `content-length` header (when present)
---
## Screenshot
<img width="1359" height="882" alt="Screenshot 2026-03-27 233804" src="https://github.com/user-attachments/assets/4ddce5eb-1a4b-4a9e-80b6-cd16fa226c13" />
---
## Future Work
* Add request size support when VM-level data becomes available
* Introduce separate request/response size columns
* Improve accuracy via VM instrumentation
---
### General checklist
* [x] I read the Contributor Guide
* [x] I read the Tree Hygiene guidelines
* [x] I followed the Flutter Style Guide
* [x] I signed the CLA
* [x] I updated relevant documentation
---
### Issues checklist
* [x] This PR fixes #6165
---
### Tests checklist
* [x] Added unit tests for new functionality
---
### AI-tooling checklist
* [x] I used AI tooling responsibly and verified all generated content
---
### Feature-change checklist
* [x] This PR changes DevTools UI
* [x] Added entry to `NEXT_RELEASE_NOTES.md`
* [x] Included screenshots
* [x] Verified changes locally
diff --git a/packages/devtools_app/lib/src/screens/network/network_model.dart b/packages/devtools_app/lib/src/screens/network/network_model.dart
index 8a6697f..9ccc3c5 100644
--- a/packages/devtools_app/lib/src/screens/network/network_model.dart
+++ b/packages/devtools_app/lib/src/screens/network/network_model.dart
@@ -29,6 +29,9 @@
int? get port;
+ int? get requestBytes => null;
+ int? get responseBytes => null;
+
bool get didFail;
/// True if the request hasn't completed yet.
@@ -160,6 +163,12 @@
@override
int get port => _socket.port;
+ @override
+ int get requestBytes => writeBytes;
+
+ @override
+ int get responseBytes => readBytes;
+
// TODO(kenz): what determines a web socket request failure?
@override
bool get didFail => false;
diff --git a/packages/devtools_app/lib/src/screens/network/network_request_inspector_views.dart b/packages/devtools_app/lib/src/screens/network/network_request_inspector_views.dart
index e9bf905..118f76c 100644
--- a/packages/devtools_app/lib/src/screens/network/network_request_inspector_views.dart
+++ b/packages/devtools_app/lib/src/screens/network/network_request_inspector_views.dart
@@ -17,6 +17,7 @@
import '../../shared/ui/common_widgets.dart';
import 'network_controller.dart';
import 'network_model.dart';
+import 'utils/http_utils.dart';
// Approximately double the indent of the expandable tile's title.
const _rowIndentPadding = 30.0;
@@ -625,6 +626,7 @@
}
List<Widget> _buildGeneralRows(BuildContext context) {
+ final bytes = data.responseBytes;
return [
// TODO(kenz): show preview for requests (png, response body, proto)
_buildRow(
@@ -658,6 +660,14 @@
),
const SizedBox(height: defaultSpacing),
],
+
+ _buildRow(
+ context: context,
+ title: 'Response Size',
+ child: _valueText(bytes != null ? formatBytes(bytes) : '-'),
+ ),
+ const SizedBox(height: defaultSpacing),
+
if (data.contentType != null) ...[
_buildRow(
context: context,
diff --git a/packages/devtools_app/lib/src/screens/network/network_screen.dart b/packages/devtools_app/lib/src/screens/network/network_screen.dart
index 0320b51..a9358f3 100644
--- a/packages/devtools_app/lib/src/screens/network/network_screen.dart
+++ b/packages/devtools_app/lib/src/screens/network/network_screen.dart
@@ -29,6 +29,7 @@
import 'network_controller.dart';
import 'network_model.dart';
import 'network_request_inspector.dart';
+import 'utils/http_utils.dart';
class NetworkScreen extends Screen {
NetworkScreen() : super.fromMetaData(ScreenMetaData.network);
@@ -352,6 +353,7 @@
static const statusColumn = StatusColumn();
static const typeColumn = TypeColumn();
static const durationColumn = DurationColumn();
+ static const responseSizeColumn = ResponseSizeColumn();
static final timestampColumn = TimestampColumn();
static const actionsColumn = ActionsColumn();
static final columns = <ColumnData<NetworkRequest>>[
@@ -360,6 +362,7 @@
statusColumn,
typeColumn,
durationColumn,
+ responseSizeColumn,
timestampColumn,
actionsColumn,
];
@@ -394,6 +397,20 @@
}
}
+class ResponseSizeColumn extends ColumnData<NetworkRequest> {
+ const ResponseSizeColumn()
+ : super('Size', alignment: ColumnAlignment.right, fixedWidthPx: 90);
+
+ @override
+ int? getValue(NetworkRequest dataObject) => dataObject.responseBytes;
+
+ @override
+ String getDisplayValue(NetworkRequest dataObject) {
+ final bytes = dataObject.responseBytes;
+ return bytes != null ? formatBytes(bytes) : '-';
+ }
+}
+
class AddressColumn extends ColumnData<NetworkRequest>
implements ColumnRenderer<NetworkRequest> {
AddressColumn()
diff --git a/packages/devtools_app/lib/src/screens/network/utils/http_utils.dart b/packages/devtools_app/lib/src/screens/network/utils/http_utils.dart
index 9efd548..b4c3079 100644
--- a/packages/devtools_app/lib/src/screens/network/utils/http_utils.dart
+++ b/packages/devtools_app/lib/src/screens/network/utils/http_utils.dart
@@ -29,3 +29,18 @@
// Calculate the byte length of the headers string
return utf8.encode(headersString).length;
}
+
+// Output Formats:
+// - 512 → "512 B"
+// - 2000 → "2.0 kB"
+// - 1000000 → "1.0 MB"
+// Values are rounded to one decimal place for kB and MB.
+// Uses decimal (base-10) units to match Chrome DevTools.
+String formatBytes(int? bytes) {
+ if (bytes == null || bytes < 0) return '-';
+ if (bytes < 1000) return '$bytes B';
+ if (bytes < 1000 * 1000) {
+ return '${(bytes / 1000).toStringAsFixed(1)} kB';
+ }
+ return '${(bytes / (1000 * 1000)).toStringAsFixed(1)} MB';
+}
diff --git a/packages/devtools_app/lib/src/shared/http/http_request_data.dart b/packages/devtools_app/lib/src/shared/http/http_request_data.dart
index 270bf06..81ec0d3 100644
--- a/packages/devtools_app/lib/src/shared/http/http_request_data.dart
+++ b/packages/devtools_app/lib/src/shared/http/http_request_data.dart
@@ -251,8 +251,28 @@
return connectionInfo != null ? connectionInfo[_localPortKey] : null;
}
+ @override
+ int? get responseBytes {
+ final headers = _request.response?.headers;
+ if (headers == null) return null;
+
+ final contentLength = headers['content-length'];
+
+ if (contentLength is String) {
+ return int.tryParse(contentLength);
+ }
+ if (contentLength is List && contentLength.isNotEmpty) {
+ final first = contentLength.first;
+
+ if (first is int) return first;
+ if (first is String) return int.tryParse(first);
+ }
+ return null;
+ }
+
/// True if the HTTP request hasn't completed yet, determined by
/// `isRequestComplete` / `isResponseComplete` from the profile data.
+
@override
bool get inProgress {
if (_isCancelled) return false;
diff --git a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
index 3a13606..323d3c1 100644
--- a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
+++ b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
@@ -40,8 +40,14 @@
## Network profiler updates
+- Added response size column to the Network tab and displayed response size in the request inspector overview.
+ [#9744](https://github.com/flutter/devtools/pull/9744)
+
- 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)
+- Added a filter setting to hide HTTP-profiler socket data.
+ [#9698](https://github.com/flutter/devtools/pull/9698)
+
## Logging updates
- Fixed an issue where log messages containing newline characters were incorrectly split into multiple separate entries in the Logging screen. [#9757](https://github.com/flutter/devtools/pull/9757)
diff --git a/packages/devtools_app/test/shared/http/http_request_data_test.dart b/packages/devtools_app/test/shared/http/http_request_data_test.dart
new file mode 100644
index 0000000..ba09d7f
--- /dev/null
+++ b/packages/devtools_app/test/shared/http/http_request_data_test.dart
@@ -0,0 +1,95 @@
+import 'package:devtools_app/src/shared/http/http_request_data.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+ group('responseBytes', () {
+ Map<String, Object?> baseJson(Map<String, Object?> responseHeaders) {
+ return {
+ 'isolateId': 'isolate-1',
+ 'id': 'request-1',
+ 'method': 'GET',
+ 'uri': 'https://example.com',
+ 'events': <Object?>[],
+ 'startTime': DateTime.now().microsecondsSinceEpoch,
+ 'endTime': DateTime.now().microsecondsSinceEpoch,
+ 'request': {
+ 'headers': <String, Object?>{},
+ 'connectionInfo': null,
+ 'contentLength': null,
+ 'cookies': <Object?>[],
+ 'followRedirects': true,
+ 'maxRedirects': 5,
+ 'persistentConnection': true,
+ },
+ 'response': {
+ 'headers': responseHeaders,
+ 'connectionInfo': null,
+ 'contentLength': null,
+ 'cookies': <Object?>[],
+ 'compressionState': 'ResponseBodyCompressionState.notCompressed',
+ 'isRedirect': false,
+ 'persistentConnection': true,
+ 'reasonPhrase': 'OK',
+ 'redirects': <Map<String, dynamic>>[],
+ 'statusCode': 200,
+ 'startTime': DateTime.now().microsecondsSinceEpoch,
+ },
+ };
+ }
+
+ // Verifies parsing when content-length is a string value.
+ test('parses content-length from string', () {
+ final request = DartIOHttpRequestData.fromJson(
+ baseJson({'content-length': '1234'}),
+ null,
+ null,
+ );
+
+ expect(request.responseBytes, 1234);
+ });
+
+ // Verifies parsing when content-length is a list of strings.
+ test('parses content-length from list of strings', () {
+ final request = DartIOHttpRequestData.fromJson(
+ baseJson({
+ 'content-length': ['5678'],
+ }),
+ null,
+ null,
+ );
+
+ expect(request.responseBytes, 5678);
+ });
+
+ // Ensures integer values inside a list are handled correctly.
+ test('handles integer in list', () {
+ final request = DartIOHttpRequestData.fromJson(
+ baseJson({
+ 'content-length': [91011],
+ }),
+ null,
+ null,
+ );
+
+ expect(request.responseBytes, 91011);
+ });
+
+ // Returns null when header is missing.
+ test('returns null for missing header', () {
+ final request = DartIOHttpRequestData.fromJson(baseJson({}), null, null);
+
+ expect(request.responseBytes, null);
+ });
+
+ // Returns null when parsing fails.
+ test('returns null for invalid value', () {
+ final request = DartIOHttpRequestData.fromJson(
+ baseJson({'content-length': 'invalid'}),
+ null,
+ null,
+ );
+
+ expect(request.responseBytes, null);
+ });
+ });
+}
diff --git a/packages/devtools_app/test/shared/http/http_utils_test.dart b/packages/devtools_app/test/shared/http/http_utils_test.dart
new file mode 100644
index 0000000..9485373
--- /dev/null
+++ b/packages/devtools_app/test/shared/http/http_utils_test.dart
@@ -0,0 +1,19 @@
+import 'package:devtools_app/src/screens/network/utils/http_utils.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+ group('formatBytes', () {
+ // Verifies correct formatting across different unit ranges.
+ test('formats bytes correctly', () {
+ expect(formatBytes(512), '512 B'); // bytes
+ expect(formatBytes(2000), '2.0 kB'); // kilobytes (base-10)
+ expect(formatBytes(1000000), '1.0 MB'); // megabytes (base-10)
+ });
+
+ // Ensures handling of invalid or missing values.
+ test('handles null and negative values', () {
+ expect(formatBytes(null), '-');
+ expect(formatBytes(-1), '-');
+ });
+ });
+}