所以,我为 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 时,这是日志中的流程:
调用dio POST时(上面的uploadImage函数):
所以,我的问题可能是:
为什么在POST函数中响应码为401而在GET函数中调用了DioError拦截器的onError却没有被调用?
更新:
当 401 是 uploadImage 函数的响应时,流程如下:
在我的 IDE 日志中,我看到此 try 块中的调用已完成,但在浏览器的网络检查中没有任何反应。我只有来自 uploadImage BE 的 401 响应和刷新令牌响应的 200 响应,并且没有重试 uploadImage 调用。
更新2:
我的问题与此处
描述的相同经过一些研究,我发现多部分文件是 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'],
});
}
}
我不确定,但我刚刚检查了 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);
}
.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);
}
}