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 6 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
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
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 @@ -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 {
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 @@ -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