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

Improve DioErrors #16

Merged
merged 7 commits into from
Nov 8, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions dio/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Breaking Changes

- Improve `DioError`s
- Previously `options.connectTimeout` and `options.receiveTimeout` were `int`s. They're now `Duration`s. To migrate change `options.connectTimeout = 1000;` to `options.connectTimeout = Duration(seconds: 1);`. The same applies to `receiveTimeout`. Setting the timeouts to `null` indicates that the system default timeouts should be used.

# 4.0.6
Expand Down
32 changes: 19 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,25 @@ 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();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checking around the code again, should we return the closure here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think that makes sense.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might figure out a proper test for this, but no need to rush.

} else {
// connectTimeout is triggered after the fetch has been completed.
}
completer.completeError(
DioError.connectionTimeout(
requestOptions: options,
timeout: options.connectTimeout!,
),
StackTrace.current,
);
xhr.abort();
},
);
}
Expand All @@ -107,10 +114,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 +147,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 +167,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 @@ -34,19 +34,19 @@ class DefaultHttpClientAdapter 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 @@ -55,13 +55,18 @@ class DefaultHttpClientAdapter 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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it necessary to remove this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the exceptions are now thrown in the future.timeout(...) callback, no TimeoutException is thrown.

If onTimeout is omitted, a timeout will cause the returned future to complete with a TimeoutException.

See https://api.dart.dev/stable/2.18.4/dart-async/Future/timeout.html

_throwConnectingTimeout();
throw DioError.connectionTimeout(
requestOptions: options,
timeout: options.connectTimeout ??
_httpClient.connectionTimeout ??
Duration.zero,
error: e,
stackTrace: stackTrace,
);
}

request.followRedirects = options.followRedirects;
Expand All @@ -72,37 +77,38 @@ class DefaultHttpClientAdapter 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 @@ -111,10 +117,9 @@ class DefaultHttpClientAdapter 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
114 changes: 95 additions & 19 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';
ueman marked this conversation as resolved.
Show resolved Hide resolved

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,26 @@ 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}';
var msg = 'DioError [${type.toPrettyDescription()}]: $message';
final error = this.error;
if (error != null) {
msg += '\nError: $error';
}
if (error is Error && error.stackTrace != stackTrace) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to hide stack traces from when describing a DioError, WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you prefer it?

I would prefer to get rid of the wrapping of exceptions, which is the cause of it. But as long as the wrapping of exceptions is done, the inner stacktrace might be helpful for debugging, so it should be included in the message. Otherwise, there's no way to get that stacktrace when using error monitoring tools.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because it holds the stack trace. The stack trace can be accessed as a field of DioError, rather than wrapped into a describing method. Consider we have an exception handler, so we can do if (error is DioError) stackTrace = error.stackTrace. You'll usually report exceptions so the report will contain duplicate stack traces.

msg += '\nInner error stacktrace:\n${error.stackTrace}';
}
if (_stackTrace != null) {
msg += '\nSource stack:\n$stackTrace';
if (stackTrace != null) {
msg += '\nInner stacktrace:\n$stackTrace';
}
return msg;
}
Expand Down
Loading