dio icon indicating copy to clipboard operation
dio copied to clipboard

Error inside interceptor is missing full stacktrace

Open PiN73 opened this issue 4 years ago • 8 comments

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.

PiN73 avatar May 27 '21 01:05 PiN73

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.

stale[bot] avatar Jun 26 '21 02:06 stale[bot]

Unstale

PiN73 avatar Jun 26 '21 10:06 PiN73

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.

stale[bot] avatar Jul 30 '21 04:07 stale[bot]

This is still an issue

PiN73 avatar Aug 05 '21 12:08 PiN73

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,
);

Andreigr0 avatar Sep 01 '21 12:09 Andreigr0

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.

fernando-s97 avatar Jan 24 '22 21:01 fernando-s97

Need help! Still an issue.

rahul-2501 avatar May 03 '22 04:05 rahul-2501

Seems like #1503 should fix it

HugoHeneault avatar Aug 01 '22 11:08 HugoHeneault

Please reopen this issue. My PR #1721 fixes this, but so far this is stil an issue.

lohnn avatar Mar 07 '23 09:03 lohnn