improve failure modes of ChromeConnection.getTabs (#88)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 121b26f..c175eda 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,10 @@
-## 1.0.2-dev
+## 1.1.0-dev
+- Have `ChromeConnection.getTabs` return better exceptions where there's a
+ failure setting up the Chrome connection (#85).
+- Introduce a new, optional `retryFor` parameter to `ChromeConnection.getTabs`.
+ This will re-try failed connections for a period of time; it can be useful to
+ mitigate some intermittent connection issues very early in Chrome's startup.
## 1.0.1
- Use `package:lints` for analysis.
diff --git a/lib/webkit_inspection_protocol.dart b/lib/webkit_inspection_protocol.dart
index ff5a7e2..6f5f7df 100644
--- a/lib/webkit_inspection_protocol.dart
+++ b/lib/webkit_inspection_protocol.dart
@@ -6,7 +6,7 @@
import 'dart:async';
import 'dart:convert';
-import 'dart:io' show HttpClient, HttpClientResponse, WebSocket;
+import 'dart:io' show HttpClient, HttpClientResponse, IOException, WebSocket;
import 'src/console.dart';
import 'src/debugger.dart';
@@ -36,16 +36,49 @@
ChromeConnection(String host, [int port = 9222])
: url = Uri.parse('http://$host:$port/');
- // TODO(DrMarcII): consider changing this to return Stream<ChromeTab>.
- Future<List<ChromeTab>> getTabs() async {
+ /// Return all the available tabs.
+ ///
+ /// This method can potentially throw a [ConnectionException] on some protocol
+ /// issues.
+ ///
+ /// An optional [retryFor] duration can be used to automatically re-try
+ /// connections for some period of time. Anecdotally, Chrome can return errors
+ /// when trying to list the available tabs very early in its startup sequence.
+ Future<List<ChromeTab>> getTabs({
+ Duration? retryFor,
+ }) async {
+ final start = DateTime.now();
+ DateTime? end = retryFor == null ? null : start.add(retryFor);
+
var response = await getUrl('/json');
- var respBody = await utf8.decodeStream(response.cast<List<int>>());
- return List<ChromeTab>.from(
- (jsonDecode(respBody) as List).map((m) => ChromeTab(m as Map)));
+ var responseBody = await utf8.decodeStream(response.cast<List<int>>());
+
+ late List decoded;
+ while (true) {
+ try {
+ decoded = jsonDecode(responseBody);
+ return List<ChromeTab>.from(decoded.map((m) => ChromeTab(m as Map)));
+ } on FormatException catch (formatException) {
+ if (end != null && end.isBefore(DateTime.now())) {
+ // Delay for retryFor / 4 milliseconds.
+ await Future.delayed(
+ Duration(milliseconds: retryFor!.inMilliseconds ~/ 4),
+ );
+ } else {
+ throw ConnectionException(
+ formatException: formatException,
+ responseStatus: '${response.statusCode} ${response.reasonPhrase}',
+ responseBody: responseBody,
+ );
+ }
+ }
+ }
}
- Future<ChromeTab?> getTab(bool Function(ChromeTab tab) accept,
- {Duration? retryFor}) async {
+ Future<ChromeTab?> getTab(
+ bool Function(ChromeTab tab) accept, {
+ Duration? retryFor,
+ }) async {
var start = DateTime.now();
var end = start;
if (retryFor != null) {
@@ -79,6 +112,39 @@
void close() => _client.close(force: true);
}
+/// An exception that can be thrown early in the connection sequence for a
+/// [ChromeConnection].
+///
+/// This exception includes the underlying exception, as well as the http
+/// response from the browser that we failed on. The [toString] implementation
+/// includes a summary of the response.
+class ConnectionException implements IOException {
+ final FormatException formatException;
+ final String responseStatus;
+ final String responseBody;
+
+ ConnectionException({
+ required this.formatException,
+ required this.responseStatus,
+ required this.responseBody,
+ });
+
+ @override
+ String toString() {
+ final buf = StringBuffer('${formatException.message}\n');
+ buf.writeln('$responseStatus; body:');
+ var lines = responseBody.split('\n');
+ if (lines.length > 10) {
+ lines = [
+ ...lines.take(10),
+ '...',
+ ];
+ }
+ buf.writeAll(lines, '\n');
+ return buf.toString();
+ }
+}
+
class ChromeTab {
final Map _map;
diff --git a/pubspec.yaml b/pubspec.yaml
index d4850c0..a36f2e6 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
name: webkit_inspection_protocol
-version: 1.0.2-dev
+version: 1.1.0-dev
description: >
A client for the Chrome DevTools Protocol (previously called the Webkit
Inspection Protocol).