From 7276db075e49fd4961a646a33a056691a50db4ba Mon Sep 17 00:00:00 2001 From: Peter Leibiger Date: Mon, 11 Sep 2023 18:26:21 +0200 Subject: [PATCH] Initial exception/stacktrace rework * rename `DioException.error` to `DioException.cause` * add `DioException.causeStackTrace` * get rid of `RequestOptions.sourceStackTrace` property * use `Error.throwWithStackTrace` to throw all errors with the manually captured stack trace at a single point in `DioMixin` * extract some inner functions inside `DioMixin.fetch` to prevent them capturing the state of `requestOptions` and `cancelToken` variables, instead pass them as arguments --- dio/lib/src/adapters/browser_adapter.dart | 3 +- dio/lib/src/adapters/io_adapter.dart | 6 +- dio/lib/src/cancel_token.dart | 4 +- dio/lib/src/dio/dio_for_native.dart | 10 +- dio/lib/src/dio_exception.dart | 55 ++-- dio/lib/src/dio_mixin.dart | 241 +++++++++------- dio/lib/src/interceptor.dart | 6 +- dio/lib/src/options.dart | 14 - dio/test/basic_test.dart | 2 +- dio/test/cancel_token_test.dart | 4 +- dio/test/download_test.dart | 2 +- dio/test/exception_test.dart | 6 +- dio/test/interceptor_test.dart | 46 +-- dio/test/options_test.dart | 4 +- dio/test/stacktrace_test.dart | 270 ++++++++---------- example/lib/request_interceptors.dart | 2 +- example/lib/response_interceptor.dart | 2 +- example/lib/transformer.dart | 2 +- .../cookie_manager/lib/src/cookie_mgr.dart | 6 +- .../lib/src/connection_manager_imp.dart | 4 +- 20 files changed, 342 insertions(+), 347 deletions(-) diff --git a/dio/lib/src/adapters/browser_adapter.dart b/dio/lib/src/adapters/browser_adapter.dart index 9ae672c1a..33ca3f524 100644 --- a/dio/lib/src/adapters/browser_adapter.dart +++ b/dio/lib/src/adapters/browser_adapter.dart @@ -110,7 +110,6 @@ class BrowserHttpClientAdapter implements HttpClientAdapter { requestOptions: options, timeout: connectionTimeout, ), - StackTrace.current, ); }, ); @@ -224,7 +223,7 @@ class BrowserHttpClientAdapter implements HttpClientAdapter { completer.completeError( DioException.requestCancelled( requestOptions: options, - reason: 'The XMLHttpRequest was aborted.', + cause: 'The XMLHttpRequest was aborted.', ), ); } diff --git a/dio/lib/src/adapters/io_adapter.dart b/dio/lib/src/adapters/io_adapter.dart index 47a324f09..9ed15c3b5 100644 --- a/dio/lib/src/adapters/io_adapter.dart +++ b/dio/lib/src/adapters/io_adapter.dart @@ -107,13 +107,13 @@ class IOHttpClientAdapter implements HttpClientAdapter { timeout: options.connectTimeout ?? httpClient.connectionTimeout ?? Duration.zero, - error: e, + cause: e, ); } throw DioException.connectionError( requestOptions: options, reason: e.message, - error: e, + cause: e, ); } @@ -169,7 +169,7 @@ class IOHttpClientAdapter implements HttpClientAdapter { throw DioException( requestOptions: options, type: DioExceptionType.badCertificate, - error: responseStream.certificate, + cause: responseStream.certificate, message: 'The certificate of the response is not approved.', ); } diff --git a/dio/lib/src/cancel_token.dart b/dio/lib/src/cancel_token.dart index 2dfa656ad..c3f56eabd 100644 --- a/dio/lib/src/cancel_token.dart +++ b/dio/lib/src/cancel_token.dart @@ -37,8 +37,8 @@ class CancelToken { void cancel([Object? reason]) { _cancelError = DioException.requestCancelled( requestOptions: requestOptions ?? RequestOptions(), - reason: reason, - stackTrace: StackTrace.current, + cause: reason, + causeStackTrace: StackTrace.current, ); if (!_completer.isCompleted) { _completer.complete(_cancelError); diff --git a/dio/lib/src/dio/dio_for_native.dart b/dio/lib/src/dio/dio_for_native.dart index d16fa4124..4010f68d7 100644 --- a/dio/lib/src/dio/dio_for_native.dart +++ b/dio/lib/src/dio/dio_for_native.dart @@ -147,7 +147,7 @@ class DioForNative with DioMixin implements Dio { } } finally { completer.completeError( - DioMixin.assureDioException(e, response.requestOptions), + assureDioException(e, response.requestOptions), ); } }); @@ -160,7 +160,7 @@ class DioForNative with DioMixin implements Dio { completer.complete(response); } catch (e) { completer.completeError( - DioMixin.assureDioException(e, response.requestOptions), + assureDioException(e, response.requestOptions), ); } }, @@ -169,7 +169,7 @@ class DioForNative with DioMixin implements Dio { await closeAndDelete(); } finally { completer.completeError( - DioMixin.assureDioException(e, response.requestOptions), + assureDioException(e, response.requestOptions), ); } }, @@ -190,10 +190,10 @@ class DioForNative with DioMixin implements Dio { throw DioException.receiveTimeout( timeout: timeout, requestOptions: response.requestOptions, - error: e, + cause: e, ); } else { - throw e; + throw assureDioException(e, response.requestOptions); } }, ); diff --git a/dio/lib/src/dio_exception.dart b/dio/lib/src/dio_exception.dart index 488a6e7bd..ca5d67cbd 100644 --- a/dio/lib/src/dio_exception.dart +++ b/dio/lib/src/dio_exception.dart @@ -27,7 +27,7 @@ enum DioExceptionType { connectionError, /// Default error type, Some other [Error]. In this case, you can use the - /// [DioException.error] if it is not null. + /// [DioException.cause] if it is not null. unknown, } @@ -62,14 +62,10 @@ class DioException implements Exception { required this.requestOptions, this.response, this.type = DioExceptionType.unknown, - this.error, - StackTrace? stackTrace, + this.cause, + StackTrace? causeStackTrace, this.message, - }) : stackTrace = identical(stackTrace, StackTrace.empty) - ? requestOptions.sourceStackTrace ?? StackTrace.current - : stackTrace ?? - requestOptions.sourceStackTrace ?? - StackTrace.current; + }) : _causeStackTrace = causeStackTrace; factory DioException.badResponse({ required int statusCode, @@ -82,13 +78,13 @@ class DioException implements Exception { 'invalid status code of $statusCode.', requestOptions: requestOptions, response: response, - error: null, + cause: null, ); factory DioException.connectionTimeout({ required Duration timeout, required RequestOptions requestOptions, - Object? error, + Object? cause, }) => DioException( type: DioExceptionType.connectionTimeout, @@ -96,7 +92,7 @@ class DioException implements Exception { 'longer than $timeout. It was aborted.', requestOptions: requestOptions, response: null, - error: error, + cause: cause, ); factory DioException.sendTimeout({ @@ -109,13 +105,13 @@ class DioException implements Exception { 'longer than $timeout to send data. It was aborted.', requestOptions: requestOptions, response: null, - error: null, + cause: null, ); factory DioException.receiveTimeout({ required Duration timeout, required RequestOptions requestOptions, - Object? error, + Object? cause, }) => DioException( type: DioExceptionType.receiveTimeout, @@ -123,34 +119,34 @@ class DioException implements Exception { 'longer than $timeout to receive data. It was aborted.', requestOptions: requestOptions, response: null, - error: error, + cause: cause, ); factory DioException.requestCancelled({ required RequestOptions requestOptions, - required Object? reason, - StackTrace? stackTrace, + required Object? cause, + StackTrace? causeStackTrace, }) => DioException( type: DioExceptionType.cancel, message: 'The request was cancelled.', requestOptions: requestOptions, response: null, - error: reason, - stackTrace: stackTrace, + cause: cause, + causeStackTrace: causeStackTrace, ); factory DioException.connectionError({ required RequestOptions requestOptions, required String reason, - Object? error, + Object? cause, }) => DioException( type: DioExceptionType.connectionError, message: 'The connection errored: $reason', requestOptions: requestOptions, response: null, - error: error, + cause: cause, ); /// The request info for the request that throws exception. @@ -164,11 +160,14 @@ class DioException implements Exception { /// The original error/exception object; /// It's usually not null when `type` is [DioExceptionType.unknown]. - final Object? error; + final Object? cause; /// The stacktrace of the original error/exception object; /// It's usually not null when `type` is [DioExceptionType.unknown]. - final StackTrace stackTrace; + final StackTrace? _causeStackTrace; + + StackTrace? get causeStackTrace => + _causeStackTrace ?? (cause is Error ? (cause as Error).stackTrace : null); /// The error message that throws a [DioException]. final String? message; @@ -178,16 +177,16 @@ class DioException implements Exception { RequestOptions? requestOptions, Response? response, DioExceptionType? type, - Object? error, - StackTrace? stackTrace, + Object? cause, + StackTrace? causeStackTrace, String? message, }) { return DioException( requestOptions: requestOptions ?? this.requestOptions, response: response ?? this.response, type: type ?? this.type, - error: error ?? this.error, - stackTrace: stackTrace ?? this.stackTrace, + cause: cause ?? this.cause, + causeStackTrace: causeStackTrace ?? this.causeStackTrace, message: message ?? this.message, ); } @@ -195,8 +194,8 @@ class DioException implements Exception { @override String toString() { String msg = 'DioException [${type.toPrettyDescription()}]: $message'; - if (error != null) { - msg += '\nError: $error'; + if (cause != null) { + msg += '\nError: $cause'; } return msg; } diff --git a/dio/lib/src/dio_mixin.dart b/dio/lib/src/dio_mixin.dart index 89f20fd70..a04328eae 100644 --- a/dio/lib/src/dio_mixin.dart +++ b/dio/lib/src/dio_mixin.dart @@ -345,7 +345,6 @@ abstract class DioMixin implements Dio { onReceiveProgress: onReceiveProgress, onSendProgress: onSendProgress, cancelToken: cancelToken, - sourceStackTrace: StackTrace.current, ); if (_closed) { @@ -370,83 +369,6 @@ abstract class DioMixin implements Dio { } } - // Convert the request interceptor to a functional callback in which - // we can handle the return value of interceptor callback. - FutureOr Function(dynamic) requestInterceptorWrapper( - InterceptorSendCallback interceptor, - ) { - return (dynamic incomingState) async { - final state = incomingState as InterceptorState; - if (state.type == InterceptorResultType.next) { - return listenCancelForAsyncTask( - requestOptions.cancelToken, - Future(() { - final requestHandler = RequestInterceptorHandler(); - interceptor(state.data as RequestOptions, requestHandler); - return requestHandler.future; - }), - ); - } else { - return state; - } - }; - } - - // Convert the response interceptor to a functional callback in which - // we can handle the return value of interceptor callback. - FutureOr Function(dynamic) responseInterceptorWrapper( - InterceptorSuccessCallback interceptor, - ) { - return (dynamic incomingState) async { - final state = incomingState as InterceptorState; - if (state.type == InterceptorResultType.next || - state.type == InterceptorResultType.resolveCallFollowing) { - return listenCancelForAsyncTask( - requestOptions.cancelToken, - Future(() { - final responseHandler = ResponseInterceptorHandler(); - interceptor(state.data as Response, responseHandler); - return responseHandler.future; - }), - ); - } else { - return state; - } - }; - } - - // Convert the error interceptor to a functional callback in which - // we can handle the return value of interceptor callback. - FutureOr Function(Object) errorInterceptorWrapper( - InterceptorErrorCallback interceptor, - ) { - return (err) { - final state = err is InterceptorState - ? err - : InterceptorState(assureDioException(err, requestOptions)); - Future handleError() async { - final errorHandler = ErrorInterceptorHandler(); - interceptor(state.data, errorHandler); - return errorHandler.future; - } - - // The request has already been cancelled, - // there is no need to listen for another cancellation. - if (state.data is DioException && - state.data.type == DioExceptionType.cancel) { - return handleError(); - } else if (state.type == InterceptorResultType.next || - state.type == InterceptorResultType.rejectCallFollowing) { - return listenCancelForAsyncTask( - requestOptions.cancelToken, - Future(handleError), - ); - } else { - throw err; - } - }; - } - // Build a request flow in which the processors(interceptors) // execute in FIFO order. Future future = Future( @@ -458,22 +380,28 @@ abstract class DioMixin implements Dio { final fun = interceptor is QueuedInterceptor ? interceptor._handleRequest : interceptor.onRequest; - future = future.then(requestInterceptorWrapper(fun)); + future = future.then(_requestInterceptorWrapper( + fun, + requestOptions.cancelToken, + )); } // Add dispatching callback into the request flow. future = future.then( - requestInterceptorWrapper(( - RequestOptions reqOpt, - RequestInterceptorHandler handler, - ) { - requestOptions = reqOpt; - _dispatchRequest(reqOpt) - .then((value) => handler.resolve(value, true)) - .catchError((e) { - handler.reject(e as DioException, true); - }); - }), + _requestInterceptorWrapper( + ( + RequestOptions reqOpt, + RequestInterceptorHandler handler, + ) { + requestOptions = reqOpt; + _dispatchRequest(reqOpt) + .then((value) => handler.resolve(value, true)) + .catchError((e) { + handler.reject(e as DioException, true); + }); + }, + requestOptions.cancelToken, + ), ); // Add response interceptors into the request flow @@ -481,7 +409,10 @@ abstract class DioMixin implements Dio { final fun = interceptor is QueuedInterceptor ? interceptor._handleResponse : interceptor.onResponse; - future = future.then(responseInterceptorWrapper(fun)); + future = future.then(_responseInterceptorWrapper( + fun, + requestOptions.cancelToken, + )); } // Add error handlers into the request flow. @@ -489,9 +420,21 @@ abstract class DioMixin implements Dio { final fun = interceptor is QueuedInterceptor ? interceptor._handleError : interceptor.onError; - future = future.catchError(errorInterceptorWrapper(fun)); + future = future.catchError(_errorInterceptorWrapper( + fun, + requestOptions, + )); } - // Normalize errors, converts errors to [DioException]. + + /// This is the deepest point where we can capture the stack trace for + /// the request. The stack trace is attached to any error that occurs + /// after this point. + /// Capturing a stack trace after this point will cause it to start + /// behind the async gap of the first interceptor or the request + /// dispatching callback. In that case all the information about the + /// original call site, where the request was created, is lost. + final stackTrace = StackTrace.current; + return future.then>((data) { return assureResponse( data is InterceptorState ? data.data : data, @@ -504,7 +447,13 @@ abstract class DioMixin implements Dio { return assureResponse(e.data, requestOptions); } } - throw assureDioException(isState ? e.data : e, requestOptions); + + /// Convert all errors to [DioException] and rethrow + /// them with the stack trace that was captured before. + Error.throwWithStackTrace( + assureDioException(isState ? e.data : e, requestOptions), + stackTrace, + ); }); } @@ -557,8 +506,12 @@ abstract class DioMixin implements Dio { response: ret, ); } - } catch (e) { - throw assureDioException(e, reqOpt); + } catch (e, stackTrace) { + throw assureDioException( + e, + reqOpt, + causeStackTrace: stackTrace, + ); } } @@ -652,6 +605,86 @@ abstract class DioMixin implements Dio { return null; } + // Convert the request interceptor to a functional callback in which + // we can handle the return value of interceptor callback. + FutureOr Function(dynamic) _requestInterceptorWrapper( + InterceptorSendCallback interceptor, + CancelToken? cancelToken, + ) { + return (dynamic incomingState) async { + final state = incomingState as InterceptorState; + if (state.type == InterceptorResultType.next) { + return listenCancelForAsyncTask( + cancelToken, + Future(() { + final requestHandler = RequestInterceptorHandler(); + interceptor(state.data as RequestOptions, requestHandler); + return requestHandler.future; + }), + ); + } else { + return state; + } + }; + } + + // Convert the response interceptor to a functional callback in which + // we can handle the return value of interceptor callback. + FutureOr Function(dynamic) _responseInterceptorWrapper( + InterceptorSuccessCallback interceptor, + CancelToken? cancelToken, + ) { + return (dynamic incomingState) async { + final state = incomingState as InterceptorState; + if (state.type == InterceptorResultType.next || + state.type == InterceptorResultType.resolveCallFollowing) { + return listenCancelForAsyncTask( + cancelToken, + Future(() { + final responseHandler = ResponseInterceptorHandler(); + interceptor(state.data as Response, responseHandler); + return responseHandler.future; + }), + ); + } else { + return state; + } + }; + } + + // Convert the error interceptor to a functional callback in which + // we can handle the return value of interceptor callback. + FutureOr Function(Object) _errorInterceptorWrapper( + InterceptorErrorCallback interceptor, + RequestOptions requestOptions, + ) { + return (err) { + final state = err is InterceptorState + ? err + : InterceptorState(assureDioException(err, requestOptions)); + Future handleError() async { + final errorHandler = ErrorInterceptorHandler(); + interceptor(state.data, errorHandler); + return errorHandler.future; + } + + // The request has already been cancelled, + // there is no need to listen for another cancellation. + if (state.data is DioException && + state.data.type == DioExceptionType.cancel) { + return handleError(); + } else if (state.type == InterceptorResultType.next || + state.type == InterceptorResultType.rejectCallFollowing) { + return listenCancelForAsyncTask( + requestOptions.cancelToken, + Future(handleError), + ); + } else { + throw err; + } + }; + } + // If the request has been cancelled, stop the request and throw error. @internal static void checkCancelled(CancelToken? cancelToken) { @@ -680,16 +713,18 @@ abstract class DioMixin implements Dio { } @internal - static DioException assureDioException( - Object err, - RequestOptions requestOptions, - ) { - if (err is DioException) { - return err; + DioException assureDioException( + Object cause, + RequestOptions requestOptions, { + StackTrace? causeStackTrace, + }) { + if (cause is DioException) { + return cause; } return DioException( requestOptions: requestOptions, - error: err, + cause: cause, + causeStackTrace: causeStackTrace, ); } diff --git a/dio/lib/src/interceptor.dart b/dio/lib/src/interceptor.dart index ec1927c20..4e0f9d3d0 100644 --- a/dio/lib/src/interceptor.dart +++ b/dio/lib/src/interceptor.dart @@ -77,7 +77,6 @@ class RequestInterceptorHandler extends _BaseHandler { ? InterceptorResultType.rejectCallFollowing : InterceptorResultType.reject, ), - error.stackTrace, ); _processNextInQueue?.call(); } @@ -123,7 +122,6 @@ class ResponseInterceptorHandler extends _BaseHandler { ? InterceptorResultType.rejectCallFollowing : InterceptorResultType.reject, ), - error.stackTrace, ); _processNextInQueue?.call(); } @@ -138,7 +136,6 @@ class ErrorInterceptorHandler extends _BaseHandler { void next(DioException error) { _completer.completeError( InterceptorState(error), - error.stackTrace, ); _processNextInQueue?.call(); } @@ -158,7 +155,6 @@ class ErrorInterceptorHandler extends _BaseHandler { void reject(DioException error) { _completer.completeError( InterceptorState(error, InterceptorResultType.reject), - error.stackTrace, ); _processNextInQueue?.call(); } @@ -200,6 +196,8 @@ class Interceptor { /// Called when an exception was occurred during the request. void onError( DioException err, + + /// TODO should the correct StackTrace be handed to the interceptor here? ErrorInterceptorHandler handler, ) { handler.next(err); diff --git a/dio/lib/src/options.dart b/dio/lib/src/options.dart index c4aa0c2eb..0df92036c 100644 --- a/dio/lib/src/options.dart +++ b/dio/lib/src/options.dart @@ -1,5 +1,3 @@ -import 'package:meta/meta.dart'; - import 'adapter.dart'; import 'cancel_token.dart'; import 'headers.dart'; @@ -296,7 +294,6 @@ class Options { CancelToken? cancelToken, ProgressCallback? onSendProgress, ProgressCallback? onReceiveProgress, - StackTrace? sourceStackTrace, }) { final query = {}; query.addAll(baseOpt.queryParameters); @@ -322,7 +319,6 @@ class Options { baseUrl: baseOpt.baseUrl, path: path, data: data, - sourceStackTrace: sourceStackTrace ?? StackTrace.current, connectTimeout: baseOpt.connectTimeout, sendTimeout: sendTimeout ?? baseOpt.sendTimeout, receiveTimeout: receiveTimeout ?? baseOpt.receiveTimeout, @@ -483,7 +479,6 @@ class RequestOptions extends _RequestConfig with OptionsMixin { ResponseDecoder? responseDecoder, ListFormat? listFormat, bool? setRequestContentTypeWhenNoPayload, - StackTrace? sourceStackTrace, }) : assert(connectTimeout == null || !connectTimeout.isNegative), super( method: method, @@ -502,7 +497,6 @@ class RequestOptions extends _RequestConfig with OptionsMixin { responseDecoder: responseDecoder, listFormat: listFormat, ) { - this.sourceStackTrace = sourceStackTrace ?? StackTrace.current; this.queryParameters = queryParameters ?? {}; this.baseUrl = baseUrl ?? ''; this.connectTimeout = connectTimeout; @@ -569,7 +563,6 @@ class RequestOptions extends _RequestConfig with OptionsMixin { requestEncoder: requestEncoder ?? this.requestEncoder, responseDecoder: responseDecoder ?? this.responseDecoder, listFormat: listFormat ?? this.listFormat, - sourceStackTrace: sourceStackTrace, ); if (contentType != null) { @@ -582,13 +575,6 @@ class RequestOptions extends _RequestConfig with OptionsMixin { return ro; } - /// The source [StackTrace] which should always point to the invocation of - /// [DioMixin.request] or if not provided, to the construction of the - /// [RequestOptions] instance. In both instances the source context should - /// still be available before it is lost due to asynchronous operations. - @internal - StackTrace? sourceStackTrace; - /// Generate the requesting [Uri] from the options. Uri get uri { String url = path; diff --git a/dio/test/basic_test.dart b/dio/test/basic_test.dart index e52faeae6..6e3a16a47 100644 --- a/dio/test/basic_test.dart +++ b/dio/test/basic_test.dart @@ -45,7 +45,7 @@ void main() { allOf([ isA(), (DioException e) => e.type == (DioExceptionType.connectionError), - if (!isWeb) (DioException e) => e.error is SocketException, + if (!isWeb) (DioException e) => e.cause is SocketException, ]), ), ); diff --git a/dio/test/cancel_token_test.dart b/dio/test/cancel_token_test.dart index 2cfb079ea..4282f5608 100644 --- a/dio/test/cancel_token_test.dart +++ b/dio/test/cancel_token_test.dart @@ -17,7 +17,7 @@ void main() { (error) { return error is DioException && error.type == DioExceptionType.cancel && - error.error == reason; + error.cause == reason; }, ), ); @@ -63,7 +63,7 @@ void main() { throwsA((error) => error is DioException && error.type == DioExceptionType.cancel && - error.error == reason), + error.cause == reason), ); } diff --git a/dio/test/download_test.dart b/dio/test/download_test.dart index 9f341ab14..04a6e2f57 100644 --- a/dio/test/download_test.dart +++ b/dio/test/download_test.dart @@ -99,7 +99,7 @@ void main() { deleteOnError: true, onReceiveProgress: (count, total) => throw AssertionError(), ) - .catchError((e) => throw (e as DioException).error!), + .catchError((e) => throw (e as DioException).cause!), throwsA(isA()), ); expect(f.existsSync(), isFalse); diff --git a/dio/test/exception_test.dart b/dio/test/exception_test.dart index 869d37ee2..bae506100 100644 --- a/dio/test/exception_test.dart +++ b/dio/test/exception_test.dart @@ -43,10 +43,10 @@ void main() { error = e; } expect(error, isNotNull); - expect(error.error, isA()); - expect((error.error as HandshakeException).osError, isNotNull); + expect(error.cause, isA()); + expect((error.cause as HandshakeException).osError, isNotNull); expect( - ((error.error as HandshakeException).osError as OSError).message, + ((error.cause as HandshakeException).osError as OSError).message, contains('Hostname mismatch'), ); }); diff --git a/dio/test/interceptor_test.dart b/dio/test/interceptor_test.dart index 80a5abb11..3eb5a846f 100644 --- a/dio/test/interceptor_test.dart +++ b/dio/test/interceptor_test.dart @@ -56,23 +56,23 @@ void main() { break; case '/reject': handler - .reject(DioException(requestOptions: reqOpt, error: 3)); + .reject(DioException(requestOptions: reqOpt, cause: 3)); break; case '/reject-next': handler.reject( - DioException(requestOptions: reqOpt, error: 4), + DioException(requestOptions: reqOpt, cause: 4), true, ); break; case '/reject-next/reject': handler.reject( - DioException(requestOptions: reqOpt, error: 5), + DioException(requestOptions: reqOpt, cause: 5), true, ); break; case '/reject-next-response': handler.reject( - DioException(requestOptions: reqOpt, error: 5), + DioException(requestOptions: reqOpt, cause: 5), true, ); break; @@ -97,13 +97,13 @@ void main() { handler.reject( DioException( requestOptions: options, - error: '/resolve-next/reject', + cause: '/resolve-next/reject', ), ); break; case '/resolve-next/reject-next': handler.reject( - DioException(requestOptions: options, error: ''), + DioException(requestOptions: options, cause: ''), true, ); break; @@ -121,14 +121,14 @@ void main() { ); } else if (err.requestOptions.path == '/resolve-next/reject-next') { - handler.next(err.copyWith(error: 1)); + handler.next(err.copyWith(cause: 1)); } else { if (err.requestOptions.path == '/reject-next/reject') { handler.reject(err); } else { - int count = err.error as int; + int count = err.cause as int; count++; - handler.next(err.copyWith(error: count)); + handler.next(err.copyWith(cause: count)); } } }, @@ -150,13 +150,13 @@ void main() { }, onError: (err, handler) { if (err.requestOptions.path == '/resolve-next/reject-next') { - int count = err.error as int; + int count = err.cause as int; count++; - handler.next(err.copyWith(error: count)); + handler.next(err.copyWith(cause: count)); } else { - int count = err.error as int; + int count = err.cause as int; count++; - handler.next(err.copyWith(error: count)); + handler.next(err.copyWith(cause: count)); } }, ), @@ -177,31 +177,31 @@ void main() { expect(response.data, 100); expect( - dio.get('/reject').catchError((e) => throw e.error as num), + dio.get('/reject').catchError((e) => throw e.cause as num), throwsA(3), ); expect( - dio.get('/reject-next').catchError((e) => throw e.error as num), + dio.get('/reject-next').catchError((e) => throw e.cause as num), throwsA(6), ); expect( - dio.get('/reject-next/reject').catchError((e) => throw e.error as num), + dio.get('/reject-next/reject').catchError((e) => throw e.cause as num), throwsA(5), ); expect( dio .get('/resolve-next/reject') - .catchError((e) => throw e.error as Object), + .catchError((e) => throw e.cause as Object), throwsA('/resolve-next/reject'), ); expect( dio .get('/resolve-next/reject-next') - .catchError((e) => throw e.error as num), + .catchError((e) => throw e.cause as num), throwsA(2), ); }); @@ -219,13 +219,13 @@ void main() { handler.next(reqOpt.copyWith(path: '/xxx')); }, onError: (err, handler) { - handler.next(err.copyWith(error: 'unexpected error')); + handler.next(err.copyWith(cause: 'unexpected error')); }, ), ); expect( - dio.get('/error').catchError((e) => throw e.error as String), + dio.get('/error').catchError((e) => throw e.cause as String), throwsA('unexpected error'), ); @@ -264,7 +264,7 @@ void main() { handler.reject( DioException( requestOptions: options, - error: 'test error', + cause: 'test error', ), ); break; @@ -272,7 +272,7 @@ void main() { handler.reject( DioException( requestOptions: options, - error: 'test error', + cause: 'test error', ), ); break; @@ -443,7 +443,7 @@ void main() { case urlNotFound3: return handler.next( e.copyWith( - error: 'custom error info [${e.response!.statusCode}]', + cause: 'custom error info [${e.response!.statusCode}]', ), ); } diff --git a/dio/test/options_test.dart b/dio/test/options_test.dart index 0a30464c6..8d9987923 100644 --- a/dio/test/options_test.dart +++ b/dio/test/options_test.dart @@ -414,7 +414,7 @@ void main() { // Throws a type error during cast. expectLater( dio.get>('/test-plain-text-content-type'), - throwsA((e) => e is DioException && e.error is TypeError), + throwsA((e) => e is DioException && e.cause is TypeError), ); }); @@ -438,7 +438,7 @@ void main() { void testInvalidArgumentException(String method) async { await expectLater( dio.fetch(RequestOptions(path: 'http://127.0.0.1', method: method)), - throwsA((e) => e is DioException && e.error is ArgumentError), + throwsA((e) => e is DioException && e.cause is ArgumentError), ); } diff --git a/dio/test/stacktrace_test.dart b/dio/test/stacktrace_test.dart index df479ea22..112c34f7e 100644 --- a/dio/test/stacktrace_test.dart +++ b/dio/test/stacktrace_test.dart @@ -11,23 +11,21 @@ import 'mock/http_mock.dart'; import 'mock/http_mock.mocks.dart'; void main() async { - group('$DioException.stackTrace', () { + group('DioException is thrown with correct stackTrace', () { test(DioExceptionType.badResponse, () async { final dio = Dio() ..httpClientAdapter = MockAdapter() ..options.baseUrl = MockAdapter.mockBase; - await expectLater( - dio.get('/foo'), - throwsA( - allOf([ - isA(), - (e) => e.type == DioExceptionType.badResponse, - (e) => - e.stackTrace.toString().contains('test/stacktrace_test.dart'), - ]), - ), - ); + try { + await dio.get('/foo'); + fail('should throw'); + } on DioException catch (e, s) { + expect(e.type, DioExceptionType.badResponse); + expect(s.toString(), contains('test/stacktrace_test.dart')); + } catch (_) { + fail('should throw DioException'); + } }); test(DioExceptionType.cancel, () async { @@ -41,17 +39,15 @@ void main() async { dio.httpClientAdapter.close(force: true); }); - await expectLater( - dio.get('/test-timeout', cancelToken: token), - throwsA( - allOf([ - isA(), - (e) => e.type == DioExceptionType.cancel, - (e) => - e.stackTrace.toString().contains('test/stacktrace_test.dart'), - ]), - ), - ); + try { + await dio.get('/test-timeout', cancelToken: token); + fail('should throw'); + } on DioException catch (e, s) { + expect(e.type, DioExceptionType.cancel); + expect(s.toString(), contains('test/stacktrace_test.dart')); + } catch (_) { + fail('should throw DioException'); + } }); test( @@ -73,18 +69,15 @@ void main() async { }, ); - await expectLater( - dio.get('/test'), - throwsA( - allOf([ - isA(), - (e) => e.type == DioExceptionType.connectionTimeout, - (e) => e.stackTrace - .toString() - .contains('test/stacktrace_test.dart'), - ]), - ), - ); + try { + await dio.get('/test'); + fail('should throw'); + } on DioException catch (e, s) { + expect(e.type, DioExceptionType.connectionTimeout); + expect(s.toString(), contains('test/stacktrace_test.dart')); + } catch (_) { + fail('should throw DioException'); + } }, MockHttpOverrides()); }, testOn: '!browser', @@ -113,18 +106,15 @@ void main() async { }, ); - await expectLater( - dio.get('/test'), - throwsA( - allOf([ - isA(), - (DioException e) => e.type == DioExceptionType.receiveTimeout, - (DioException e) => e.stackTrace - .toString() - .contains('test/stacktrace_test.dart'), - ]), - ), - ); + try { + await dio.get('/test'); + fail('should throw'); + } on DioException catch (e, s) { + expect(e.type, DioExceptionType.receiveTimeout); + expect(s.toString(), contains('test/stacktrace_test.dart')); + } catch (_) { + fail('should throw DioException'); + } }, MockHttpOverrides()); }, testOn: '!browser', @@ -152,18 +142,15 @@ void main() async { }, ); - await expectLater( - dio.get('/test', data: 'some data'), - throwsA( - allOf([ - isA(), - (DioException e) => e.type == DioExceptionType.sendTimeout, - (DioException e) => e.stackTrace - .toString() - .contains('test/stacktrace_test.dart'), - ]), - ), - ); + try { + await dio.get('/test', data: 'some data'); + fail('should throw'); + } on DioException catch (e, s) { + expect(e.type, DioExceptionType.sendTimeout); + expect(s.toString(), contains('test/stacktrace_test.dart')); + } catch (_) { + fail('should throw DioException'); + } }, MockHttpOverrides()); }, testOn: '!browser', @@ -189,72 +176,67 @@ void main() async { }, ); - await expectLater( - dio.get('/test'), - throwsA( - allOf([ - isA(), - (DioException e) => e.type == DioExceptionType.badCertificate, - (DioException e) => e.stackTrace - .toString() - .contains('test/stacktrace_test.dart'), - ]), - ), - ); + try { + await dio.get('/test'); + fail('should throw'); + } on DioException catch (e, s) { + expect(e.type, DioExceptionType.badCertificate); + expect(s.toString(), contains('test/stacktrace_test.dart')); + } catch (_) { + fail('should throw DioException'); + } }, MockHttpOverrides()); }, testOn: '!browser', ); - group('DioExceptionType.connectionError', () { + + group(DioExceptionType.connectionError, () { test( 'SocketException on request', () async { final dio = Dio() ..options.baseUrl = 'https://does.not.exist' ..httpClientAdapter = IOHttpClientAdapter(); - await expectLater( - dio.get('/test', data: 'test'), - throwsA( - allOf([ - isA(), - (e) => e.type == DioExceptionType.connectionError, - (e) => e.error is SocketException, - (e) => (e.error as SocketException) - .message - .contains("Failed host lookup: 'does.not.exist'"), - (e) => e.stackTrace - .toString() - .contains('test/stacktrace_test.dart'), - ]), - ), - ); + + try { + await dio.get('/test', data: 'test'); + fail('should throw'); + } on DioException catch (e, s) { + expect(e.type, DioExceptionType.connectionError); + expect(e.cause, isA()); + expect( + (e.cause as SocketException).message, + contains("Failed host lookup: 'does.not.exist'"), + ); + expect(s.toString(), contains('test/stacktrace_test.dart')); + } catch (_) { + fail('should throw DioException'); + } }, testOn: 'vm', ); }); - group('DioExceptionType.unknown', () { + + group(DioExceptionType.unknown, () { test( JsonUnsupportedObjectError, () async { final dio = Dio()..options.baseUrl = 'https://does.not.exist'; - await expectLater( - dio.get( + try { + await dio.get( '/test', options: Options(contentType: Headers.jsonContentType), data: Object(), - ), - throwsA( - allOf([ - isA(), - (DioException e) => e.type == DioExceptionType.unknown, - (DioException e) => e.error is JsonUnsupportedObjectError, - (DioException e) => e.stackTrace - .toString() - .contains('test/stacktrace_test.dart'), - ]), - ), - ); + ); + fail('should throw'); + } on DioException catch (e, s) { + expect(e.type, DioExceptionType.unknown); + expect(e.cause, isA()); + expect(s.toString(), contains('test/stacktrace_test.dart')); + } catch (_) { + fail('should throw DioException'); + } }, testOn: '!browser', ); @@ -280,19 +262,16 @@ void main() async { }, ); - await expectLater( - dio.get('/test', data: 'test'), - throwsA( - allOf([ - isA(), - (e) => e.type == DioExceptionType.unknown, - (e) => e.error is SocketException, - (e) => e.stackTrace - .toString() - .contains('test/stacktrace_test.dart'), - ]), - ), - ); + try { + await dio.get('/test', data: 'test'); + fail('should throw'); + } on DioException catch (e, s) { + expect(e.type, DioExceptionType.unknown); + expect(e.cause, isA()); + expect(s.toString(), contains('test/stacktrace_test.dart')); + } catch (_) { + fail('should throw DioException'); + } }, testOn: 'vm', ); @@ -303,34 +282,33 @@ void main() async { ..options.baseUrl = EchoAdapter.mockBase ..httpClientAdapter = EchoAdapter(); - StackTrace? caughtStackTrace; + // StackTrace? caughtStackTrace; dio.interceptors.addAll([ InterceptorsWrapper( onError: (err, handler) { - caughtStackTrace = err.stackTrace; + // caughtStackTrace = err.bestStackTrace; handler.next(err); }, ), InterceptorsWrapper( onRequest: (options, handler) { - final error = DioException(error: Error(), requestOptions: options); + final error = DioException(cause: Error(), requestOptions: options); handler.reject(error, true); }, ), ]); - await expectLater( - dio.get('/error'), - throwsA( - allOf([ - isA(), - (e) => e.stackTrace == caughtStackTrace, - (e) => - e.stackTrace.toString().contains('test/stacktrace_test.dart'), - ]), - ), - reason: 'Stacktrace should be available in onError', - ); + try { + await dio.get('/error'); + fail('should throw'); + } on DioException catch (e, s) { + expect(e.type, DioExceptionType.unknown); + expect(e.cause, isA()); + expect(s.toString(), contains('test/stacktrace_test.dart')); + // expect(s, caughtStackTrace); + } catch (_) { + fail('should throw DioException'); + } }); test('QueuedInterceptor gets stacktrace in onError', () async { @@ -338,18 +316,19 @@ void main() async { ..options.baseUrl = EchoAdapter.mockBase ..httpClientAdapter = EchoAdapter(); - StackTrace? caughtStackTrace; + // StackTrace? caughtStackTrace; dio.interceptors.addAll([ QueuedInterceptorsWrapper( onError: (err, handler) { - caughtStackTrace = err.stackTrace; + // TODO should we get better access to the stacktrace here? + // caughtStackTrace = err.bestStackTrace; handler.next(err); }, ), QueuedInterceptorsWrapper( onRequest: (options, handler) { final error = DioException( - error: Error(), + cause: Error(), requestOptions: options, ); handler.reject(error, true); @@ -357,18 +336,17 @@ void main() async { ), ]); - await expectLater( - dio.get('/error'), - throwsA( - allOf([ - isA(), - (e) => e.stackTrace == caughtStackTrace, - (e) => - e.stackTrace.toString().contains('test/stacktrace_test.dart'), - ]), - ), - reason: 'Stacktrace should be available in onError', - ); + try { + await dio.get('/error'); + fail('should throw'); + } on DioException catch (e, s) { + expect(e.type, DioExceptionType.unknown); + expect(e.cause, isA()); + expect(s.toString(), contains('test/stacktrace_test.dart')); + // expect(s, caughtStackTrace); + } catch (_) { + fail('should throw DioException'); + } }); }); } diff --git a/example/lib/request_interceptors.dart b/example/lib/request_interceptors.dart index d29e967cf..b3220d791 100644 --- a/example/lib/request_interceptors.dart +++ b/example/lib/request_interceptors.dart @@ -25,7 +25,7 @@ void main() async { return handler.reject( DioException( requestOptions: options, - error: 'test error', + cause: 'test error', ), ); default: diff --git a/example/lib/response_interceptor.dart b/example/lib/response_interceptor.dart index 76e5de827..7ce5167f5 100644 --- a/example/lib/response_interceptor.dart +++ b/example/lib/response_interceptor.dart @@ -39,7 +39,7 @@ void main() async { case urlNotFound3: handler.next( e.copyWith( - error: 'custom error info [${e.response!.statusCode}]', + cause: 'custom error info [${e.response!.statusCode}]', ), ); break; diff --git a/example/lib/transformer.dart b/example/lib/transformer.dart index 1673e251c..489d321a9 100644 --- a/example/lib/transformer.dart +++ b/example/lib/transformer.dart @@ -12,7 +12,7 @@ class MyTransformer extends BackgroundTransformer { Future transformRequest(RequestOptions options) async { if (options.data is List) { throw DioException( - error: "Can't send List to sever directly", + cause: "Can't send List to sever directly", requestOptions: options, ); } else { diff --git a/plugins/cookie_manager/lib/src/cookie_mgr.dart b/plugins/cookie_manager/lib/src/cookie_mgr.dart index 271926a27..01f0e2959 100644 --- a/plugins/cookie_manager/lib/src/cookie_mgr.dart +++ b/plugins/cookie_manager/lib/src/cookie_mgr.dart @@ -63,7 +63,7 @@ class CookieManager extends Interceptor { }).catchError((dynamic e, StackTrace s) { final err = DioException( requestOptions: options, - error: e, + cause: e, stackTrace: s, ); handler.reject(err, true); @@ -76,7 +76,7 @@ class CookieManager extends Interceptor { (dynamic e, StackTrace s) { final err = DioException( requestOptions: response.requestOptions, - error: e, + cause: e, stackTrace: s, ); handler.reject(err, true); @@ -91,7 +91,7 @@ class CookieManager extends Interceptor { (dynamic e, StackTrace s) { final error = DioException( requestOptions: err.response!.requestOptions, - error: e, + cause: e, stackTrace: s, ); handler.next(error); diff --git a/plugins/http2_adapter/lib/src/connection_manager_imp.dart b/plugins/http2_adapter/lib/src/connection_manager_imp.dart index 1ef0bd5d7..1ef00bf5f 100644 --- a/plugins/http2_adapter/lib/src/connection_manager_imp.dart +++ b/plugins/http2_adapter/lib/src/connection_manager_imp.dart @@ -103,7 +103,7 @@ class _ConnectionManager implements ConnectionManager { throw DioException( requestOptions: options, type: DioExceptionType.badCertificate, - error: socket.peerCertificate, + cause: socket.peerCertificate, message: 'The certificate of the response is not approved.', ); } @@ -177,7 +177,7 @@ class _ConnectionManager implements ConnectionManager { Never onProxyError(Object? error, StackTrace stackTrace) { throw DioException( requestOptions: options, - error: error, + cause: error, type: DioExceptionType.connectionError, stackTrace: stackTrace, );