From c995ef4c4447388f1a739d84dd0a7d17697b015e Mon Sep 17 00:00:00 2001 From: hrajwade96 Date: Sat, 12 Apr 2025 21:55:26 +0530 Subject: [PATCH 1/3] added mime checks, minor logic improvements in recording --- .../src/screens/network/har_data_entry.dart | 11 ++++++++- .../screens/network/network_controller.dart | 10 ++++---- .../src/shared/http/http_request_data.dart | 23 +++++++++++++++++-- .../lib/src/shared/primitives/utils.dart | 15 ++++++++++++ 4 files changed, 52 insertions(+), 7 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/network/har_data_entry.dart b/packages/devtools_app/lib/src/screens/network/har_data_entry.dart index fe50b818403..8e5560e544c 100644 --- a/packages/devtools_app/lib/src/screens/network/har_data_entry.dart +++ b/packages/devtools_app/lib/src/screens/network/har_data_entry.dart @@ -119,6 +119,9 @@ class HarDataEntry { }; }).toList(); + final isBinary = !isTextMimeType(e.type); + final responseBodyBytes = e.encodedResponse; + return { NetworkEventKeys.startedDateTime.name: e.startTimestamp .toUtc() @@ -153,8 +156,14 @@ class HarDataEntry { NetworkEventKeys.content.name: { NetworkEventKeys.size.name: e.responseBody?.length, NetworkEventKeys.mimeType.name: e.type, - NetworkEventKeys.text.name: e.responseBody, + if (responseBodyBytes != null && isBinary) ...{ + NetworkEventKeys.text.name: base64.encode(responseBodyBytes), + 'encoding': 'base64', + } else if (e.responseBody != null) ...{ + NetworkEventKeys.text.name: e.responseBody, + }, }, + NetworkEventKeys.redirectURL.name: '', NetworkEventKeys.headersSize.name: calculateHeadersSize( e.responseHeaders, diff --git a/packages/devtools_app/lib/src/screens/network/network_controller.dart b/packages/devtools_app/lib/src/screens/network/network_controller.dart index 309b62e8503..cd922a01ed9 100644 --- a/packages/devtools_app/lib/src/screens/network/network_controller.dart +++ b/packages/devtools_app/lib/src/screens/network/network_controller.dart @@ -78,9 +78,8 @@ class NetworkController extends DevToolsScreenController debugPrint('No valid request data to export'); return ''; } - + // Build the HAR object try { - // Build the HAR object final har = HarNetworkData(_httpRequests!); return ExportController().downloadFile( json.encode(har.toJson()), @@ -206,8 +205,11 @@ class NetworkController extends DevToolsScreenController shouldLoad: (data) => !data.isEmpty, loadData: (data) => loadOfflineData(data), ); - } - if (serviceConnection.serviceManager.connectedState.value.connected) { + } else if (serviceConnection + .serviceManager + .connectedState + .value + .connected) { await startRecording(); } } 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 8be9458d23f..4d72b6afde1 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 @@ -101,8 +101,27 @@ class DartIOHttpRequestData extends NetworkRequest { ); _request = updated; final fullRequest = _request as HttpProfileRequest; - _responseBody = utf8.decode(fullRequest.responseBody!); - _requestBody = utf8.decode(fullRequest.requestBody!); + final responseMime = + responseHeaders?['content-type']?.toString().split(';').first; + final requestMime = + requestHeaders?['content-type']?.toString().split(';').first; + + if (fullRequest.responseBody != null) { + if (isTextMimeType(responseMime)) { + _responseBody = utf8.decode(fullRequest.responseBody!); + } else { + _responseBody = base64.encode(fullRequest.responseBody!); + } + } + + if (fullRequest.requestBody != null) { + if (isTextMimeType(requestMime)) { + _requestBody = utf8.decode(fullRequest.requestBody!); + } else { + _requestBody = base64.encode(fullRequest.requestBody!); + } + } + notifyListeners(); } } finally { diff --git a/packages/devtools_app/lib/src/shared/primitives/utils.dart b/packages/devtools_app/lib/src/shared/primitives/utils.dart index 3da354092a5..4945dbe6fef 100644 --- a/packages/devtools_app/lib/src/shared/primitives/utils.dart +++ b/packages/devtools_app/lib/src/shared/primitives/utils.dart @@ -1143,3 +1143,18 @@ String devtoolsAssetsBasePath({required String origin, required String path}) { pathParts.removeLast(); return '$trimmedOrigin${pathParts.join(separator)}'; } + +/// Returns `true` if the given [mimeType] is considered textual and can be +/// safely decoded as UTF-8 without base64 encoding. +/// +/// This function is useful for determining whether the content of an HTTP +/// request or response can be directly included in a HAR or JSON file as +/// human-readable text. +bool isTextMimeType(String? mimeType) { + if (mimeType == null) return false; + return mimeType.startsWith('text/') || + mimeType == 'application/json' || + mimeType == 'application/javascript' || + mimeType == 'application/xml' || + mimeType == 'application/x-www-form-urlencoded'; +} From f8f0a6742c483bbda9fc7ddae5c09ac0d1b28ef5 Mon Sep 17 00:00:00 2001 From: Hrishikesh Date: Sat, 24 May 2025 19:11:59 +0530 Subject: [PATCH 2/3] mime fixes --- .../lib/src/shared/http/http_request_data.dart | 14 +++++++++++++- .../lib/src/shared/primitives/utils.dart | 13 ++++++++----- 2 files changed, 21 insertions(+), 6 deletions(-) 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 4d72b6afde1..749989f651e 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 @@ -101,12 +101,14 @@ class DartIOHttpRequestData extends NetworkRequest { ); _request = updated; final fullRequest = _request as HttpProfileRequest; - final responseMime = + var responseMime = responseHeaders?['content-type']?.toString().split(';').first; final requestMime = requestHeaders?['content-type']?.toString().split(';').first; if (fullRequest.responseBody != null) { + responseMime = normalizeContentType(responseHeaders?['content-type']); + if (isTextMimeType(responseMime)) { _responseBody = utf8.decode(fullRequest.responseBody!); } else { @@ -129,6 +131,16 @@ class DartIOHttpRequestData extends NetworkRequest { } } + //TODO check if all cases are handled correctly + String? normalizeContentType(dynamic header) { + if (header is List && header.isNotEmpty) { + return header.first.toString().split(';').first.trim().toLowerCase(); + } else if (header is String) { + return header.split(';').first.trim().toLowerCase(); + } + return null; + } + static List _parseCookies(List? cookies) { if (cookies == null) return []; return cookies.map((cookie) => Cookie.fromSetCookieValue(cookie)).toList(); diff --git a/packages/devtools_app/lib/src/shared/primitives/utils.dart b/packages/devtools_app/lib/src/shared/primitives/utils.dart index 4945dbe6fef..74ee5ecf241 100644 --- a/packages/devtools_app/lib/src/shared/primitives/utils.dart +++ b/packages/devtools_app/lib/src/shared/primitives/utils.dart @@ -1152,9 +1152,12 @@ String devtoolsAssetsBasePath({required String origin, required String path}) { /// human-readable text. bool isTextMimeType(String? mimeType) { if (mimeType == null) return false; - return mimeType.startsWith('text/') || - mimeType == 'application/json' || - mimeType == 'application/javascript' || - mimeType == 'application/xml' || - mimeType == 'application/x-www-form-urlencoded'; + + // Strip charset if present + final cleanedMime = mimeType.split(';').first.trim().toLowerCase(); + + return cleanedMime.startsWith('text/') || + cleanedMime == 'application/json' || + cleanedMime == 'application/javascript' || + cleanedMime == 'application/xml'; } From d3eb915c4d511badb7c5211328528ab5cf877dd5 Mon Sep 17 00:00:00 2001 From: hrajwade96 Date: Wed, 3 Sep 2025 21:09:52 +0530 Subject: [PATCH 3/3] added new helper functions, moved one function --- .../src/screens/network/utils/http_utils.dart | 52 +++++++++++++++++++ .../src/shared/http/http_request_data.dart | 26 ++++------ .../lib/src/shared/primitives/utils.dart | 18 ------- 3 files changed, 63 insertions(+), 33 deletions(-) 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 9efd5482a73..7ef81239cba 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,55 @@ int calculateHeadersSize(Map? headers) { // Calculate the byte length of the headers string return utf8.encode(headersString).length; } + +/// Returns `true` if the given [mimeType] is considered textual and can be +/// safely decoded as UTF-8 without base64 encoding. +/// +/// This function is useful for determining whether the content of an HTTP +/// request or response can be directly included in a HAR or JSON file as +/// human-readable text. +bool isTextMimeType(String? mimeType) { + if (mimeType == null) return false; + + // Strip charset if present + final cleanedMime = mimeType.split(';').first.trim().toLowerCase(); + + return cleanedMime.startsWith('text/') || + cleanedMime == 'application/json' || + cleanedMime == 'application/javascript' || + cleanedMime == 'application/xml'; +} + +/// Extracts and normalizes the `content-type` MIME type from the headers. +/// +/// - Supports headers as either a `List` or a single `String`. +/// - Strips any parameters (e.g., `charset=utf-8`) and converts to lowercase. +/// - Returns `null` if no valid MIME type is found. +/// +/// Example: +/// - "application/json; charset=utf-8" → "application/json" +/// - ["text/html; charset=UTF-8"] → "text/html" +String? getHeadersMimeType(dynamic header) { + if (header == null) return null; + + final value = header is List + ? (header.isNotEmpty ? header.first : null) + : header; + + if (value == null || value.trim().isEmpty) return null; + + return value.split(';').first.trim().toLowerCase(); +} + +/// Converts the given [bodyBytes] to a String based on its [mimeType]. +/// +/// - If the MIME type is text-based (e.g., `application/json`, `text/html`), +/// it decodes the raw bytes as UTF-8 for readability. +/// - Otherwise, it Base64 encodes the bytes so they can be safely stored +/// in JSON-based exports such as HAR files. +String convertBodyBytesToString(List bodyBytes, String? mimeType) { + if (isTextMimeType(mimeType)) { + return utf8.decode(bodyBytes); + } + return base64.encode(bodyBytes); +} 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 749989f651e..c3d66087d84 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 @@ -11,6 +11,7 @@ import 'package:mime/mime.dart'; import 'package:vm_service/vm_service.dart'; import '../../screens/network/network_model.dart'; +import '../../screens/network/utils/http_utils.dart'; import '../globals.dart'; import '../primitives/utils.dart'; import 'constants.dart'; @@ -101,27 +102,22 @@ class DartIOHttpRequestData extends NetworkRequest { ); _request = updated; final fullRequest = _request as HttpProfileRequest; - var responseMime = - responseHeaders?['content-type']?.toString().split(';').first; - final requestMime = - requestHeaders?['content-type']?.toString().split(';').first; + var responseMime = getHeadersMimeType(responseHeaders?['content-type']); + final requestMime = getHeadersMimeType(requestHeaders?['content-type']); if (fullRequest.responseBody != null) { responseMime = normalizeContentType(responseHeaders?['content-type']); - - if (isTextMimeType(responseMime)) { - _responseBody = utf8.decode(fullRequest.responseBody!); - } else { - _responseBody = base64.encode(fullRequest.responseBody!); - } + _responseBody = convertBodyBytesToString( + fullRequest.responseBody!, + responseMime, + ); } if (fullRequest.requestBody != null) { - if (isTextMimeType(requestMime)) { - _requestBody = utf8.decode(fullRequest.requestBody!); - } else { - _requestBody = base64.encode(fullRequest.requestBody!); - } + _requestBody = convertBodyBytesToString( + fullRequest.requestBody!, + requestMime, + ); } notifyListeners(); diff --git a/packages/devtools_app/lib/src/shared/primitives/utils.dart b/packages/devtools_app/lib/src/shared/primitives/utils.dart index 74ee5ecf241..3da354092a5 100644 --- a/packages/devtools_app/lib/src/shared/primitives/utils.dart +++ b/packages/devtools_app/lib/src/shared/primitives/utils.dart @@ -1143,21 +1143,3 @@ String devtoolsAssetsBasePath({required String origin, required String path}) { pathParts.removeLast(); return '$trimmedOrigin${pathParts.join(separator)}'; } - -/// Returns `true` if the given [mimeType] is considered textual and can be -/// safely decoded as UTF-8 without base64 encoding. -/// -/// This function is useful for determining whether the content of an HTTP -/// request or response can be directly included in a HAR or JSON file as -/// human-readable text. -bool isTextMimeType(String? mimeType) { - if (mimeType == null) return false; - - // Strip charset if present - final cleanedMime = mimeType.split(';').first.trim().toLowerCase(); - - return cleanedMime.startsWith('text/') || - cleanedMime == 'application/json' || - cleanedMime == 'application/javascript' || - cleanedMime == 'application/xml'; -}