From 2d4ccc88ef5642f92382d88888a780c8f770e16c Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Tue, 27 Feb 2024 15:11:33 -0800 Subject: [PATCH 01/10] API adjustments based on cupertino_http usage experience --- pkgs/http_profile/lib/http_profile.dart | 379 ++++++++++++------ pkgs/http_profile/test/close_test.dart | 115 ++++++ .../test/populating_profiles_test.dart | 153 +++---- .../test/profiling_enabled_test.dart | 4 +- 4 files changed, 466 insertions(+), 185 deletions(-) create mode 100644 pkgs/http_profile/test/close_test.dart diff --git a/pkgs/http_profile/lib/http_profile.dart b/pkgs/http_profile/lib/http_profile.dart index b0b7a87677..17023c90d0 100644 --- a/pkgs/http_profile/lib/http_profile.dart +++ b/pkgs/http_profile/lib/http_profile.dart @@ -7,6 +7,54 @@ import 'dart:developer' show Service, addHttpClientProfilingData; import 'dart:io' show HttpClient, HttpClientResponseCompressionState; import 'dart:isolate' show Isolate; +/// "token" as defined in RFC 2616, 2.2 +/// See https://datatracker.ietf.org/doc/html/rfc2616#section-2.2 +const _tokenChars = r"!#$%&'*+\-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`" + 'abcdefghijklmnopqrstuvwxyz|~'; + +/// Splits comma-seperated header values. +var _headerSplitter = RegExp(r'[ \t]*,[ \t]*'); + +/// Splits comma-seperated "Set-Cookie" header values. +/// +/// Set-Cookie strings can contain commas. In particular, the following +/// productions defined in RFC-6265, section 4.1.1: +/// - e.g. "Expires=Sun, 06 Nov 1994 08:49:37 GMT" +/// - e.g. "Path=somepath," +/// - e.g. "AnyString,Really," +/// +/// Some values are ambiguous e.g. +/// "Set-Cookie: lang=en; Path=/foo/" +/// "Set-Cookie: SID=x23" +/// and: +/// "Set-Cookie: lang=en; Path=/foo/,SID=x23" +/// would both be result in `response.headers` => "lang=en; Path=/foo/,SID=x23" +/// +/// The idea behind this regex is that ",=" is more likely to +/// start a new then be part of or . +/// +/// See https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.1 +var _setCookieSplitter = RegExp(r'[ \t]*,[ \t]*(?=[' + _tokenChars + r']+=)'); + +/// Splits comma-seperated header values into a [List]. +/// +/// Copied from `package:http`. +Map> _splitHeaderValues(Map headers) { + var headersWithFieldLists = >{}; + headers.forEach((key, value) { + if (!value.contains(',')) { + headersWithFieldLists[key] = [value]; + } else { + if (key == 'set-cookie') { + headersWithFieldLists[key] = value.split(_setCookieSplitter); + } else { + headersWithFieldLists[key] = value.split(_headerSplitter); + } + } + }); + return headersWithFieldLists; +} + /// Describes an event related to an HTTP request. final class HttpProfileRequestEvent { final int _timestamp; @@ -71,9 +119,16 @@ class HttpProfileRedirectData { /// Describes details about an HTTP request. final class HttpProfileRequestData { final Map _data; - + final StreamController> _body = StreamController>(); + bool _isClosed = false; final void Function() _updated; + Map get _requestData => + _data['requestData'] as Map; + + /// The body of the request. + StreamSink> get bodySink => _body.sink; + /// Information about the networking connection used in the HTTP request. /// /// This information is meant to be used for debugging. @@ -82,6 +137,7 @@ final class HttpProfileRequestData { /// [String] or [int]. For example: /// { 'localPort': 1285, 'remotePort': 443, 'connectionPoolId': '21x23' } set connectionInfo(Map value) { + _checkAndUpdate(); for (final v in value.values) { if (!(v is String || v is int)) { throw ArgumentError( @@ -89,98 +145,146 @@ final class HttpProfileRequestData { ); } } - _data['connectionInfo'] = {...value}; - _updated(); + _requestData['connectionInfo'] = {...value}; } /// The content length of the request, in bytes. - set contentLength(int value) { - _data['contentLength'] = value; - _updated(); + set contentLength(int? value) { + _checkAndUpdate(); + if (value == null) { + _requestData.remove('contentLength'); + } else { + _requestData['contentLength'] = value; + } + } + + /// Whether automatic redirect following was enabled for the request. + set followRedirects(bool value) { + _checkAndUpdate(); + _requestData['followRedirects'] = value; } - /// The cookies presented to the server (in the 'cookie' header). + /// The request headers where duplicate headers are represented using a list + /// of values. /// - /// Usage example: + /// For example: /// /// ```dart - /// profile.requestData.cookies = [ - /// 'sessionId=abc123', - /// 'csrftoken=def456', - /// ]; + /// // Foo: Bar + /// // Foo: Baz + /// + /// profile?.requestData.headersListValues({'Foo', ['Bar', 'Baz']}); /// ``` - set cookies(List value) { - _data['cookies'] = [...value]; - _updated(); - } - - /// The error associated with a failed request. - set error(String value) { - _data['error'] = value; - _updated(); - } - - /// Whether automatic redirect following was enabled for the request. - set followRedirects(bool value) { - _data['followRedirects'] = value; - _updated(); + set headersListValues(Map>? value) { + _checkAndUpdate(); + if (value == null) { + _requestData.remove('headers'); + return; + } + _requestData['headers'] = {...value}; } - set headers(Map> value) { - _data['headers'] = {...value}; - _updated(); + /// The request headers where duplicate headers are represented using + /// comma-seperated list of values. + /// + /// For example: + /// + /// ```dart + /// // Foo: Bar + /// // Foo: Baz + /// + /// profile?.requestData.headersCommaValues({'Foo', 'Bar, Baz']}); + /// ``` + set headersCommaValues(Map? value) { + _checkAndUpdate(); + if (value == null) { + _requestData.remove('headers'); + return; + } + _requestData['headers'] = _splitHeaderValues(value); } /// The maximum number of redirects allowed during the request. set maxRedirects(int value) { - _data['maxRedirects'] = value; - _updated(); + _checkAndUpdate(); + _requestData['maxRedirects'] = value; } /// The requested persistent connection state. set persistentConnection(bool value) { - _data['persistentConnection'] = value; - _updated(); + _checkAndUpdate(); + _requestData['persistentConnection'] = value; } /// Proxy authentication details for the request. set proxyDetails(HttpProfileProxyData value) { - _data['proxyDetails'] = value._toJson(); - _updated(); + _checkAndUpdate(); + _requestData['proxyDetails'] = value._toJson(); } - const HttpProfileRequestData._( + HttpProfileRequestData._( this._data, this._updated, ); + + void _checkAndUpdate() { + if (_isClosed) { + throw StateError('HttpProfileResponseData has been closed, no further ' + 'updates are allowed'); + } + _updated(); + } + + /// Signal that the request, including the entire request body, has been + /// sent. + /// + /// [bodySink] will be closed and the fields of [HttpProfileRequestData] will + /// no longer be changeable. + /// + /// [endTime] is the time when the request was fully sent. It defaults to the + /// current time. + void close([DateTime? endTime]) { + _checkAndUpdate(); + _isClosed = true; + bodySink.close(); + _data['requestEndTimestamp'] = + (endTime ?? DateTime.now()).microsecondsSinceEpoch; + } + + /// Signal that sending the request has failed with an error. + /// + /// [bodySink] will be closed and the fields of [HttpProfileRequestData] will + /// no longer be changeable. + /// + /// [value] is a textual description of the error e.g. 'host does not exist'. + /// + /// [endTime] is the time when the error occurred. It defaults to the current + /// time. + void closeWithError(String value, [DateTime? endTime]) { + _checkAndUpdate(); + _isClosed = true; + bodySink.close(); + _requestData['error'] = value; + _data['requestEndTimestamp'] = + (endTime ?? DateTime.now()).microsecondsSinceEpoch; + } } /// Describes details about a response to an HTTP request. final class HttpProfileResponseData { + bool _isClosed = false; final Map _data; - final void Function() _updated; + final StreamController> _body = StreamController>(); /// Records a redirect that the connection went through. void addRedirect(HttpProfileRedirectData redirect) { + _checkAndUpdate(); (_data['redirects'] as List>).add(redirect._toJson()); - _updated(); } - /// The cookies set by the server (from the 'set-cookie' header). - /// - /// Usage example: - /// - /// ```dart - /// profile.responseData.cookies = [ - /// 'sessionId=abc123', - /// 'id=def456; Max-Age=2592000; Domain=example.com', - /// ]; - /// ``` - set cookies(List value) { - _data['cookies'] = [...value]; - _updated(); - } + /// The body of the request. + StreamSink> get bodySink => _body.sink; /// Information about the networking connection used in the HTTP response. /// @@ -190,6 +294,7 @@ final class HttpProfileResponseData { /// [String] or [int]. For example: /// { 'localPort': 1285, 'remotePort': 443, 'connectionPoolId': '21x23' } set connectionInfo(Map value) { + _checkAndUpdate(); for (final v in value.values) { if (!(v is String || v is int)) { throw ArgumentError( @@ -198,12 +303,46 @@ final class HttpProfileResponseData { } } _data['connectionInfo'] = {...value}; - _updated(); } - set headers(Map> value) { + /// The reponse headers where duplicate headers are represented using a list + /// of values. + /// + /// For example: + /// + /// ```dart + /// // Foo: Bar + /// // Foo: Baz + /// + /// profile?.requestData.headersListValues({'Foo', ['Bar', 'Baz']}); + /// ``` + set headersListValues(Map>? value) { + _checkAndUpdate(); + if (value == null) { + _data.remove('headers'); + return; + } _data['headers'] = {...value}; - _updated(); + } + + /// The response headers where duplicate headers are represented using + /// comma-seperated list of values. + /// + /// For example: + /// + /// ```dart + /// // Foo: Bar + /// // Foo: Baz + /// + /// profile?.responseData.headersCommaValues({'Foo', 'Bar, Baz']}); + /// ``` + set headersCommaValues(Map? value) { + _checkAndUpdate(); + if (value == null) { + _data.remove('headers'); + return; + } + _data['headers'] = _splitHeaderValues(value); } // The compression state of the response. @@ -212,55 +351,51 @@ final class HttpProfileResponseData { // received across the wire and whether callers will receive compressed or // uncompressed bytes when they listen to the response body byte stream. set compressionState(HttpClientResponseCompressionState value) { + _checkAndUpdate(); _data['compressionState'] = value.name; - _updated(); } - set reasonPhrase(String value) { - _data['reasonPhrase'] = value; - _updated(); + // The reason phrase associated with the response e.g. "OK". + set reasonPhrase(String? value) { + _checkAndUpdate(); + if (value == null) { + _data.remove('reasonPhrase'); + } else { + _data['reasonPhrase'] = value; + } } /// Whether the status code was one of the normal redirect codes. set isRedirect(bool value) { + _checkAndUpdate(); _data['isRedirect'] = value; - _updated(); } /// The persistent connection state returned by the server. set persistentConnection(bool value) { + _checkAndUpdate(); _data['persistentConnection'] = value; - _updated(); } /// The content length of the response body, in bytes. - set contentLength(int value) { - _data['contentLength'] = value; - _updated(); + set contentLength(int? value) { + _checkAndUpdate(); + if (value == null) { + _data.remove('contentLength'); + } else { + _data['contentLength'] = value; + } } set statusCode(int value) { + _checkAndUpdate(); _data['statusCode'] = value; - _updated(); } /// The time at which the initial response was received. set startTime(DateTime value) { + _checkAndUpdate(); _data['startTime'] = value.microsecondsSinceEpoch; - _updated(); - } - - /// The time at which the response was completed. Note that DevTools will not - /// consider the request to be complete until [endTime] is non-null. - set endTime(DateTime value) { - _data['endTime'] = value.microsecondsSinceEpoch; - _updated(); - } - - /// The error associated with a failed request. - set error(String value) { - _data['error'] = value; - _updated(); } HttpProfileResponseData._( @@ -269,6 +404,46 @@ final class HttpProfileResponseData { ) { _data['redirects'] = >[]; } + + void _checkAndUpdate() { + if (_isClosed) { + throw StateError('HttpProfileResponseData has been closed, no further ' + 'updates are allowed'); + } + _updated(); + } + + /// Signal that the response, including the entire response body, has been + /// received. + /// + /// [bodySink] will be closed and the fields of [HttpProfileResponseData] will + /// no longer be changeable. + /// + /// [endTime] is the time when the response was fully received. It defaults + /// to the current time. + void close([DateTime? endTime]) { + _checkAndUpdate(); + _isClosed = true; + bodySink.close(); + _data['endTime'] = (endTime ?? DateTime.now()).microsecondsSinceEpoch; + } + + /// Signal that receiving the response has failed with an error. + /// + /// [bodySink] will be closed and the fields of [HttpProfileResponseData] will + /// no longer be changeable. + /// + /// [value] is a textual description of the error e.g. 'host does not exist'. + /// + /// [endTime] is the time when the error occurred. It defaults to the current + /// time. + void closeWithError(String value, [DateTime? endTime]) { + _checkAndUpdate(); + _isClosed = true; + bodySink.close(); + _data['error'] = value; + _data['endTime'] = (endTime ?? DateTime.now()).microsecondsSinceEpoch; + } } /// A record of debugging information about an HTTP request. @@ -306,70 +481,42 @@ final class HttpClientRequestProfile { _updated(); } - /// The time at which the request was completed. Note that DevTools will not - /// consider the request to be complete until [requestEndTimestamp] is - /// non-null. - set requestEndTimestamp(DateTime value) { - _data['requestEndTimestamp'] = value.microsecondsSinceEpoch; - _updated(); - } - /// Details about the request. late final HttpProfileRequestData requestData; - final StreamController> _requestBody = - StreamController>(); - - /// The body of the request. - StreamSink> get requestBodySink { - _updated(); - return _requestBody.sink; - } - /// Details about the response. late final HttpProfileResponseData responseData; - final StreamController> _responseBody = - StreamController>(); - - /// The body of the response. - StreamSink> get responseBodySink { - _updated(); - return _responseBody.sink; - } - void _updated() => _data['_lastUpdateTime'] = DateTime.now().microsecondsSinceEpoch; HttpClientRequestProfile._({ - required DateTime requestStartTimestamp, + required DateTime requestStartTime, required String requestMethod, required String requestUri, }) { _data['isolateId'] = Service.getIsolateId(Isolate.current)!; - _data['requestStartTimestamp'] = - requestStartTimestamp.microsecondsSinceEpoch; + _data['requestStartTimestamp'] = requestStartTime.microsecondsSinceEpoch; _data['requestMethod'] = requestMethod; _data['requestUri'] = requestUri; _data['events'] = >[]; _data['requestData'] = {}; - requestData = HttpProfileRequestData._( - _data['requestData'] as Map, _updated); + requestData = HttpProfileRequestData._(_data, _updated); _data['responseData'] = {}; responseData = HttpProfileResponseData._( _data['responseData'] as Map, _updated); - _data['_requestBodyStream'] = _requestBody.stream; - _data['_responseBodyStream'] = _responseBody.stream; + _data['_requestBodyStream'] = requestData._body.stream; + _data['_responseBodyStream'] = responseData._body.stream; // This entry is needed to support the updatedSince parameter of // ext.dart.io.getHttpProfile. - _data['_lastUpdateTime'] = DateTime.now().microsecondsSinceEpoch; + _updated(); } /// If HTTP profiling is enabled, returns an [HttpClientRequestProfile], /// otherwise returns `null`. static HttpClientRequestProfile? profile({ /// The time at which the request was initiated. - required DateTime requestStartTimestamp, + required DateTime requestStartTime, /// The HTTP request method associated with the request. required String requestMethod, @@ -383,7 +530,7 @@ final class HttpClientRequestProfile { return null; } final requestProfile = HttpClientRequestProfile._( - requestStartTimestamp: requestStartTimestamp, + requestStartTime: requestStartTime, requestMethod: requestMethod, requestUri: requestUri, ); diff --git a/pkgs/http_profile/test/close_test.dart b/pkgs/http_profile/test/close_test.dart new file mode 100644 index 0000000000..8b468611b6 --- /dev/null +++ b/pkgs/http_profile/test/close_test.dart @@ -0,0 +1,115 @@ +// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file +// 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:developer' show getHttpClientProfilingData; + +import 'package:http_profile/http_profile.dart'; +import 'package:test/test.dart'; + +void main() { + late HttpClientRequestProfile profile; + late Map backingMap; + + setUp(() { + HttpClientRequestProfile.profilingEnabled = true; + + profile = HttpClientRequestProfile.profile( + requestStartTime: DateTime.parse('2024-03-21'), + requestMethod: 'GET', + requestUri: 'https://www.example.com', + )!; + + final profileBackingMaps = getHttpClientProfilingData(); + expect(profileBackingMaps.length, isPositive); + backingMap = profileBackingMaps.lastOrNull!; + }); + + group('requestData.close', () { + test('no arguments', () async { + expect(backingMap['requestEndTimestamp'], isNull); + profile.requestData.close(); + + expect( + backingMap['requestEndTimestamp'], + closeTo(DateTime.now().microsecondsSinceEpoch, + Duration.microsecondsPerSecond), + ); + }); + + test('with time', () async { + expect(backingMap['requestEndTimestamp'], isNull); + profile.requestData.close(DateTime.parse('2024-03-23')); + + expect( + backingMap['requestEndTimestamp'], + DateTime.parse('2024-03-23').microsecondsSinceEpoch, + ); + }); + + test('then write body', () async { + profile.requestData.close(); + + expect( + () => profile.requestData.bodySink.add([1, 2, 3]), + throwsStateError, + ); + }); + + test('then mutate', () async { + profile.requestData.close(); + + expect( + () => profile.requestData.contentLength = 5, + throwsStateError, + ); + }); + }); + + group('responseData.close', () { + late Map responseData; + + setUp(() { + responseData = backingMap['responseData'] as Map; + }); + + test('no arguments', () async { + expect(responseData['endTime'], isNull); + profile.responseData.close(); + + expect( + responseData['endTime'], + closeTo(DateTime.now().microsecondsSinceEpoch, + Duration.microsecondsPerSecond), + ); + }); + + test('with time', () async { + expect(responseData['endTime'], isNull); + profile.responseData.close(DateTime.parse('2024-03-23')); + + expect( + responseData['endTime'], + DateTime.parse('2024-03-23').microsecondsSinceEpoch, + ); + }); + + test('then write body', () async { + profile.responseData.close(); + + expect( + () => profile.responseData.bodySink.add([1, 2, 3]), + throwsStateError, + ); + }); + + test('then mutate', () async { + profile.responseData.close(); + + expect( + () => profile.responseData.contentLength = 5, + throwsStateError, + ); + }); + }); +} diff --git a/pkgs/http_profile/test/populating_profiles_test.dart b/pkgs/http_profile/test/populating_profiles_test.dart index a1cbbb4b30..63a92f1f25 100644 --- a/pkgs/http_profile/test/populating_profiles_test.dart +++ b/pkgs/http_profile/test/populating_profiles_test.dart @@ -18,7 +18,7 @@ void main() { HttpClientRequestProfile.profilingEnabled = true; profile = HttpClientRequestProfile.profile( - requestStartTimestamp: DateTime.parse('2024-03-21'), + requestStartTime: DateTime.parse('2024-03-21'), requestMethod: 'GET', requestUri: 'https://www.example.com', )!; @@ -61,8 +61,7 @@ void main() { test('populating HttpClientRequestProfile.requestEndTimestamp', () async { expect(backingMap['requestEndTimestamp'], isNull); - - profile.requestEndTimestamp = DateTime.parse('2024-03-23'); + profile.requestData.close(DateTime.parse('2024-03-23')); expect( backingMap['requestEndTimestamp'], @@ -98,26 +97,21 @@ void main() { expect(requestData['contentLength'], 1200); }); - test('populating HttpClientRequestProfile.requestData.cookies', () async { + test('HttpClientRequestProfile.requestData.contentLength = nil', () async { final requestData = backingMap['requestData'] as Map; - expect(requestData['cookies'], isNull); - profile.requestData.cookies = [ - 'sessionId=abc123', - 'csrftoken=def456', - ]; + profile.requestData.contentLength = 1200; + expect(requestData['contentLength'], 1200); - final cookies = requestData['cookies'] as List; - expect(cookies.length, 2); - expect(cookies[0], 'sessionId=abc123'); - expect(cookies[1], 'csrftoken=def456'); + profile.requestData.contentLength = null; + expect(requestData['contentLength'], isNull); }); test('populating HttpClientRequestProfile.requestData.error', () async { final requestData = backingMap['requestData'] as Map; expect(requestData['error'], isNull); - profile.requestData.error = 'failed'; + profile.requestData.closeWithError('failed'); expect(requestData['error'], 'failed'); }); @@ -132,17 +126,42 @@ void main() { expect(requestData['followRedirects'], true); }); - test('populating HttpClientRequestProfile.requestData.headers', () async { + test('populating HttpClientRequestProfile.requestData.headersListValues', + () async { final requestData = backingMap['requestData'] as Map; expect(requestData['headers'], isNull); - profile.requestData.headers = { + profile.requestData.headersListValues = { + 'fruit': ['apple', 'banana', 'grape'], 'content-length': ['0'], }; final headers = requestData['headers'] as Map>; - expect(headers['content-length']!.length, 1); - expect(headers['content-length']![0], '0'); + expect(headers, { + 'fruit': ['apple', 'banana', 'grape'], + 'content-length': ['0'], + }); + }); + + test('populating HttpClientRequestProfile.requestData.headersCommaValues', + () async { + final requestData = backingMap['requestData'] as Map; + expect(requestData['headers'], isNull); + + profile.requestData.headersCommaValues = { + 'set-cookie': + // ignore: missing_whitespace_between_adjacent_strings + 'id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/,,HE,=L=LO,' + 'sessionId=e8bb43229de9; Domain=foo.example.com' + }; + + final headers = requestData['headers'] as Map>; + expect(headers, { + 'set-cookie': [ + 'id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/,,HE,=L=LO', + 'sessionId=e8bb43229de9; Domain=foo.example.com' + ] + }); }); test('populating HttpClientRequestProfile.requestData.maxRedirects', @@ -205,21 +224,6 @@ void main() { expect(redirect['location'], 'https://images.example.com/1'); }); - test('populating HttpClientRequestProfile.responseData.cookies', () async { - final responseData = backingMap['responseData'] as Map; - expect(responseData['cookies'], isNull); - - profile.responseData.cookies = [ - 'sessionId=abc123', - 'id=def456; Max-Age=2592000; Domain=example.com', - ]; - - final cookies = responseData['cookies'] as List; - expect(cookies.length, 2); - expect(cookies[0], 'sessionId=abc123'); - expect(cookies[1], 'id=def456; Max-Age=2592000; Domain=example.com'); - }); - test('populating HttpClientRequestProfile.responseData.connectionInfo', () async { final responseData = backingMap['responseData'] as Map; @@ -238,24 +242,44 @@ void main() { expect(connectionInfo['connectionPoolId'], '21x23'); }); - test('populating HttpClientRequestProfile.responseData.headers', () async { + test('populating HttpClientRequestProfile.responseData.headersListValues', + () async { final responseData = backingMap['responseData'] as Map; expect(responseData['headers'], isNull); - profile.responseData.headers = { + profile.responseData.headersListValues = { 'connection': ['keep-alive'], 'cache-control': ['max-age=43200'], 'content-type': ['application/json', 'charset=utf-8'], }; final headers = responseData['headers'] as Map>; - expect(headers['connection']!.length, 1); - expect(headers['connection']![0], 'keep-alive'); - expect(headers['cache-control']!.length, 1); - expect(headers['cache-control']![0], 'max-age=43200'); - expect(headers['content-type']!.length, 2); - expect(headers['content-type']![0], 'application/json'); - expect(headers['content-type']![1], 'charset=utf-8'); + expect(headers, { + 'connection': ['keep-alive'], + 'cache-control': ['max-age=43200'], + 'content-type': ['application/json', 'charset=utf-8'], + }); + }); + + test('populating HttpClientRequestProfile.responseData.headersCommaValues', + () async { + final responseData = backingMap['responseData'] as Map; + expect(responseData['headers'], isNull); + + profile.responseData.headersCommaValues = { + 'set-cookie': + // ignore: missing_whitespace_between_adjacent_strings + 'id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/,,HE,=L=LO,' + 'sessionId=e8bb43229de9; Domain=foo.example.com' + }; + + final headers = responseData['headers'] as Map>; + expect(headers, { + 'set-cookie': [ + 'id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/,,HE,=L=LO', + 'sessionId=e8bb43229de9; Domain=foo.example.com' + ] + }); }); test('populating HttpClientRequestProfile.responseData.compressionState', @@ -308,6 +332,15 @@ void main() { expect(responseData['contentLength'], 1200); }); + test('HttpClientRequestProfile.responseData.contentLength = nil', () async { + final responseData = backingMap['responseData'] as Map; + profile.responseData.contentLength = 1200; + expect(responseData['contentLength'], 1200); + + profile.responseData.contentLength = null; + expect(responseData['contentLength'], isNull); + }); + test('populating HttpClientRequestProfile.responseData.statusCode', () async { final responseData = backingMap['responseData'] as Map; expect(responseData['statusCode'], isNull); @@ -333,7 +366,7 @@ void main() { final responseData = backingMap['responseData'] as Map; expect(responseData['endTime'], isNull); - profile.responseData.endTime = DateTime.parse('2024-03-23'); + profile.responseData.close(DateTime.parse('2024-03-23')); expect( responseData['endTime'], @@ -345,7 +378,7 @@ void main() { final responseData = backingMap['responseData'] as Map; expect(responseData['error'], isNull); - profile.responseData.error = 'failed'; + profile.responseData.closeWithError('failed'); expect(responseData['error'], 'failed'); }); @@ -354,33 +387,19 @@ void main() { final requestBodyStream = backingMap['_requestBodyStream'] as Stream>; - profile.requestBodySink.add([1, 2, 3]); - - await Future.wait([ - Future.sync( - () async => expect( - await requestBodyStream.expand((i) => i).toList(), - [1, 2, 3], - ), - ), - profile.requestBodySink.close(), - ]); + 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 requestBodyStream = + final responseBodyStream = backingMap['_responseBodyStream'] as Stream>; - profile.responseBodySink.add([1, 2, 3]); - - await Future.wait([ - Future.sync( - () async => expect( - await requestBodyStream.expand((i) => i).toList(), - [1, 2, 3], - ), - ), - profile.responseBodySink.close(), - ]); + profile.responseData.bodySink.add([1, 2, 3]); + profile.responseData.close(); + + expect(await responseBodyStream.expand((i) => i).toList(), [1, 2, 3]); }); } diff --git a/pkgs/http_profile/test/profiling_enabled_test.dart b/pkgs/http_profile/test/profiling_enabled_test.dart index 3062c79719..7d9b63410f 100644 --- a/pkgs/http_profile/test/profiling_enabled_test.dart +++ b/pkgs/http_profile/test/profiling_enabled_test.dart @@ -13,7 +13,7 @@ void main() { expect(HttpClient.enableTimelineLogging, true); expect( HttpClientRequestProfile.profile( - requestStartTimestamp: DateTime.parse('2024-03-21'), + requestStartTime: DateTime.parse('2024-03-21'), requestMethod: 'GET', requestUri: 'https://www.example.com', ), @@ -26,7 +26,7 @@ void main() { expect(HttpClient.enableTimelineLogging, false); expect( HttpClientRequestProfile.profile( - requestStartTimestamp: DateTime.parse('2024-03-21'), + requestStartTime: DateTime.parse('2024-03-21'), requestMethod: 'GET', requestUri: 'https://www.example.com', ), From 54f59eef41b7a0f10855cae01fd44d160ce2fbba Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Wed, 28 Feb 2024 15:11:49 -0800 Subject: [PATCH 02/10] Update pkgs/http_profile/lib/http_profile.dart Co-authored-by: Derek Xu --- pkgs/http_profile/lib/http_profile.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/http_profile/lib/http_profile.dart b/pkgs/http_profile/lib/http_profile.dart index 17023c90d0..3afa9ad453 100644 --- a/pkgs/http_profile/lib/http_profile.dart +++ b/pkgs/http_profile/lib/http_profile.dart @@ -283,7 +283,7 @@ final class HttpProfileResponseData { (_data['redirects'] as List>).add(redirect._toJson()); } - /// The body of the request. + /// The body of the response. StreamSink> get bodySink => _body.sink; /// Information about the networking connection used in the HTTP response. From cc1e02bd70a05af366a97b8f6e2df00dba2a5202 Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Wed, 28 Feb 2024 15:18:07 -0800 Subject: [PATCH 03/10] Update pkgs/http_profile/lib/http_profile.dart Co-authored-by: Derek Xu --- pkgs/http_profile/lib/http_profile.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/http_profile/lib/http_profile.dart b/pkgs/http_profile/lib/http_profile.dart index 3afa9ad453..ba6ec22ab7 100644 --- a/pkgs/http_profile/lib/http_profile.dart +++ b/pkgs/http_profile/lib/http_profile.dart @@ -12,7 +12,7 @@ import 'dart:isolate' show Isolate; const _tokenChars = r"!#$%&'*+\-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`" 'abcdefghijklmnopqrstuvwxyz|~'; -/// Splits comma-seperated header values. +/// Splits comma-separated header values. var _headerSplitter = RegExp(r'[ \t]*,[ \t]*'); /// Splits comma-seperated "Set-Cookie" header values. From 9fb53f003ef5b4b69c73cbee0f267b2904904c31 Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Wed, 28 Feb 2024 15:18:16 -0800 Subject: [PATCH 04/10] Update pkgs/http_profile/lib/http_profile.dart Co-authored-by: Derek Xu --- pkgs/http_profile/lib/http_profile.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/http_profile/lib/http_profile.dart b/pkgs/http_profile/lib/http_profile.dart index ba6ec22ab7..c28b55707f 100644 --- a/pkgs/http_profile/lib/http_profile.dart +++ b/pkgs/http_profile/lib/http_profile.dart @@ -15,7 +15,7 @@ const _tokenChars = r"!#$%&'*+\-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`" /// Splits comma-separated header values. var _headerSplitter = RegExp(r'[ \t]*,[ \t]*'); -/// Splits comma-seperated "Set-Cookie" header values. +/// Splits comma-separated "Set-Cookie" header values. /// /// Set-Cookie strings can contain commas. In particular, the following /// productions defined in RFC-6265, section 4.1.1: From a12c1c80a6c6ab3420f17da3721d33342a58df37 Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Wed, 28 Feb 2024 15:18:27 -0800 Subject: [PATCH 05/10] Update pkgs/http_profile/lib/http_profile.dart Co-authored-by: Derek Xu --- pkgs/http_profile/lib/http_profile.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/http_profile/lib/http_profile.dart b/pkgs/http_profile/lib/http_profile.dart index c28b55707f..d311f3bf8f 100644 --- a/pkgs/http_profile/lib/http_profile.dart +++ b/pkgs/http_profile/lib/http_profile.dart @@ -36,7 +36,7 @@ var _headerSplitter = RegExp(r'[ \t]*,[ \t]*'); /// See https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.1 var _setCookieSplitter = RegExp(r'[ \t]*,[ \t]*(?=[' + _tokenChars + r']+=)'); -/// Splits comma-seperated header values into a [List]. +/// Splits comma-separated header values into a [List]. /// /// Copied from `package:http`. Map> _splitHeaderValues(Map headers) { From 4ce344647046d574b7bbf7159cfbb18729921634 Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Wed, 28 Feb 2024 15:18:40 -0800 Subject: [PATCH 06/10] Update pkgs/http_profile/lib/http_profile.dart Co-authored-by: Derek Xu --- pkgs/http_profile/lib/http_profile.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/http_profile/lib/http_profile.dart b/pkgs/http_profile/lib/http_profile.dart index d311f3bf8f..f90f00cc51 100644 --- a/pkgs/http_profile/lib/http_profile.dart +++ b/pkgs/http_profile/lib/http_profile.dart @@ -184,7 +184,7 @@ final class HttpProfileRequestData { _requestData['headers'] = {...value}; } - /// The request headers where duplicate headers are represented using + /// The request headers where duplicate headers are represented using a /// comma-seperated list of values. /// /// For example: From e2a89d4325ddb19382972ce58f30b02b3a0ea181 Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Wed, 28 Feb 2024 15:18:50 -0800 Subject: [PATCH 07/10] Update pkgs/http_profile/lib/http_profile.dart Co-authored-by: Derek Xu --- pkgs/http_profile/lib/http_profile.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/http_profile/lib/http_profile.dart b/pkgs/http_profile/lib/http_profile.dart index f90f00cc51..0255e9a984 100644 --- a/pkgs/http_profile/lib/http_profile.dart +++ b/pkgs/http_profile/lib/http_profile.dart @@ -185,7 +185,7 @@ final class HttpProfileRequestData { } /// The request headers where duplicate headers are represented using a - /// comma-seperated list of values. + /// comma-separated list of values. /// /// For example: /// From 2f46671505d6131484bfe64334eab9d3ab59bd90 Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Wed, 28 Feb 2024 15:21:49 -0800 Subject: [PATCH 08/10] Update pkgs/http_profile/lib/http_profile.dart Co-authored-by: Derek Xu --- pkgs/http_profile/lib/http_profile.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/http_profile/lib/http_profile.dart b/pkgs/http_profile/lib/http_profile.dart index 0255e9a984..a78fd6ce32 100644 --- a/pkgs/http_profile/lib/http_profile.dart +++ b/pkgs/http_profile/lib/http_profile.dart @@ -325,7 +325,7 @@ final class HttpProfileResponseData { _data['headers'] = {...value}; } - /// The response headers where duplicate headers are represented using + /// The response headers where duplicate headers are represented using a /// comma-seperated list of values. /// /// For example: From edd70bcfd49ccee4f1672ade0d2809783b1f0f87 Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Wed, 28 Feb 2024 15:22:11 -0800 Subject: [PATCH 09/10] Update pkgs/http_profile/lib/http_profile.dart Co-authored-by: Derek Xu --- pkgs/http_profile/lib/http_profile.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/http_profile/lib/http_profile.dart b/pkgs/http_profile/lib/http_profile.dart index a78fd6ce32..4f9be4b6cc 100644 --- a/pkgs/http_profile/lib/http_profile.dart +++ b/pkgs/http_profile/lib/http_profile.dart @@ -326,7 +326,7 @@ final class HttpProfileResponseData { } /// The response headers where duplicate headers are represented using a - /// comma-seperated list of values. + /// comma-separated list of values. /// /// For example: /// From 068ab5c759a0639db934941dab6b9cd1fd637126 Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Wed, 28 Feb 2024 15:23:13 -0800 Subject: [PATCH 10/10] Unawaited --- pkgs/http_profile/lib/http_profile.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pkgs/http_profile/lib/http_profile.dart b/pkgs/http_profile/lib/http_profile.dart index 4f9be4b6cc..768d161853 100644 --- a/pkgs/http_profile/lib/http_profile.dart +++ b/pkgs/http_profile/lib/http_profile.dart @@ -2,7 +2,7 @@ // 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' show StreamController, StreamSink; +import 'dart:async' show StreamController, StreamSink, unawaited; import 'dart:developer' show Service, addHttpClientProfilingData; import 'dart:io' show HttpClient, HttpClientResponseCompressionState; import 'dart:isolate' show Isolate; @@ -246,7 +246,7 @@ final class HttpProfileRequestData { void close([DateTime? endTime]) { _checkAndUpdate(); _isClosed = true; - bodySink.close(); + unawaited(bodySink.close()); _data['requestEndTimestamp'] = (endTime ?? DateTime.now()).microsecondsSinceEpoch; } @@ -263,7 +263,7 @@ final class HttpProfileRequestData { void closeWithError(String value, [DateTime? endTime]) { _checkAndUpdate(); _isClosed = true; - bodySink.close(); + unawaited(bodySink.close()); _requestData['error'] = value; _data['requestEndTimestamp'] = (endTime ?? DateTime.now()).microsecondsSinceEpoch; @@ -424,7 +424,7 @@ final class HttpProfileResponseData { void close([DateTime? endTime]) { _checkAndUpdate(); _isClosed = true; - bodySink.close(); + unawaited(bodySink.close()); _data['endTime'] = (endTime ?? DateTime.now()).microsecondsSinceEpoch; } @@ -440,7 +440,7 @@ final class HttpProfileResponseData { void closeWithError(String value, [DateTime? endTime]) { _checkAndUpdate(); _isClosed = true; - bodySink.close(); + unawaited(bodySink.close()); _data['error'] = value; _data['endTime'] = (endTime ?? DateTime.now()).microsecondsSinceEpoch; }