Skip to content

Commit fd3eaa6

Browse files
srawlinsCommit Queue
authored andcommitted
_http: ensure _HttpProfileData.startResponse is called before finishResponse
Fixes flutter/devtools#8509 From my research into the bug: > I see indeed that `_HttpProfileData.finishResponse()` is called before `_HttpProfileData.startResponse()`. I think this is just a result of microtask scheduling; when the response has no body, `_incoming.dataDone` completes before `_httpRequest._responseCompleter`. > Since `startResponse` is responsible for setting `responseInProgress` to `true`, the call to `finishResponse` short circuit returns immediately. We therefore don't finish the response timeline, or log the end time, etc. The fix here does not rely on microtask organization, which I think would be fragile. Instead, we don't listen to the `dataDone` future until we've logged the start of the response. Change-Id: Ib735742b297a261e6566ae8caf9eac0d71fc8fdb Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/415140 Reviewed-by: Ben Konyi <[email protected]> Commit-Queue: Samuel Rawlins <[email protected]>
1 parent a9ea504 commit fd3eaa6

File tree

2 files changed

+91
-14
lines changed

2 files changed

+91
-14
lines changed

pkg/vm_service/test/get_http_profile_test.dart

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -248,8 +248,8 @@ Future<void> hasValidHttpRequests(HttpProfile profile, String method) async {
248248
} else {
249249
// write() was used.
250250
expect(
251-
utf8.decode(fullRequest.requestBody!).startsWith('$method http'),
252-
true,
251+
utf8.decode(fullRequest.requestBody!),
252+
startsWith('$method http'),
253253
);
254254
}
255255
}
@@ -258,9 +258,15 @@ Future<void> hasValidHttpRequests(HttpProfile profile, String method) async {
258258
final responseData = r.response!;
259259
expect(responseData.statusCode, greaterThanOrEqualTo(100));
260260
expect(responseData.endTime, isNotNull);
261-
expect(responseData.startTime!.isAfter(r.endTime!), true);
262-
expect(responseData.startTime!.isBefore(responseData.endTime!), true);
263-
expect(utf8.decode(fullRequest.responseBody!), method);
261+
expect(responseData.startTime!, _isAfterOrSameAs(r.endTime!));
262+
expect(
263+
responseData.startTime!,
264+
_isBeforeOrSameAs(responseData.endTime!),
265+
);
266+
if (method != 'HEAD') {
267+
// The HEAD response has no body.
268+
expect(utf8.decode(fullRequest.responseBody!), method);
269+
}
264270
responseData.headers;
265271
responseData.compressionState;
266272
responseData.connectionInfo;
@@ -279,7 +285,7 @@ Future<void> hasValidHttpRequests(HttpProfile profile, String method) async {
279285
}
280286

281287
void hasValidHttpProfile(HttpProfile profile, String method) {
282-
expect(profile.requests.where((e) => e.method == method).length, 10);
288+
expect(profile.requests.where((e) => e.method == method), hasLength(10));
283289
}
284290

285291
Future<void> hasValidHttpCONNECTs(HttpProfile profile) =>
@@ -329,7 +335,7 @@ final tests = <IsolateTest>[
329335
final isolateId = isolateRef.id!;
330336

331337
final httpProfile = await service.getHttpProfile(isolateId);
332-
expect(httpProfile.requests.length, 70);
338+
expect(httpProfile.requests, hasLength(70));
333339

334340
// Verify timeline events.
335341
await hasValidHttpCONNECTs(httpProfile);
@@ -350,3 +356,56 @@ void main([args = const <String>[]]) => runIsolateTests(
350356
'get_http_profile_test.dart',
351357
testeeBefore: testMain,
352358
);
359+
360+
class _DateTimeMatcher extends Matcher {
361+
const _DateTimeMatcher(this._expected, this._condition);
362+
363+
final DateTime _expected;
364+
365+
final _DateTimeCondition _condition;
366+
367+
@override
368+
Description describe(Description description) {
369+
return description.add('DateTime ${_condition.name} $_expected');
370+
}
371+
372+
@override
373+
Description describeMismatch(
374+
Object? item,
375+
Description mismatchDescription,
376+
Map matchState,
377+
bool verbose,
378+
) {
379+
if (item is! DateTime) return mismatchDescription;
380+
final difference = _condition == _DateTimeCondition.before
381+
? item.difference(_expected)
382+
: _expected.difference(item);
383+
final actualRelation =
384+
_condition == _DateTimeCondition.before ? 'after' : 'before';
385+
return mismatchDescription
386+
.add('is ${difference.inMicroseconds} microseconds $actualRelation');
387+
}
388+
389+
@override
390+
bool matches(Object? item, Map matchState) {
391+
if (item is! DateTime) {
392+
return false;
393+
}
394+
return switch (_condition) {
395+
_DateTimeCondition.before =>
396+
item.isBefore(_expected) || item.isAtSameMomentAs(_expected),
397+
_DateTimeCondition.after =>
398+
item.isAfter(_expected) || item.isAtSameMomentAs(_expected),
399+
};
400+
}
401+
}
402+
403+
enum _DateTimeCondition {
404+
before,
405+
after;
406+
}
407+
408+
_DateTimeMatcher _isBeforeOrSameAs(DateTime item) =>
409+
_DateTimeMatcher(item, _DateTimeCondition.before);
410+
_DateTimeMatcher _isAfterOrSameAs(DateTime item) =>
411+
_DateTimeMatcher(item, _DateTimeCondition.after);

sdk/lib/_http/http_impl.dart

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ class _HttpProfileData {
6969
_updated();
7070
}
7171

72+
/// Adds an HTTP request event to the timeline and the list of events.
7273
void requestEvent(String name, {Map? arguments}) {
7374
_timeline.instant(name, arguments: arguments);
7475
requestEvents.add(_HttpProfileEvent(name, arguments));
@@ -131,6 +132,7 @@ class _HttpProfileData {
131132
_updated();
132133
}
133134

135+
/// Marks the response as "started."
134136
void startResponse({required HttpClientResponse response}) {
135137
responseDetails = <String, dynamic>{
136138
'headers': formatHeaders(response.headers),
@@ -175,6 +177,7 @@ class _HttpProfileData {
175177
_updated();
176178
}
177179

180+
/// Marks the response as "finished."
178181
void finishResponse() {
179182
// Guard against the response being completed more than once or being
180183
// completed before the response actually finished starting.
@@ -186,9 +189,10 @@ class _HttpProfileData {
186189
_updated();
187190
}
188191

192+
/// Marks the response as "finished" with an error.
189193
void finishResponseWithError(String error) {
190-
// Return if finishResponseWithError has already been called. Can happen if
191-
// the response stream is listened to with `cancelOnError: false`.
194+
// Return if `finishResponseWithError` has already been called. Can happen
195+
// if the response stream is listened to with `cancelOnError: false`.
192196
if (!responseInProgress!) return;
193197
responseInProgress = false;
194198
responseEndTimestamp = DateTime.now().microsecondsSinceEpoch;
@@ -239,6 +243,13 @@ class _HttpProfileData {
239243
static final String isolateId = Service.getIsolateId(Isolate.current)!;
240244

241245
bool requestInProgress = true;
246+
247+
/// Whether the response-processing has started, and has not yet finished.
248+
///
249+
/// This field has three meaningful states:
250+
/// * `null`: processing the response has not started.
251+
/// * `true`: processing the response has started.
252+
/// * `false`: processing the response has finished.
242253
bool? responseInProgress;
243254

244255
late final String id;
@@ -609,9 +620,6 @@ class _HttpClientResponse extends _HttpInboundMessageListInt
609620
super(_incoming) {
610621
// Set uri for potential exceptions.
611622
_incoming.uri = _httpRequest.uri;
612-
// Ensure the response profile is completed, even if the response stream is
613-
// never actually listened to.
614-
_incoming.dataDone.then((_) => _profileData?.finishResponse());
615623
}
616624

617625
static HttpClientResponseCompressionState _getCompressionState(
@@ -1482,6 +1490,8 @@ class _HttpClientRequest extends _HttpOutboundMessage<HttpClientResponse>
14821490
final _HttpClient _httpClient;
14831491
final _HttpClientConnection _httpClientConnection;
14841492

1493+
/// A [Completer] that completes when the response has been fully received,
1494+
/// including any redirects and authentications.
14851495
final Completer<HttpClientResponse> _responseCompleter =
14861496
Completer<HttpClientResponse>();
14871497

@@ -1522,6 +1532,11 @@ class _HttpClientRequest extends _HttpOutboundMessage<HttpClientResponse>
15221532
// 'certificate': response.certificate,
15231533
response: response,
15241534
);
1535+
(response as _HttpClientResponse)._incoming.dataDone.then(
1536+
// Ensure the response profile is finished, even if the response stream
1537+
// is never listened to.
1538+
(_) => _profileData?.finishResponse(),
1539+
);
15251540
}, onError: (e) {});
15261541
}
15271542

@@ -1554,7 +1569,10 @@ class _HttpClientRequest extends _HttpOutboundMessage<HttpClientResponse>
15541569
HttpConnectionInfo? get connectionInfo =>
15551570
_httpClientConnection.connectionInfo;
15561571

1557-
void _onIncoming(_HttpIncoming incoming) {
1572+
/// Handles [incoming] by possibly following redirects, possibly
1573+
/// authenticating, and completing [_responseCompleter] when the response is
1574+
/// complete.
1575+
void _handleIncoming(_HttpIncoming incoming) {
15581576
if (_aborted) {
15591577
return;
15601578
}
@@ -2396,7 +2414,7 @@ class _HttpClientConnection {
23962414
if (nextnonce != null) creds.nonce = nextnonce;
23972415
}
23982416
}
2399-
request._onIncoming(incoming);
2417+
request._handleIncoming(incoming);
24002418
})
24012419
// If we see a state error, we failed to get the 'first'
24022420
// element.

0 commit comments

Comments
 (0)