cfug / dio

A powerful HTTP client for Dart and Flutter, which supports global settings, Interceptors, FormData, aborting and canceling a request, files uploading and downloading, requests timeout, custom adapters, etc.
https://dio.pub
MIT License
12.47k stars 1.51k forks source link

Files cannot be deleted if network exceptions are thrown #1920

Closed feimenggo closed 10 months ago

feimenggo commented 1 year ago

Package

dio

Version

5.3.0

Output of flutter doctor -v

No response

Dart Version

3.10.6

Steps to Reproduce

1.在Windows平台,使用dio上传文件 dio.requestUri(uri, data: file.openRead())

2.当遇到网络错误导致上传失败 Bad state: DioException [unknown]: null Error: SocketException: Write failed (OS Error: 你的主机中的软件中止了一个已建立的连接。 , errno = 10053), address = private-xxx.cos.ap-guangzhou.myqcloud.com, port = 51980

3.此时想要删除该文件会提示以下错误 PathAccessException: Cannot delete file, path = 'C:\Users\6\AppData\Roaming\xxx\xxx.jpg' (OS Error: 另一个程序正在使用此文件,进程无法访问。 , errno = 32)

4.由于file.openRead() 创建的stream没有done或被cancel,仍然占用着该文件,因此无法删除文件

Expected Result

当遇到网络错误导致上传失败后,需要调用StreamSubscription的cancel方法,关闭流。

Actual Result

由于file.openRead() 创建的stream没有done或被cancel,仍然占用着该文件,因此无法删除文件

AlexV525 commented 1 year ago

与 dio 貌似没有任何关系,需要你自己维护 stream 的处理?

feimenggo commented 1 year ago

与 dio 貌似没有任何关系,需要你自己维护 stream 的处理?

Stream<List> openRead([int? start, int? end]); 方法,返回的是Stream<List>,将其作为参数传给dio了,dio会调用listen()来读数据,StreamSubscription listen(void onData(T event)?, {Function? onError, void onDone()?, bool? cancelOnError});,所以遇到错误的时候需要调用StreamSubscription的cancel方法关闭流。

AlexV525 commented 1 year ago

dio会调用listen()来读数据

我们的代码中没有任何地方对入参 Stream 进行订阅,传入的 Stream 均由 HttpRequest 进行处理。

feimenggo commented 1 year ago
image

最终是在这里被addStream()的,那当流还没有读完的时候,遇到网络错误了要怎么关闭流呢,是不是应该调用request.abort()方法?

feimenggo commented 1 year ago
image

是不是应该在这里遇到错误的时候,调用 request.close(); 或 request.abort(); 方法呢?

AlexV525 commented 1 year ago

是不是应该在这里遇到错误的时候,调用 request.close(); 或 request.abort(); 方法呢?

可以先行确认是否有改善

feimenggo commented 1 year ago

是不是应该在这里遇到错误的时候,调用 request.close(); 或 request.abort(); 方法呢?

可以先行确认是否有改善

我通过在上传期间手动断开WiFi网络来模拟SocketException: Write failed (OS Error: 你的主机中的软件中止了一个已建立的连接。,errno = 10053),但是不知道为什么没有catch到SocketException异常。

AlexV525 commented 1 year ago

不需要通过操作物理设备模拟,理论上只需要延迟调用 throw 即可。

feimenggo commented 1 year ago

在哪个位置调用throw呢?

AlexV525 commented 1 year ago

request 开始之后,delay 一个 throw。

AlexV525 commented 1 year ago

尝试把 io_adapter.dart#L154 改成如图后再试 image

feimenggo commented 1 year ago

怎么代码不一样,我这里是在150行呢?

image
AlexV525 commented 1 year ago

拉主分支的代码调试呀…

feimenggo commented 1 year ago

怎么代码不一样,我这里是在150行呢? image

我刚试了dio: 5.3.2,在L150位置加try,没有捕获到异常,而是直接在最开始dio.requestUri()处捕获到异常了

AlexV525 commented 11 months ago

问题是否依旧存在?如存在是否可以提供可以直接复现的最小demo?

feimenggo commented 11 months ago

问题是否依旧存在?如存在是否可以提供可以直接复现的最小demo?

我明天用主分支测试一下

feimenggo commented 11 months ago

我试了dio: ^5.3.3,问题仍然存在,只在Windows才会遇到。 我是在上传文件的过程中,断开电脑的网络连接,然后删除这个文件,就会提示:文件正在使用,操作无法完成,因为文件已在 xx软件 中打开。

feimenggo commented 11 months ago

我尝试拉main分支的代码调试 0b2043520120e2705cae29aaf4632a66 报了这个错误,这是什么原因? 2099e96daed7e45272c6e3c2c1d9d325

AlexV525 commented 11 months ago
depedencies:
  dio:
    git:
      url: https://github.com/cfug/dio
      path: dio
feimenggo commented 11 months ago

遇到依赖冲突了,不知道该如何解决...

Because qcloud_cos_client 0.0.7 depends on dio ^5.3.0 and no versions of qcloud_cos_client match >0.0.7 <0.1.0, qcloud_cos_client ^0.0.7 requires dio from hosted.
So, because app depends on both dio from git and qcloud_cos_client ^0.0.7, version solving failed.
AlexV525 commented 11 months ago

提供可以直接复现的最小demo,此处不再做无关回复。

feimenggo commented 11 months ago

我不知道该如何模拟上传接口。 简单来说就是使用dio上传文件到腾讯对象存储,当上传还没结束的时候,断开了电脑的网络连接,此时会提示”你的主机中的软件中止了一个已建立的连接“,此时这个文件文件流仍然被dio持有着没有释放,因此无法移动/删除文件,会提示:文件正在使用,操作无法完成,因为文件已在 xx软件 中打开。

AlexV525 commented 10 months ago

与 dio 无关,等待上游修复。

zpp0196 commented 7 months ago

试试 Stream.asBroadcastStream()

参考:https://github.com/localsend/localsend/pull/1228

NightFeather0615 commented 6 months ago

试试 Stream.asBroadcastStream()

参考:localsend/localsend#1228

目前看來這應該是最簡潔的方法,缺點就是要等 StreamSubscription 自己取消

這裡有另一種可以馬上釋放 file handle 的解法,更可控但複雜度高不少,其實就是把 .asBroadcastStream() 拆出來用😂

const fileName = "some/file/path";

final file = File(fileName);
final fileStream = file.openRead();
final fileReadController = StreamController<List<int>>();
final fileReadSubscription = fileStream.listen(
  fileReadController.add,
  onError: fileReadController.addError,
  onDone: () => fileReadController.close()
);

final dio = Dio();

try {
  await dio.post(
    "https://httpbun.com/post",
    data: fileReadController.stream
  );
} catch (_) {
  await fileReadSubscription.cancel();
  await file.delete();
}