Skip to content
This repository has been archived by the owner on Feb 13, 2023. It is now read-only.

Commit

Permalink
Improve DioErrors (#16)
Browse files Browse the repository at this point in the history
Signed-off-by: Jonas Uekötter <[email protected]>
Co-authored-by: Alex Li <[email protected]>
  • Loading branch information
ueman and AlexV525 committed Nov 8, 2022
1 parent 2aca3b8 commit 090f18f
Show file tree
Hide file tree
Showing 11 changed files with 225 additions and 126 deletions.
6 changes: 6 additions & 0 deletions dio/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@

## Breaking Changes

- Improve `DioError`s. There are now more cases in which the inner original stacktrace is supplied.
- `DioErrorType.connectTimeout` was changed to `DioErrorType.connectionTimeout`
- `DioErrorType.response` was changed to `DioErrorType.badResponse`
- `DioErrorType.other` was changed to `DioErrorType.unknown`
- `DioError` has now multiple constructors for specific error cases. Since `DioError` is only thrown internal to the libary, you shouldn't need to adapt any code.
- Catching `DioError`s still works the same way as before.
- `HttpClientAdapter` must now be implemented instead of extended.
- Any classes specific to `dart:io` platforms can now be imported via `import 'package:dio/io.dart';`. Classes specific to web can be imported via `import 'package:dio/web.dart';`
- `DefaultHttpClientAdapter` was renamed to `IOHttpClientAdapter`
Expand Down
33 changes: 20 additions & 13 deletions dio/lib/src/adapters/browser_adapter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -73,18 +73,26 @@ class BrowserHttpClientAdapter implements HttpClientAdapter {
connectionTimeout,
() {
if (!completer.isCompleted) {
xhr.abort();
completer.completeError(
DioError(
DioError.connectionTimeout(
requestOptions: options,
error: 'Connecting timed out [${options.connectTimeout}ms]',
type: DioErrorType.connectTimeout,
timeout: connectionTimeout,
),
StackTrace.current,
);
xhr.abort();
return;
} else {
// connectTimeout is triggered after the fetch has been completed.
}
xhr.abort();
completer.completeError(
DioError.connectionTimeout(
requestOptions: options,
timeout: options.connectTimeout!,
),
StackTrace.current,
);
},
);
}
Expand All @@ -107,10 +115,9 @@ class BrowserHttpClientAdapter implements HttpClientAdapter {
if (duration > sendTimeout) {
uploadStopwatch.stop();
completer.completeError(
DioError(
DioError.sendTimeout(
timeout: sendTimeout,
requestOptions: options,
error: 'Sending timed out [${options.sendTimeout}ms]',
type: DioErrorType.sendTimeout,
),
StackTrace.current,
);
Expand Down Expand Up @@ -141,10 +148,9 @@ class BrowserHttpClientAdapter implements HttpClientAdapter {
if (duration > reveiveTimeout) {
downloadStopwatch.stop();
completer.completeError(
DioError(
DioError.receiveTimeout(
timeout: options.receiveTimeout!,
requestOptions: options,
error: 'Receiving timed out [${options.receiveTimeout}ms]',
type: DioErrorType.receiveTimeout,
),
StackTrace.current,
);
Expand All @@ -162,11 +168,12 @@ class BrowserHttpClientAdapter implements HttpClientAdapter {
connectTimeoutTimer?.cancel();
// Unfortunately, the underlying XMLHttpRequest API doesn't expose any
// specific information about the error itself.
// See also: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequestEventTarget/onerror
completer.completeError(
DioError(
type: DioErrorType.response,
error: 'XMLHttpRequest error.',
DioError.connectionError(
requestOptions: options,
reason: 'The XMLHttpRequest onError callback was called. '
'This typically indicates an error on the network layer.',
),
StackTrace.current,
);
Expand Down
81 changes: 43 additions & 38 deletions dio/lib/src/adapters/io_adapter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,19 @@ class IOHttpClientAdapter implements HttpClientAdapter {
var _httpClient = _configHttpClient(cancelFuture, options.connectTimeout);
var reqFuture = _httpClient.openUrl(options.method, options.uri);

void _throwConnectingTimeout() {
throw DioError(
requestOptions: options,
error: 'Connecting timed out [${options.connectTimeout}ms]',
type: DioErrorType.connectTimeout,
);
}

late HttpClientRequest request;
try {
final connectionTimeout = options.connectTimeout;
if (connectionTimeout != null) {
request = await reqFuture.timeout(connectionTimeout);
request = await reqFuture.timeout(
connectionTimeout,
onTimeout: () {
throw DioError.connectionTimeout(
requestOptions: options,
timeout: connectionTimeout,
);
},
);
} else {
request = await reqFuture;
}
Expand All @@ -58,13 +58,18 @@ class IOHttpClientAdapter implements HttpClientAdapter {
options.headers.forEach((k, v) {
if (v != null) request.headers.set(k, '$v');
});
} on SocketException catch (e) {
if (e.message.contains('timed out')) {
_throwConnectingTimeout();
} on SocketException catch (e, stackTrace) {
if (!e.message.contains('timed out')) {
rethrow;
}
rethrow;
} on TimeoutException {
_throwConnectingTimeout();
throw DioError.connectionTimeout(
requestOptions: options,
timeout: options.connectTimeout ??
_httpClient.connectionTimeout ??
Duration.zero,
error: e,
stackTrace: stackTrace,
);
}

request.followRedirects = options.followRedirects;
Expand All @@ -75,37 +80,38 @@ class IOHttpClientAdapter implements HttpClientAdapter {
var future = request.addStream(requestStream);
final sendTimeout = options.sendTimeout;
if (sendTimeout != null) {
future = future.timeout(sendTimeout);
}
try {
await future;
} on TimeoutException {
request.abort();
throw DioError(
requestOptions: options,
error: 'Sending timeout[${options.sendTimeout}ms]',
type: DioErrorType.sendTimeout,
future = future.timeout(
sendTimeout,
onTimeout: () {
request.abort();
throw DioError.sendTimeout(
timeout: sendTimeout,
requestOptions: options,
);
},
);
}

await future;
}

final stopwatch = Stopwatch()..start();
var future = request.close();
final receiveTimeout = options.receiveTimeout;
if (receiveTimeout != null) {
future = future.timeout(receiveTimeout);
}
late HttpClientResponse responseStream;
try {
responseStream = await future;
} on TimeoutException {
throw DioError(
requestOptions: options,
error: 'Receiving data timeout[${options.receiveTimeout}]',
type: DioErrorType.receiveTimeout,
future = future.timeout(
receiveTimeout,
onTimeout: () {
throw DioError.receiveTimeout(
timeout: receiveTimeout,
requestOptions: options,
);
},
);
}

final responseStream = await future;

var stream =
responseStream.transform<Uint8List>(StreamTransformer.fromHandlers(
handleData: (data, sink) {
Expand All @@ -114,10 +120,9 @@ class IOHttpClientAdapter implements HttpClientAdapter {
final receiveTimeout = options.receiveTimeout;
if (receiveTimeout != null && duration > receiveTimeout) {
sink.addError(
DioError(
DioError.receiveTimeout(
timeout: receiveTimeout,
requestOptions: options,
error: 'Receiving data timeout[${options.receiveTimeout}]',
type: DioErrorType.receiveTimeout,
),
);
responseStream.detachSocket().then((socket) => socket.destroy());
Expand Down
9 changes: 4 additions & 5 deletions dio/lib/src/cancel_token.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,12 @@ class CancelToken {
Future<DioError> get whenCancel => _completer.future;

/// Cancel the request
void cancel([dynamic reason]) {
_cancelError = DioError(
type: DioErrorType.cancel,
error: reason,
void cancel([Object? reason]) {
_cancelError = DioError.requestCancelled(
requestOptions: requestOptions ?? RequestOptions(path: ''),
reason: reason,
stackTrace: StackTrace.current,
);
_cancelError!.stackTrace = StackTrace.current;

if (!_completer.isCompleted) {
_completer.complete(_cancelError);
Expand Down
110 changes: 90 additions & 20 deletions dio/lib/src/dio_error.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,109 @@ import 'options.dart';
import 'response.dart';

enum DioErrorType {
/// It occurs when url is opened timeout.
connectTimeout,
/// Caused by a connection timeout.
connectionTimeout,

/// It occurs when url is sent timeout.
sendTimeout,

///It occurs when receiving timeout.
receiveTimeout,

/// When the server response, but with a incorrect status, such as 404, 503...
response,
/// The [DioError] was caused by an incorrect status code as configured by
/// [ValidateStatus].
badResponse,

/// When the request is cancelled, dio will throw a error with this type.
cancel,

/// Caused for example by a `xhr.onError` or SocketExceptions.
connectionError,

/// Default error type, Some other Error. In this case, you can
/// use the DioError.error if it is not null.
other,
unknown,
}

extension _DioErrorTypeExtension on DioErrorType {
String toPrettyDescription() {
switch (this) {
case DioErrorType.connectionTimeout:
return 'connection timeout';
case DioErrorType.sendTimeout:
return 'send timeout';
case DioErrorType.receiveTimeout:
return 'receive timeout';
case DioErrorType.badResponse:
return 'bad response';
case DioErrorType.cancel:
return 'request cancelled';
case DioErrorType.connectionError:
return 'connection error';
case DioErrorType.unknown:
return 'unknown';
}
}
}

/// DioError describes the error info when request failed.
/// DioError describes the exception info when a request failed.
class DioError implements Exception {
/// Prefer using one of the other constructors.
/// They're most likely better fitting.
DioError({
required this.requestOptions,
this.response,
this.type = DioErrorType.other,
this.type = DioErrorType.unknown,
this.error,
this.stackTrace,
this.message,
});

DioError.badResponse({
required int statusCode,
required this.requestOptions,
required this.response,
}) : type = DioErrorType.badResponse,
message = 'The request returned an '
'invalid status code of $statusCode.';

DioError.connectionTimeout({
required Duration timeout,
required this.requestOptions,
this.error,
this.stackTrace,
}) : type = DioErrorType.connectionTimeout,
message = 'The request connection took '
'longer than $timeout. It was aborted.';

DioError.sendTimeout({
required Duration timeout,
required this.requestOptions,
}) : type = DioErrorType.sendTimeout,
message = 'The request took '
'longer than $timeout to send data. It was aborted.';

DioError.receiveTimeout({
required Duration timeout,
required this.requestOptions,
}) : type = DioErrorType.receiveTimeout,
message = 'The request took '
'longer than $timeout to receive data. It was aborted.';

DioError.requestCancelled({
required this.requestOptions,
required Object? reason,
this.stackTrace,
}) : type = DioErrorType.cancel,
message = 'The request was cancelled.',
error = reason;

DioError.connectionError({
required this.requestOptions,
required String reason,
}) : type = DioErrorType.connectionError,
message = 'The connection errored: $reason';

/// Request info.
RequestOptions requestOptions;

Expand All @@ -42,24 +116,20 @@ class DioError implements Exception {

/// The original error/exception object; It's usually not null when `type`
/// is DioErrorType.other
dynamic error;

StackTrace? _stackTrace;
Object? error;

set stackTrace(StackTrace? stack) => _stackTrace = stack;
/// The stacktrace of the original error/exception object;
/// It's usually not null when `type` is DioErrorType.other
StackTrace? stackTrace;

StackTrace? get stackTrace => _stackTrace;

String get message => (error?.toString() ?? '');
String? message;

@override
String toString() {
var msg = 'DioError [$type]: $message';
if (error is Error) {
msg += '\n${(error as Error).stackTrace}';
}
if (_stackTrace != null) {
msg += '\nSource stack:\n$stackTrace';
var msg = 'DioError [${type.toPrettyDescription()}]: $message';
final error = this.error;
if (error != null) {
msg += '\nError: $error';
}
return msg;
}
Expand Down
Loading

0 comments on commit 090f18f

Please sign in to comment.