dio
dio copied to clipboard
Error inside interceptor is missing full stacktrace
New Issue Checklist
- [x] I have searched for a similar issue in the project and found none
Issue Info
| Info | Value |
|---|---|
| Platform Name | Android & iOS |
| Platform Version | any |
| Dio Version | 4.0.0 |
| Repro rate | all the time (100%) |
| Demo project link | PiN73/test_dio_interceptor |
Issue Description and Steps
Consider this code
import 'package:dio/dio.dart';
void main() async {
final dio = Dio()..interceptors.add(ProblemInterceptor());
await dio.get('https://baidu.com/');
}
class ProblemInterceptor extends Interceptor {
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
throw Exception('Unexpected problem inside onResponse');
}
}
Here we have an interceptor in one place and using it in another.
As interceptor contains error, it is thrown during response handling:
% dart main.dart
Unhandled exception:
DioError [DioErrorType.other]: Exception: Unexpected problem inside onResponse
#0 ProblemInterceptor.onResponse (file:///test_dio_interceptor/main.dart:11:5)
#1 DioMixin.fetch._responseInterceptorWrapper.<anonymous closure>.<anonymous closure>.<anonymous closure> (package:dio/src/dio_mixin.dart:526:28)
#2 DioMixin.checkIfNeedEnqueue (package:dio/src/dio_mixin.dart:795:22)
#3 DioMixin.fetch._responseInterceptorWrapper.<anonymous closure>.<anonymous closure> (package:dio/src/dio_mixin.dart:524:22)
#4 new Future.<anonymous closure> (dart:async/future.dart:174:37)
#5 Timer._createTimer.<anonymous closure> (dart:async-patch/timer_patch.dart:18:15)
#6 _Timer._runTimers (dart:isolate-patch/timer_impl.dart:395:19)
#7 _Timer._handleMessage (dart:isolate-patch/timer_impl.dart:426:5)
#8 _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:184:12)
#0 DioMixin.fetch.<anonymous closure> (package:dio/src/dio_mixin.dart:618:7)
#1 _RootZone.runBinary (dart:async/zone.dart:1618:54)
#2 _FutureListener.handleError (dart:async/future_impl.dart:169:20)
#3 Future._propagateToListeners.handleError (dart:async/future_impl.dart:719:47)
#4 Future._propagateToListeners (dart:async/future_impl.dart:740:24)
#5 Future._completeError (dart:async/future_impl.dart:550:5)
#6 _SyncCompleter._completeError (dart:async/future_impl.dart:61:12)
#7 _Completer.completeError (dart:async/future_impl.dart:33:5)
#8 Future.any.onError (dart:async/future.dart:466:45)
#9 _RootZone.runBinary (dart:async/zone.dart:1618:54)
#10 _FutureListener.handleError (dart:async/future_impl.dart:169:20)
#11 Future._propagateToListeners.handleError (dart:async/future_impl.dart:719:47)
#12 Future._propagateToListeners (dart:async/future_impl.dart:740:24)
#13 Future._completeError (dart:async/future_impl.dart:550:5)
#14 Future._asyncCompleteError.<anonymous closure> (dart:async/future_impl.dart:606:7)
#15 _microtaskLoop (dart:async/schedule_microtask.dart:40:21)
#16 _startMicrotaskLoop (dart:async/schedule_microtask.dart:49:5)
#17 _runPendingImmediateCallback (dart:isolate-patch/isolate_patch.dart:120:13)
#18 _Timer._runTimers (dart:isolate-patch/timer_impl.dart:402:11)
#19 _Timer._handleMessage (dart:isolate-patch/timer_impl.dart:426:5)
#20 _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:184:12)
But the error/stacktrace doesn't contain any information about the line where request is made from:
5 | await dio.get('https://baidu.com/');
This complicates debugging.
If the stacktrace contained something like this
...
<asynchronous suspension>
#_ main (file:///test_dio_interceptor/main.dart:5:3)
it would be much more helpful for debugging.
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. If this is still an issue, please make sure it is up to date and if so, add a comment that this is still an issue to keep it open. Thank you for your contributions.
Unstale
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. If this is still an issue, please make sure it is up to date and if so, add a comment that this is still an issue to keep it open. Thank you for your contributions.
This is still an issue
You can implement your own Dio for this purpose:
Example
import 'dart:async';
import 'dart:convert';
import 'dart:math' as math;
import 'dart:typed_data';
import 'package:dio/adapter.dart';
import 'package:dio/adapter_browser.dart';
import 'package:dio/dio.dart';
//ignore: implementation_imports
import 'package:dio/src/interceptor.dart';
import 'package:flutter/foundation.dart';
class CustomDio with DioMixin implements Dio {
CustomDio([BaseOptions? baseOptions]) {
options = baseOptions ?? BaseOptions();
httpClientAdapter =
kIsWeb ? BrowserHttpClientAdapter() : DefaultHttpClientAdapter();
}
@override
Future<Response<T>> fetch<T>(RequestOptions requestOptions) async {
final stacktrace = StackTrace.current;
if (requestOptions.cancelToken != null) {
requestOptions.cancelToken!.requestOptions = requestOptions;
}
if (T != dynamic &&
!(requestOptions.responseType == ResponseType.bytes ||
requestOptions.responseType == ResponseType.stream)) {
if (T == String) {
requestOptions.responseType = ResponseType.plain;
} else {
requestOptions.responseType = ResponseType.json;
}
}
// Convert the request interceptor to a functional callback in which
// we can handle the return value of interceptor callback.
FutureOr Function(dynamic) _requestInterceptorWrapper(
void Function(RequestOptions options, RequestInterceptorHandler handler)
interceptor) {
return (dynamic _state) async {
final state = _state as InterceptorState;
if (state.type == InterceptorResultType.next) {
return DioMixin.listenCancelForAsyncTask(
requestOptions.cancelToken,
Future(() {
return DioMixin.checkIfNeedEnqueue(interceptors.requestLock, () {
final requestHandler = RequestInterceptorHandler();
interceptor(state.data, 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<dynamic> _responseInterceptorWrapper(
InterceptorState<Response<dynamic>> state,
void Function(Response response, ResponseInterceptorHandler handler)
interceptor,
) async {
// final InterceptorState state = inter as InterceptorState;
if (state.type == InterceptorResultType.next ||
state.type == InterceptorResultType.resolveCallFollowing) {
return DioMixin.listenCancelForAsyncTask(
requestOptions.cancelToken,
Future(() {
return DioMixin.checkIfNeedEnqueue(interceptors.responseLock, () {
final responseHandler = ResponseInterceptorHandler();
interceptor(state.data, responseHandler);
return responseHandler.future;
});
}),
);
} else {
return state;
}
}
// Build a request flow in which the processors(interceptors)
// execute in FIFO order.
// Start the request flow
var future = Future<dynamic>(() => InterceptorState(requestOptions));
// Add request interceptors to request flow
for (final interceptor in interceptors) {
future = future.then(_requestInterceptorWrapper(interceptor.onRequest));
}
// Add dispatching callback to request flow
future = future.then(_requestInterceptorWrapper((
RequestOptions reqOpt,
RequestInterceptorHandler handler,
) {
requestOptions = reqOpt;
_dispatchRequest(reqOpt).then(
(value) => handler.resolve(value, true),
onError: (e) => handler.reject(e, true),
);
}));
// Add response interceptors to request flow
for (final interceptor in interceptors) {
future = future.then((value) =>
_responseInterceptorWrapper(value, interceptor.onResponse));
}
// Add error handlers to request flow
FutureOr Function(dynamic, StackTrace stackTrace) _errorInterceptorWrapper(
void Function(DioError err, ErrorInterceptorHandler handler) interceptor,
) {
Future<dynamic> func(dynamic err, StackTrace stackTrace) {
final InterceptorState error;
if (err is! InterceptorState) {
error = InterceptorState(DioMixin.assureDioError(
err,
requestOptions,
stackTrace,
));
} else {
error = err;
}
if (error.type == InterceptorResultType.next ||
error.type == InterceptorResultType.rejectCallFollowing) {
return DioMixin.listenCancelForAsyncTask(
requestOptions.cancelToken,
Future(() {
return DioMixin.checkIfNeedEnqueue(interceptors.errorLock, () {
final errorHandler = ErrorInterceptorHandler();
interceptor(error.data, errorHandler);
return errorHandler.future;
});
}),
);
} else {
throw err;
}
}
return func;
}
for (final interceptor in interceptors) {
future = future.catchError((val, trace) =>
_errorInterceptorWrapper(interceptor.onError).call(val, trace));
}
// Normalize errors, we convert error to the DioError
return future.then<Response<T>>((data) {
return DioMixin.assureResponse<T>(
data is InterceptorState ? data.data : data,
requestOptions,
);
}).catchError((err, _) {
final isState = err is InterceptorState;
if (isState) {
if ((err as InterceptorState).type == InterceptorResultType.resolve) {
return DioMixin.assureResponse<T>(err.data, requestOptions);
}
}
throw DioMixin.assureDioError(
isState ? err.data : err,
requestOptions,
stacktrace,
);
});
}
Future<Response<T>> _dispatchRequest<T>(RequestOptions reqOpt) async {
final cancelToken = reqOpt.cancelToken;
ResponseBody responseBody;
try {
final stream = await _transformData(reqOpt);
responseBody = await httpClientAdapter.fetch(
reqOpt,
stream,
cancelToken?.whenCancel,
);
responseBody.headers = responseBody.headers;
final headers = Headers.fromMap(responseBody.headers);
final ret = Response(
headers: headers,
requestOptions: reqOpt,
redirects: responseBody.redirects ?? [],
isRedirect: responseBody.isRedirect,
statusCode: responseBody.statusCode,
statusMessage: responseBody.statusMessage,
extra: responseBody.extra,
);
final statusOk = reqOpt.validateStatus(responseBody.statusCode);
if (statusOk || reqOpt.receiveDataWhenStatusError == true) {
final forceConvert = !(T == dynamic || T == String) &&
!(reqOpt.responseType == ResponseType.bytes ||
reqOpt.responseType == ResponseType.stream);
String? contentType;
if (forceConvert) {
contentType = headers.value(Headers.contentTypeHeader);
headers.set(Headers.contentTypeHeader, Headers.jsonContentType);
}
ret.data = await transformer.transformResponse(reqOpt, responseBody);
if (forceConvert) {
headers.set(Headers.contentTypeHeader, contentType);
}
} else {
await responseBody.stream.listen(null).cancel();
}
DioMixin.checkCancelled(cancelToken);
if (statusOk) {
return DioMixin.checkIfNeedEnqueue(interceptors.responseLock, () => ret)
as Response<T>;
} else {
throw DioError(
requestOptions: reqOpt,
response: ret,
error: 'Http status error [${responseBody.statusCode}]',
type: DioErrorType.response,
);
}
} catch (e) {
throw DioMixin.assureDioError(e, reqOpt);
}
}
Future<Stream<Uint8List>?> _transformData(RequestOptions options) async {
final data = options.data;
List<int> bytes;
Stream<List<int>> stream;
const allowPayloadMethods = ['POST', 'PUT', 'PATCH', 'DELETE'];
if (data != null && allowPayloadMethods.contains(options.method)) {
// Handle the FormData
int? length;
if (data is Stream) {
assert(data is Stream<List>,
'Stream type must be `Stream<List>`, but ${data.runtimeType} is found.');
stream = data as Stream<List<int>>;
options.headers.keys.any((String key) {
if (key.toLowerCase() == Headers.contentLengthHeader) {
length = int.parse(options.headers[key].toString());
return true;
}
return false;
});
} else if (data is FormData) {
options.headers[Headers.contentTypeHeader] =
'multipart/form-data; boundary=${data.boundary}';
stream = data.finalize();
length = data.length;
options.headers[Headers.contentLengthHeader] = length.toString();
} else {
// Call request transformer.
final _data = await transformer.transformRequest(options);
if (options.requestEncoder != null) {
bytes = options.requestEncoder!(_data, options);
} else {
//Default convert to utf8
bytes = utf8.encode(_data);
}
// support data sending progress
length = bytes.length;
options.headers[Headers.contentLengthHeader] = length.toString();
final group = <List<int>>[];
const size = 1024;
final groupCount = (bytes.length / size).ceil();
for (var i = 0; i < groupCount; ++i) {
final start = i * size;
group.add(bytes.sublist(start, math.min(start + size, bytes.length)));
}
stream = Stream.fromIterable(group);
}
var complete = 0;
final byteStream =
stream.transform<Uint8List>(StreamTransformer.fromHandlers(
handleData: (data, sink) {
final cancelToken = options.cancelToken;
if (cancelToken != null && cancelToken.isCancelled) {
cancelToken.requestOptions = options;
sink
..addError(cancelToken.cancelError!)
..close();
} else {
sink.add(Uint8List.fromList(data));
if (length != null) {
complete += data.length;
if (options.onSendProgress != null) {
options.onSendProgress!(complete, length!);
}
}
}
},
));
if (options.sendTimeout > 0) {
byteStream.timeout(Duration(milliseconds: options.sendTimeout),
onTimeout: (sink) {
sink.addError(DioError(
requestOptions: options,
error: 'Sending timeout[${options.connectTimeout}ms]',
type: DioErrorType.sendTimeout,
));
sink.close();
});
}
return byteStream;
}
return null;
}
}
The main changes compared to original implementation are
final stacktrace = StackTrace.current; // line 23
// ...
throw DioMixin.assureDioError( // line 173
isState ? err.data : err,
requestOptions,
stacktrace,
);
Still an issue in 4.0.4.
In interceptor, the stack trace is null.
In my code, inside on DioError catch (e), e.stackStrace is not null, and has the correct stack trace.
Need help! Still an issue.
Seems like #1503 should fix it
Please reopen this issue. My PR #1721 fixes this, but so far this is stil an issue.