diff --git a/pkgs/http_profile/lib/src/http_client_request_profile.dart b/pkgs/http_profile/lib/src/http_client_request_profile.dart index ffa8ac5ebb..1ce632cfd0 100644 --- a/pkgs/http_profile/lib/src/http_client_request_profile.dart +++ b/pkgs/http_profile/lib/src/http_client_request_profile.dart @@ -76,10 +76,15 @@ final class HttpClientRequestProfile { _data['requestData'] = {}; requestData = HttpProfileRequestData._(_data, _updated); _data['responseData'] = {}; - responseData = HttpProfileResponseData._( - _data['responseData'] as Map, _updated); - _data['_requestBodyStream'] = requestData._body.stream; - _data['_responseBodyStream'] = responseData._body.stream; + responseData = HttpProfileResponseData._(_data, _updated); + _data['requestBodyBytes'] = []; + requestData._body.stream.listen( + (final bytes) => (_data['requestBodyBytes'] as List).addAll(bytes), + ); + _data['responseBodyBytes'] = []; + responseData._body.stream.listen( + (final bytes) => (_data['responseBodyBytes'] as List).addAll(bytes), + ); // This entry is needed to support the updatedSince parameter of // ext.dart.io.getHttpProfile. _updated(); diff --git a/pkgs/http_profile/lib/src/http_profile.dart b/pkgs/http_profile/lib/src/http_profile.dart index bca3bfd310..3c208050b4 100644 --- a/pkgs/http_profile/lib/src/http_profile.dart +++ b/pkgs/http_profile/lib/src/http_profile.dart @@ -3,6 +3,7 @@ // BSD-style license that can be found in the LICENSE file. import 'dart:async'; +import 'dart:collection' show UnmodifiableListView; import 'dart:developer' show Service, addHttpClientProfilingData; import 'dart:io' show HttpClient, HttpClientResponseCompressionState; import 'dart:isolate' show Isolate; diff --git a/pkgs/http_profile/lib/src/http_profile_request_data.dart b/pkgs/http_profile/lib/src/http_profile_request_data.dart index 6c5aeb19a9..2bcd1a7c6c 100644 --- a/pkgs/http_profile/lib/src/http_profile_request_data.dart +++ b/pkgs/http_profile/lib/src/http_profile_request_data.dart @@ -38,9 +38,13 @@ final class HttpProfileRequestData { Map get _requestData => _data['requestData'] as Map; - /// The body of the request. + /// A sink that can be used to record the body of the request. StreamSink> get bodySink => _body.sink; + /// The body of the request represented as an unmodifiable list of bytes. + List get bodyBytes => + UnmodifiableListView(_data['requestBodyBytes'] as List); + /// Information about the networking connection used in the HTTP request. /// /// This information is meant to be used for debugging. @@ -155,10 +159,10 @@ final class HttpProfileRequestData { /// /// [endTime] is the time when the request was fully sent. It defaults to the /// current time. - void close([DateTime? endTime]) { + Future close([DateTime? endTime]) async { _checkAndUpdate(); _isClosed = true; - unawaited(bodySink.close()); + await bodySink.close(); _data['requestEndTimestamp'] = (endTime ?? DateTime.now()).microsecondsSinceEpoch; } @@ -172,10 +176,10 @@ final class HttpProfileRequestData { /// /// [endTime] is the time when the error occurred. It defaults to the current /// time. - void closeWithError(String value, [DateTime? endTime]) { + Future closeWithError(String value, [DateTime? endTime]) async { _checkAndUpdate(); _isClosed = true; - unawaited(bodySink.close()); + await bodySink.close(); _requestData['error'] = value; _data['requestEndTimestamp'] = (endTime ?? DateTime.now()).microsecondsSinceEpoch; diff --git a/pkgs/http_profile/lib/src/http_profile_response_data.dart b/pkgs/http_profile/lib/src/http_profile_response_data.dart index ee0720cfa2..3a972b4f85 100644 --- a/pkgs/http_profile/lib/src/http_profile_response_data.dart +++ b/pkgs/http_profile/lib/src/http_profile_response_data.dart @@ -32,15 +32,23 @@ final class HttpProfileResponseData { final void Function() _updated; final StreamController> _body = StreamController>(); + Map get _responseData => + _data['responseData'] as Map; + /// Records a redirect that the connection went through. void addRedirect(HttpProfileRedirectData redirect) { _checkAndUpdate(); - (_data['redirects'] as List>).add(redirect._toJson()); + (_responseData['redirects'] as List>) + .add(redirect._toJson()); } - /// The body of the response. + /// A sink that can be used to record the body of the response. StreamSink> get bodySink => _body.sink; + /// The body of the response represented as an unmodifiable list of bytes. + List get bodyBytes => + UnmodifiableListView(_data['responseBodyBytes'] as List); + /// Information about the networking connection used in the HTTP response. /// /// This information is meant to be used for debugging. @@ -57,7 +65,7 @@ final class HttpProfileResponseData { ); } } - _data['connectionInfo'] = {...value}; + _responseData['connectionInfo'] = {...value}; } /// The reponse headers where duplicate headers are represented using a list @@ -74,10 +82,10 @@ final class HttpProfileResponseData { set headersListValues(Map>? value) { _checkAndUpdate(); if (value == null) { - _data.remove('headers'); + _responseData.remove('headers'); return; } - _data['headers'] = {...value}; + _responseData['headers'] = {...value}; } /// The response headers where duplicate headers are represented using a @@ -94,10 +102,10 @@ final class HttpProfileResponseData { set headersCommaValues(Map? value) { _checkAndUpdate(); if (value == null) { - _data.remove('headers'); + _responseData.remove('headers'); return; } - _data['headers'] = splitHeaderValues(value); + _responseData['headers'] = splitHeaderValues(value); } // The compression state of the response. @@ -107,57 +115,57 @@ final class HttpProfileResponseData { // uncompressed bytes when they listen to the response body byte stream. set compressionState(HttpClientResponseCompressionState value) { _checkAndUpdate(); - _data['compressionState'] = value.name; + _responseData['compressionState'] = value.name; } // The reason phrase associated with the response e.g. "OK". set reasonPhrase(String? value) { _checkAndUpdate(); if (value == null) { - _data.remove('reasonPhrase'); + _responseData.remove('reasonPhrase'); } else { - _data['reasonPhrase'] = value; + _responseData['reasonPhrase'] = value; } } /// Whether the status code was one of the normal redirect codes. set isRedirect(bool value) { _checkAndUpdate(); - _data['isRedirect'] = value; + _responseData['isRedirect'] = value; } /// The persistent connection state returned by the server. set persistentConnection(bool value) { _checkAndUpdate(); - _data['persistentConnection'] = value; + _responseData['persistentConnection'] = value; } /// The content length of the response body, in bytes. set contentLength(int? value) { _checkAndUpdate(); if (value == null) { - _data.remove('contentLength'); + _responseData.remove('contentLength'); } else { - _data['contentLength'] = value; + _responseData['contentLength'] = value; } } set statusCode(int value) { _checkAndUpdate(); - _data['statusCode'] = value; + _responseData['statusCode'] = value; } /// The time at which the initial response was received. set startTime(DateTime value) { _checkAndUpdate(); - _data['startTime'] = value.microsecondsSinceEpoch; + _responseData['startTime'] = value.microsecondsSinceEpoch; } HttpProfileResponseData._( this._data, this._updated, ) { - _data['redirects'] = >[]; + _responseData['redirects'] = >[]; } void _checkAndUpdate() { @@ -176,11 +184,12 @@ final class HttpProfileResponseData { /// /// [endTime] is the time when the response was fully received. It defaults /// to the current time. - void close([DateTime? endTime]) { + Future close([DateTime? endTime]) async { _checkAndUpdate(); _isClosed = true; - unawaited(bodySink.close()); - _data['endTime'] = (endTime ?? DateTime.now()).microsecondsSinceEpoch; + await bodySink.close(); + _responseData['endTime'] = + (endTime ?? DateTime.now()).microsecondsSinceEpoch; } /// Signal that receiving the response has failed with an error. @@ -192,11 +201,12 @@ final class HttpProfileResponseData { /// /// [endTime] is the time when the error occurred. It defaults to the current /// time. - void closeWithError(String value, [DateTime? endTime]) { + Future closeWithError(String value, [DateTime? endTime]) async { _checkAndUpdate(); _isClosed = true; - unawaited(bodySink.close()); - _data['error'] = value; - _data['endTime'] = (endTime ?? DateTime.now()).microsecondsSinceEpoch; + await bodySink.close(); + _responseData['error'] = value; + _responseData['endTime'] = + (endTime ?? DateTime.now()).microsecondsSinceEpoch; } } diff --git a/pkgs/http_profile/test/close_test.dart b/pkgs/http_profile/test/close_test.dart index 8b468611b6..ffa3579917 100644 --- a/pkgs/http_profile/test/close_test.dart +++ b/pkgs/http_profile/test/close_test.dart @@ -28,7 +28,7 @@ void main() { group('requestData.close', () { test('no arguments', () async { expect(backingMap['requestEndTimestamp'], isNull); - profile.requestData.close(); + await profile.requestData.close(); expect( backingMap['requestEndTimestamp'], @@ -39,7 +39,7 @@ void main() { test('with time', () async { expect(backingMap['requestEndTimestamp'], isNull); - profile.requestData.close(DateTime.parse('2024-03-23')); + await profile.requestData.close(DateTime.parse('2024-03-23')); expect( backingMap['requestEndTimestamp'], @@ -48,7 +48,7 @@ void main() { }); test('then write body', () async { - profile.requestData.close(); + await profile.requestData.close(); expect( () => profile.requestData.bodySink.add([1, 2, 3]), @@ -57,7 +57,7 @@ void main() { }); test('then mutate', () async { - profile.requestData.close(); + await profile.requestData.close(); expect( () => profile.requestData.contentLength = 5, @@ -75,7 +75,7 @@ void main() { test('no arguments', () async { expect(responseData['endTime'], isNull); - profile.responseData.close(); + await profile.responseData.close(); expect( responseData['endTime'], @@ -86,7 +86,7 @@ void main() { test('with time', () async { expect(responseData['endTime'], isNull); - profile.responseData.close(DateTime.parse('2024-03-23')); + await profile.responseData.close(DateTime.parse('2024-03-23')); expect( responseData['endTime'], @@ -95,7 +95,7 @@ void main() { }); test('then write body', () async { - profile.responseData.close(); + await profile.responseData.close(); expect( () => profile.responseData.bodySink.add([1, 2, 3]), @@ -104,7 +104,7 @@ void main() { }); test('then mutate', () async { - profile.responseData.close(); + await profile.responseData.close(); expect( () => profile.responseData.contentLength = 5, diff --git a/pkgs/http_profile/test/http_profile_request_data_test.dart b/pkgs/http_profile/test/http_profile_request_data_test.dart index 4a0c13e9b4..cbd6a20ec4 100644 --- a/pkgs/http_profile/test/http_profile_request_data_test.dart +++ b/pkgs/http_profile/test/http_profile_request_data_test.dart @@ -41,7 +41,7 @@ void main() { test('populating HttpClientRequestProfile.requestEndTimestamp', () async { expect(backingMap['requestEndTimestamp'], isNull); - profile.requestData.close(DateTime.parse('2024-03-23')); + await profile.requestData.close(DateTime.parse('2024-03-23')); expect( backingMap['requestEndTimestamp'], @@ -91,7 +91,7 @@ void main() { final requestData = backingMap['requestData'] as Map; expect(requestData['error'], isNull); - profile.requestData.closeWithError('failed'); + await profile.requestData.closeWithError('failed'); expect(requestData['error'], 'failed'); }); @@ -185,4 +185,16 @@ void main() { expect(proxyDetails['isDirect'], true); expect(proxyDetails['port'], 4321); }); + + test('using HttpClientRequestProfile.requestData.bodySink', () async { + final requestBodyBytes = backingMap['requestBodyBytes'] as List; + expect(requestBodyBytes, isEmpty); + expect(profile.requestData.bodyBytes, isEmpty); + + profile.requestData.bodySink.add([1, 2, 3]); + await profile.requestData.close(); + + expect(requestBodyBytes, [1, 2, 3]); + expect(profile.requestData.bodyBytes, [1, 2, 3]); + }); } diff --git a/pkgs/http_profile/test/http_profile_response_data_test.dart b/pkgs/http_profile/test/http_profile_response_data_test.dart index 625c097664..e24a758021 100644 --- a/pkgs/http_profile/test/http_profile_response_data_test.dart +++ b/pkgs/http_profile/test/http_profile_response_data_test.dart @@ -2,7 +2,6 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. -import 'dart:async'; import 'dart:developer' show getHttpClientProfilingData; import 'dart:io'; @@ -187,7 +186,7 @@ void main() { final responseData = backingMap['responseData'] as Map; expect(responseData['endTime'], isNull); - profile.responseData.close(DateTime.parse('2024-03-23')); + await profile.responseData.close(DateTime.parse('2024-03-23')); expect( responseData['endTime'], @@ -199,28 +198,20 @@ void main() { final responseData = backingMap['responseData'] as Map; expect(responseData['error'], isNull); - profile.responseData.closeWithError('failed'); + await profile.responseData.closeWithError('failed'); expect(responseData['error'], 'failed'); }); - test('using HttpClientRequestProfile.requestBodySink', () async { - final requestBodyStream = - backingMap['_requestBodyStream'] as Stream>; - - profile.requestData.bodySink.add([1, 2, 3]); - profile.requestData.close(); - - expect(await requestBodyStream.expand((i) => i).toList(), [1, 2, 3]); - }); - - test('using HttpClientRequestProfile.responseBodySink', () async { - final responseBodyStream = - backingMap['_responseBodyStream'] as Stream>; + test('using HttpClientRequestProfile.responseData.bodySink', () async { + final responseBodyBytes = backingMap['responseBodyBytes'] as List; + expect(responseBodyBytes, isEmpty); + expect(profile.responseData.bodyBytes, isEmpty); profile.responseData.bodySink.add([1, 2, 3]); - profile.responseData.close(); + await profile.responseData.close(); - expect(await responseBodyStream.expand((i) => i).toList(), [1, 2, 3]); + expect(responseBodyBytes, [1, 2, 3]); + expect(profile.responseData.bodyBytes, [1, 2, 3]); }); }