Flutter Dio 在 Interceptor 的 onError 方法中令牌刷新后不会重新调用 POST 方法并上传文件

问题描述 投票:0回答:3

所以,我为 api 调用设置了一个拦截器。看起来像这样:

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;
    }
  }
}

现在,当我使用 dio GET 方法调用某些 api 并且令牌已过期时,onError 拦截器会处理 401 并刷新令牌。之后,之前调用的请求继续,一切正常完成。

但是,当我尝试使用 dio POST 执行确切的操作时,它无法按预期工作。如果有 401 响应代码,它应该经历 onError 并刷新令牌,然后继续调用之前调用的 POST 函数,如下所示:

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}');
    }
  }

与我使用的许多其他功能一样,这是功能之一,效果很好:

 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}');
    }
  }

到目前为止我已经尝试了一切。我所做的一切看起来都是这样的:

当调用 dio GET 函数且响应为 401 时,这是日志中的流程:

  • DioError被捕获并进入拦截器的onError
  • 检查错误是否为 401 并刷新令牌
  • 加载令牌并再次调用初始 GET 函数并返回预期值

调用dio POST时(上面的uploadImage函数):

  • 如果响应为 401,则不会进入拦截器的 onError,而是立即调用 ToastMessage 并向用户显示上传过程尚未完成(实际上并未完成)
  • 发生这种情况后,它会进入 onError 拦截器并刷新令牌

所以,我的问题可能是:

为什么在POST函数中响应码为401而在GET函数中调用了DioError拦截器的onError却没有被调用?

更新:

当 401 是 uploadImage 函数的响应时,流程如下:

  • 进入拦截器
  • 刷新令牌
  • 成功刷新令牌后,它会进入 try 块并使用正确的请求选项再次尝试调用 uploadImage
  • 它突然跳回 onError 拦截器的顶部(这意味着即使我没有收到任何类型的错误,try 块也没有通过)
  • 返回uploadImage的DioError并返回ToastMessage

在我的 IDE 日志中,我看到此 try 块中的调用已完成,但在浏览器的网络检查中没有任何反应。我只有来自 uploadImage BE 的 401 响应和刷新令牌响应的 200 响应,并且没有重试 uploadImage 调用。

更新2:

我的问题与此处

描述的相同
flutter post http-status-code-401 refresh-token dio
3个回答
1
投票

经过一些研究,我发现多部分文件是 Stream,在令牌刷新后重试 API 调用时需要重新实例化它们。

所以,我设法解决了我的问题,这些是更新的功能,以防其他人偶然发现这个问题。

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}');
    }
  }

然后这是新的拦截器 onError 部分:

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;
    }
  }
}

保存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'],
    });
  }
}

0
投票

我不确定,但我刚刚检查了 401 处理的实现,我使用:

RequestOptions origin = err.requestOptions;

相反:

RequestOptions origin = err.response!.requestOptions;

这是我的代码的一部分

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

0
投票

.clone()
上有一个新的
MultipartFile
方法,正是针对这种情况引入的。

这就是我自己解决这个问题的方法。我创建了一个 Api 客户端类,在我的存储库类中使用它,并使用 dio 拦截器来更新身份验证令牌。


// This is the function I call in the dio interceptor in the onError case
// You will most likely have to adjust this to your own needs
Future<void> _refreshAndRedoRequest(
    DioException exception, ErrorInterceptorHandler handler) async {
  try {
    logger.d('Refreshing token and retrying request');
    // This is my custom method to renew the tokens. You can replace this with your own.
    await authStateNotifier.renewTokens();
    _setAuthHeader(exception.requestOptions);

    // Clone the original request
    final requestOptions = exception.requestOptions;
    final requestClone = requestOptions.copyWith();

    // We are going to store the formData in here
    final dynamic retryFormData;

    // Check if the request is a file upload
    if (requestOptions.data is FormData) {
      // Get the FormData instance from options.data
      final oldFormData = requestOptions.data as FormData;

      // Create a new FormData instance
      final newFormData = FormData();

      // Clone the fields
      newFormData.fields.addAll(oldFormData.fields);

      // Clone the files
      for (var fileEntry in oldFormData.files) {
        newFormData.files.add(MapEntry(
          fileEntry.key,
          // This is necessary because we otherwise get a "Bad state: Can't finalize a finalized MultipartFile" error
          // See: https://github.com/cfug/dio/issues/482
          // This is the clone method I am talking about.
          fileEntry.value.clone(),
        ));
      }

      retryFormData = newFormData;
    } else {
      // Not a file upload, just copy the old request data
      retryFormData = requestClone.data;
    }

    // Manually send the request again
    try {
      // Create a new Dio instance for the retried request
      // This is necessary because otherwise if there is an error in the retry
      // the request and all future requests will be stuck unless you restart the app.
      final dioForRetry = Dio(baseOptions);

      final response = await dioForRetry.request(
        requestClone.path,
        cancelToken: requestClone.cancelToken,
        data: retryFormData,
        onReceiveProgress: requestClone.onReceiveProgress,
        onSendProgress: requestClone.onSendProgress,
        queryParameters: requestClone.queryParameters,
        options: Options(
          method: requestClone.method,
          headers: requestClone.headers,
          contentType: requestClone.contentType,
        ),
      );

      // If the request is successful, resolve it
      handler.resolve(response);
    } catch (e) {
      logger.d('Error in retrying request: $e');
      // If the request fails, reject it
      final exception = DioException.badResponse(
          statusCode: 401,
          requestOptions: RequestOptions(),
          response: Response(requestOptions: RequestOptions()));
      handler.reject(exception);
    }
  } catch (e) {
    logger.d('Error in renewing tokens: $e');
    final exception = DioException.badResponse(
        statusCode: 401,
        requestOptions: RequestOptions(),
        response: Response(requestOptions: RequestOptions()));
    handler.reject(exception);
  }
}
© www.soinside.com 2019 - 2024. All rights reserved.