Unverified Commit b0215078 authored by Anurag Roy's avatar Anurag Roy Committed by GitHub

[flutter_tools] Add timeout duration to error and handle exceptions for HttpHostValidator. (#98290)

parent 67f25caf
...@@ -32,25 +32,23 @@ List<String> androidRequiredHttpHosts(Platform platform) { ...@@ -32,25 +32,23 @@ List<String> androidRequiredHttpHosts(Platform platform) {
]; ];
} }
// Validator that checks all provided hosts are reachable and responsive // Validator that checks all provided hosts are reachable and responsive
class HttpHostValidator extends DoctorValidator { class HttpHostValidator extends DoctorValidator {
HttpHostValidator( HttpHostValidator({
{required Platform platform, required Platform platform,
required FeatureFlags featureFlags, required FeatureFlags featureFlags,
required HttpClient httpClient}) required HttpClient httpClient,
: _platform = platform, }) : _platform = platform,
_featureFlags = featureFlags, _featureFlags = featureFlags,
_httpClient = httpClient, _httpClient = httpClient,
super('HTTP Host Availability'); super('HTTP Host Availability');
final Platform _platform; final Platform _platform;
final FeatureFlags _featureFlags; final FeatureFlags _featureFlags;
final HttpClient _httpClient; final HttpClient _httpClient;
@override @override
String get slowWarning => String get slowWarning => 'HTTP Host availability check is taking a long time...';
'HTTP Host availability check is taking a long time...';
List<String> get _requiredHosts => <String>[ List<String> get _requiredHosts => <String>[
if (_featureFlags.isMacOSEnabled) ...macOSRequiredHttpHosts, if (_featureFlags.isMacOSEnabled) ...macOSRequiredHttpHosts,
...@@ -59,71 +57,83 @@ class HttpHostValidator extends DoctorValidator { ...@@ -59,71 +57,83 @@ class HttpHostValidator extends DoctorValidator {
_platform.environment[kEnvCloudUrl] ?? kgCloudHttpHost, _platform.environment[kEnvCloudUrl] ?? kgCloudHttpHost,
]; ];
/// Make a head request to the HTTP host. HTTP Host is available if no exception happened /// Make a head request to the HTTP host for checking availability
Future<_HostValidationResult> _checkHostAvailability(String host) async { Future<_HostValidationResult> _checkHostAvailability(String host) async {
late final int timeout;
try { try {
final int timeout = timeout = int.parse(_platform.environment[kDoctorHostTimeout] ?? '10');
int.parse(_platform.environment[kDoctorHostTimeout] ?? '10');
final HttpClientRequest req = await _httpClient.headUrl(Uri.parse(host)); final HttpClientRequest req = await _httpClient.headUrl(Uri.parse(host));
await req.close().timeout(Duration(seconds: timeout)); await req.close().timeout(Duration(seconds: timeout));
// HTTP Host is available if no exception happened // HTTP host is available if no exception happened
return _HostValidationResult.success(host); return _HostValidationResult.success(host);
} on TimeoutException { } on TimeoutException {
return _HostValidationResult.fail( return _HostValidationResult.fail(host, 'Failed to connect to host in $timeout second${timeout == 1 ? '': 's'}');
host, 'Failed to connect to $host in seconds');
} on SocketException catch (e) { } on SocketException catch (e) {
return _HostValidationResult.fail( return _HostValidationResult.fail(host, 'An error occurred while checking the HTTP host: ${e.message}');
host, 'An error occurred while checking the HTTP host: ${e.message}');
} on HttpException catch (e) { } on HttpException catch (e) {
return _HostValidationResult.fail(host, return _HostValidationResult.fail(host, 'An error occurred while checking the HTTP host: ${e.message}');
'An error occurred while checking the HTTP host: ${e.toString()}');
} on OSError catch (e) { } on OSError catch (e) {
return _HostValidationResult.fail( return _HostValidationResult.fail(host, 'An error occurred while checking the HTTP host: ${e.message}');
host, 'An error occurred while checking the HTTP host: ${e.message}'); } on FormatException catch (e) {
if (e.message.contains('Invalid radix-10 number')) {
return _HostValidationResult.fail(host, 'The value of $kDoctorHostTimeout(${_platform.environment[kDoctorHostTimeout]}) is not a valid duration in seconds');
} else if (e.message.contains('Invalid empty scheme')){
// Check if the invalid host is kEnvPubHostedUrl, else it must be kEnvCloudUrl
final String? pubHostedUrl = _platform.environment[kEnvPubHostedUrl];
if (pubHostedUrl != null && host == pubHostedUrl) {
return _HostValidationResult.fail(host, 'The value of $kEnvPubHostedUrl(${_platform.environment[kEnvPubHostedUrl]}) could not be parsed as a valid url');
}
return _HostValidationResult.fail(host, 'The value of $kEnvCloudUrl(${_platform.environment[kEnvCloudUrl]}) could not be parsed as a valid url');
}
return _HostValidationResult.fail(host, 'An error occurred while checking the HTTP host: ${e.message}');
} on ArgumentError catch (e) {
final String exceptionMessage = e.message.toString();
if (exceptionMessage.contains('No host specified')) {
// Check if the invalid host is kEnvPubHostedUrl, else it must be kEnvCloudUrl
final String? pubHostedUrl = _platform.environment[kEnvPubHostedUrl];
if (pubHostedUrl != null && host == pubHostedUrl) {
return _HostValidationResult.fail(host, 'The value of $kEnvPubHostedUrl(${_platform.environment[kEnvPubHostedUrl]}) is not a valid host');
}
return _HostValidationResult.fail(host, 'The value of $kEnvCloudUrl(${_platform.environment[kEnvCloudUrl]}) is not a valid host');
}
return _HostValidationResult.fail(host, 'An error occurred while checking the HTTP host: $exceptionMessage');
} }
} }
@override @override
Future<ValidationResult> validate() async { Future<ValidationResult> validate() async {
final List<ValidationMessage> messages = <ValidationMessage>[]; final List<ValidationMessage> messages = <ValidationMessage>[];
final Iterable<Future<_HostValidationResult>> availabilityResultFutures = final Iterable<Future<_HostValidationResult>> availabilityResultFutures = _requiredHosts.map(_checkHostAvailability);
_requiredHosts.map(_checkHostAvailability); final List<_HostValidationResult> availabilityResults = await Future.wait(availabilityResultFutures);
final List<_HostValidationResult> availabilityResults =
(await Future.wait(availabilityResultFutures)).toList();
if (availabilityResults if (availabilityResults.every((_HostValidationResult result) => result.available)) {
.every((_HostValidationResult result) => result.available)) {
return ValidationResult( return ValidationResult(
ValidationType.installed, ValidationType.installed,
messages messages..add(const ValidationMessage('All required HTTP hosts are available')),
..add(const ValidationMessage( );
'All required HTTP hosts are available')));
} }
availabilityResults availabilityResults.removeWhere((_HostValidationResult result) => result.available);
.removeWhere((_HostValidationResult result) => result.available);
for (final _HostValidationResult result in availabilityResults) { for (final _HostValidationResult result in availabilityResults) {
messages.add(ValidationMessage.error( messages.add(ValidationMessage.error('HTTP host "${result.host}" is not reachable. Reason: ${result.failResultInfo}'));
'HTTP host ${result.host} is not reachable. Reason: ${result.failResultInfo}'));
} }
return ValidationResult( return ValidationResult(
availabilityResults.length == _requiredHosts.length availabilityResults.length == _requiredHosts.length
? ValidationType.notAvailable ? ValidationType.notAvailable
: ValidationType.partial, : ValidationType.partial,
messages); messages,
);
} }
} }
class _HostValidationResult { class _HostValidationResult {
_HostValidationResult.success(this.host) _HostValidationResult.success(this.host)
: failResultInfo = '', : failResultInfo = '',
available = true; available = true;
_HostValidationResult.fail(this.host, this.failResultInfo) _HostValidationResult.fail(this.host, this.failResultInfo) : available = false;
: available = false;
final String failResultInfo; final String failResultInfo;
final String host; final String host;
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/base/platform.dart';
...@@ -63,35 +64,12 @@ void main() { ...@@ -63,35 +64,12 @@ void main() {
// Run the validation check and get the results // Run the validation check and get the results
final ValidationResult result = await httpHostValidator.validate(); final ValidationResult result = await httpHostValidator.validate();
// Check for a ValidationType.installed result // Check for a ValidationType.notAvailable result
expect(result.type, equals(ValidationType.notAvailable)); expect(result.type, equals(ValidationType.notAvailable));
} }
}); });
testWithoutContext('one http hosts are not available', () async { testWithoutContext('one http host is not available', () async {
// Run the check for all operating systems one by one
for(final String os in osTested) {
final Platform platform = FakePlatform(operatingSystem: os);
final HttpHostValidator httpHostValidator = HttpHostValidator(
platform: platform,
featureFlags: TestFeatureFlags(),
httpClient: FakeHttpClient.list(<FakeRequest>[
FakeRequest(Uri.parse(kgCloudHttpHost), method: HttpMethod.head, responseError: const OSError('Name or service not known', -2)),
FakeRequest(Uri.parse(androidRequiredHttpHosts(platform)[0]), method: HttpMethod.head),
FakeRequest(Uri.parse(kPubDevHttpHost), method: HttpMethod.head),
FakeRequest(Uri.parse(macOSRequiredHttpHosts[0]), method: HttpMethod.head),
]),
);
// Run the validation check and get the results
final ValidationResult result = await httpHostValidator.validate();
// Check for a ValidationType.installed result
expect(result.type, equals(ValidationType.partial));
}
});
testWithoutContext('one http hosts are not available', () async {
// Run the check for all operating systems one by one // Run the check for all operating systems one by one
for(final String os in osTested) { for(final String os in osTested) {
final Platform platform = FakePlatform(operatingSystem: os); final Platform platform = FakePlatform(operatingSystem: os);
...@@ -109,7 +87,7 @@ void main() { ...@@ -109,7 +87,7 @@ void main() {
// Run the validation check and get the results // Run the validation check and get the results
final ValidationResult result = await httpHostValidator.validate(); final ValidationResult result = await httpHostValidator.validate();
// Check for a ValidationType.installed result // Check for a ValidationType.partial result
expect(result.type, equals(ValidationType.partial)); expect(result.type, equals(ValidationType.partial));
} }
}); });
...@@ -152,12 +130,12 @@ void main() { ...@@ -152,12 +130,12 @@ void main() {
// Run the validation check and get the results // Run the validation check and get the results
final ValidationResult result = await httpHostValidator.validate(); final ValidationResult result = await httpHostValidator.validate();
// Check for a ValidationType.installed result // Check for a ValidationType.notAvailable result
expect(result.type, equals(ValidationType.notAvailable)); expect(result.type, equals(ValidationType.notAvailable));
} }
}); });
testWithoutContext('one http hosts are not available', () async { testWithoutContext('one http host is not available', () async {
// Run the check for all operating systems one by one // Run the check for all operating systems one by one
for(final String os in osTested) { for(final String os in osTested) {
final Platform platform = FakePlatform(operatingSystem: os, environment: kTestEnvironment); final Platform platform = FakePlatform(operatingSystem: os, environment: kTestEnvironment);
...@@ -174,31 +152,87 @@ void main() { ...@@ -174,31 +152,87 @@ void main() {
// Run the validation check and get the results // Run the validation check and get the results
final ValidationResult result = await httpHostValidator.validate(); final ValidationResult result = await httpHostValidator.validate();
// Check for a ValidationType.installed result // Check for a ValidationType.partial result
expect(result.type, equals(ValidationType.partial)); expect(result.type, equals(ValidationType.partial));
} }
}); });
testWithoutContext('one http hosts are not available', () async { testWithoutContext('does not throw on invalid user-defined timeout', () async {
// Run the check for all operating systems one by one final HttpHostValidator httpHostValidator = HttpHostValidator(
for(final String os in osTested) { platform: FakePlatform(
final Platform platform = FakePlatform(operatingSystem: os, environment: kTestEnvironment); environment: <String,String> {
final HttpHostValidator httpHostValidator = HttpHostValidator( 'PUB_HOSTED_URL': kTestEnvPubHost,
platform: platform, 'FLUTTER_STORAGE_BASE_URL': kTestEnvGCloudHost,
featureFlags: TestFeatureFlags(), 'FLUTTER_DOCTOR_HOST_TIMEOUT' : 'deadbeef',
httpClient: FakeHttpClient.list(<FakeRequest>[ },
FakeRequest(Uri.parse(kTestEnvGCloudHost), method: HttpMethod.head, responseError: const OSError('Name or service not known', -2)), ),
FakeRequest(Uri.parse(kTestEnvPubHost), method: HttpMethod.head), featureFlags: TestFeatureFlags(isAndroidEnabled: false),
FakeRequest(Uri.parse(macOSRequiredHttpHosts[0]), method: HttpMethod.head), httpClient: FakeHttpClient.any(),
]), );
);
// Run the validation check and get the results
final ValidationResult result = await httpHostValidator.validate();
expect(result.type, equals(ValidationType.notAvailable));
expect(
result.messages,
contains(const ValidationMessage.error(
'HTTP host "$kTestEnvPubHost" is not reachable. '
'Reason: The value of FLUTTER_DOCTOR_HOST_TIMEOUT(deadbeef) is not a valid duration in seconds',
)),
);
});
// Run the validation check and get the results testWithoutContext('does not throw on unparseable user-defined host uri', () async {
final ValidationResult result = await httpHostValidator.validate(); final HttpHostValidator httpHostValidator = HttpHostValidator(
platform: FakePlatform(
environment: <String,String> {
'PUB_HOSTED_URL': '::Not A Uri::',
'FLUTTER_STORAGE_BASE_URL': kTestEnvGCloudHost,
'FLUTTER_DOCTOR_HOST_TIMEOUT' : '1',
},
),
featureFlags: TestFeatureFlags(isAndroidEnabled: false),
httpClient: FakeHttpClient.any(),
);
// Run the validation check and get the results
final ValidationResult result = await httpHostValidator.validate();
expect(result.type, equals(ValidationType.partial));
expect(
result.messages,
contains(const ValidationMessage.error(
'HTTP host "::Not A Uri::" is not reachable. '
'Reason: The value of PUB_HOSTED_URL(::Not A Uri::) could not be parsed as a valid url',
)),
);
});
// Check for a ValidationType.installed result testWithoutContext('does not throw on invalid user-defined host', () async {
expect(result.type, equals(ValidationType.partial)); final HttpHostValidator httpHostValidator = HttpHostValidator(
} platform: FakePlatform(
environment: <String,String> {
'PUB_HOSTED_URL': kTestEnvPubHost,
'FLUTTER_STORAGE_BASE_URL': '',
'FLUTTER_DOCTOR_HOST_TIMEOUT' : '1',
},
),
featureFlags: TestFeatureFlags(isAndroidEnabled: false),
httpClient: FakeHttpClient.any(),
);
// Run the validation check and get the results
final ValidationResult result = await httpHostValidator.validate();
expect(result.type, equals(ValidationType.partial));
expect(
result.messages,
contains(const ValidationMessage.error(
'HTTP host "" is not reachable. '
'Reason: The value of FLUTTER_STORAGE_BASE_URL() is not a valid host',
)),
);
}); });
}); });
...@@ -267,4 +301,24 @@ void main() { ...@@ -267,4 +301,24 @@ void main() {
}); });
}); });
}); });
testWithoutContext('Http host validator timeout message includes timeout duration.', () async {
final HttpHostValidator httpHostValidator = HttpHostValidator(
platform: FakePlatform(environment: kTestEnvironment),
featureFlags: TestFeatureFlags(isAndroidEnabled: false),
httpClient: FakeHttpClient.list(<FakeRequest>[
FakeRequest(Uri.parse(kTestEnvPubHost), method: HttpMethod.head, responseError: TimeoutException('Timeout error')),
FakeRequest(Uri.parse(kTestEnvGCloudHost), method: HttpMethod.head),
]),
);
// Run the validation check and get the results
final ValidationResult result = await httpHostValidator.validate();
// Timeout duration for tests is set to 1 second
expect(
result.messages,
contains(const ValidationMessage.error('HTTP host "$kTestEnvPubHost" is not reachable. Reason: Failed to connect to host in 1 second')),
);
});
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment