skip to Main Content

So, I have an interceptor set for api calls. It looks like this:

class AuthorizationInterceptor extends Interceptor {
  @override
  void onRequest(
      RequestOptions options, RequestInterceptorHandler handler) async {
    if (options.headers.containsKey('requiresToken') &&
        options.headers['requiresToken'] == false) {
      options.headers.remove('requiresToken');

      super.onRequest(options, handler);
    } else {
      String token = await SecureStorage.loadAccessToken();

      options.headers['Authorization'] = 'Bearer $token';
      // options.headers['Content-Type'] = 'application/json';

      super.onRequest(options, handler);
    }
  }

  @override
  void onError(DioError err, ErrorInterceptorHandler handler) async {
    if (err.response?.statusCode == 401) {
      log('++++++ interceptor error ++++++');

      if (await SecureStorage.loadAccessToken() == '') {
        super.onError(err, handler);
        return;
      }

      bool isTokenRefreshed = await AuthApi.refreshToken();

      if (isTokenRefreshed) {
        RequestOptions origin = err.response!.requestOptions;

        String token = await SecureStorage.loadAccessToken();
        origin.headers["Authorization"] = "Bearer $token";

        try {
          final Response response = await DioClient.request(
            url: origin.path,
            data: origin.data,
            options: Options(
              headers: origin.headers,
              method: origin.method,
            ),
          );

          handler.resolve(response);
        } catch (e) {
          super.onError(err, handler);
        }
      }
    } else {
      super.onError(err, handler);
      return;
    }
  }
}

Now, when I’m calling some api with dio GET method and the token is expired, onError interceptor handles 401 and refreshes the token. After that the request that was previously called continues and everything finishes fine.

But, when I try to do the exact thing using dio POST it doesn’t work as expected. If there’s a 401 response code it should go through onError and refresh the token and then continue to call previously called POST function which looks like this:

static Future uploadImage(PlatformFile image, String disclaimer,
      {String? imageTitle}) async {
    String imageExtension = image.extension!;
    String imageName = '${imageTitle ?? 'image'}.$imageExtension';

    final formData = FormData.fromMap({
      'upload_file': MultipartFile.fromBytes(
        image.bytes!,
        filename: imageName,
        contentType: MediaType('media_content', imageExtension),
      ),
      'disclaimer': disclaimer,
    });

    try {
      final response = await DioClient.post(
        url: Endpoint.images,
        data: formData,
        options: Options(
          headers: {
            'Content-Type': 'multipart/form-data',
          },
        ),
      );

      return response.data;
    } on DioError catch (err) {
      ToastMessage.apiError(err);
      log('DioError uploadImage response: ${ToastMessage.message}');
    }
  }

This is one of the functions, as many others I use, that works fine:

 static Future getPosts(
      {required int page,
      int? pageSize,
      String? searchParam,
      String? status,
      String? categoryId}) async {
    try {
      final response = await DioClient.get(
        url: Endpoint.getPosts,
        query: {
          'page': page,
          if (pageSize != null) 'page_size': pageSize,
          if (status != null) 'status': status,
          if (searchParam != null) 'search_param': searchParam,
          if (categoryId != null) 'category_id': categoryId,
        },
      );

      return response.data;
    } on DioError catch (err) {
      ToastMessage.apiError(err);
      log('DioError get posts response: ${ToastMessage.message}');
    }
  }

I tried everything so far. Everything I do looks like this:

When calling dio GET functions and the response is 401 this is the flow in logs:

  • DioError is caught and it enters onError of the interceptor
  • checks if error is 401 and refreshes the token
  • loads the token and calls initial GET function again and returns expected values

When calling dio POST (above uploadImage function):

  • if response is 401 IT DOESN’T enter onError of the interceptor but immediately calls ToastMessage and shows the user the upload process wasn’t finish (which it really wasn’t)
  • after this happens THEN IT ENTERS onError interceptor and refreshes the token

So, my question would probably be:

Why is onError of the DioError interceptor not called if the response code is 401 in POST function but is called in GET functions?

UPDATE:

When 401 is the response of the uploadImage function this is the flow:

  • it enters the interceptor
  • refreshes the token
  • after successful token refresh it enters try block and retries to call uploadImage again with correct request options
  • SUDDENLY IT JUMPS BACK AT THE TOP of the onError interceptor (this implies that try block didn’t pass eventhough I got no errors of any kind)
  • goes back to uploadImage’s DioError and returns ToastMessage

In my IDE’s logs I see that the call in this try block is done but in my browser’s network inspection nothing happens. I just have 401 response from uploadImage BE and 200 response for refresh token response and no retried uploadImage call.

UPDATE 2:

My issue is the same as described here

2

Answers


  1. Chosen as BEST ANSWER

    After some research I found out that multipart files are Stream and they need to be re-instantiated when retrying an API call after token refresh.

    So, I managed to solve my problem and these are updated functions in case someone else stumble upon this problem.

    static Future uploadImage(PlatformFile image, String disclaimer,
          {String? imageTitle}) async {
        String imageExtension = image.extension!;
        String imageName = '${imageTitle ?? 'image'}.$imageExtension';
    
        final formData = FormData.fromMap({
          'upload_file': MultipartFile.fromBytes(
            image.bytes!,
            filename: imageName,
            contentType: MediaType('media_content', imageExtension),
          ),
          'disclaimer': disclaimer,
        });
    
        try {
          final response = await DioClient.post(
            url: Endpoint.images,
            data: formData,
            options: Options(
              headers: {
                'Content-Type': 'multipart/form-data',
              },
              // Added this extra key to send image data in the request body
              extra: {
                'image': {
                  'imageBytes': image.bytes,
                  'filename': imageName,
                  'imageExtension': imageExtension,
                  'disclaimer': disclaimer,
                },
              },
            ),
          );
    
          return response.data;
        } on DioError catch (err) {
          ToastMessage.apiError(err);
          log('DioError uploadImage response: ${ToastMessage.message}');
        }
      }
    

    Then this is new interceptor onError part:

    class AuthorizationInterceptor extends Interceptor {
      
      // onRequest
    
      @override
      void onError(DioError err, ErrorInterceptorHandler handler) async {
        if (err.response?.statusCode == 401) {
          log('++++++ interceptor error ++++++');
    
          if (await SecureStorage.loadAccessToken() == '') {
            super.onError(err, handler);
            return;
          }
    
          bool isTokenRefreshed = await AuthApi.refreshToken();
    
          if (isTokenRefreshed) {
            RequestOptions origin = err.requestOptions;
    
            String token = await SecureStorage.loadAccessToken();
            origin.headers["Authorization"] = "Bearer $token";
    
            final Options options = Options(
              method: origin.method,
              headers: origin.headers,
            );
    
            try {
              // If the request is a file upload, we need to re-initialize the file
              if (origin.extra.containsKey('image')) {
                origin.data =
                    FormDataHandler.uploadImageData(origin.extra['image']);
              }
    
              final Response retryResponse = await DioClient.request(
                url: origin.path,
                data: origin.data,
                query: origin.queryParameters,
                options: options,
              );
    
              return handler.resolve(retryResponse);
            } catch (e) {
              super.onError(err, handler);
            }
          }
        } else {
          super.onError(err, handler);
          return;
        }
      }
    }
    

    The file that keeps FormDataHandler:

    class FormDataHandler {
      static FormData uploadImageData(Map<String, dynamic> imageData) {
        return FormData.fromMap({
          'upload_file': MultipartFile.fromBytes(
            imageData['imageBytes'],
            filename: imageData['filename'],
            contentType: MediaType('media_content', imageData['imageExtension']),
          ),
          'disclaimer': imageData['disclaimer'],
        });
      }
    }
    

  2. I’m not sure but I’ve just checked my implementation on 401 handling and I use:

    RequestOptions origin = err.requestOptions;
    

    instead:

    RequestOptions origin = err.response!.requestOptions;
    

    here is part from my code

     final Options newOptions = Options(
          method: err.requestOptions.method,
          headers: headers,
        );
    
        try {
          final Response<dynamic> newResponse = await _dio.request(
            err.requestOptions.path,
            data: err.requestOptions.data,
            queryParameters: err.requestOptions.queryParameters,
            options: newOptions,
          );
    
          handler.resolve(newResponse);
        } on DioError catch (err) {
          handler.next(err);
        }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search