Dio: Bad state: Can't finalize a finalized MultipartFile

Created on 24 Sep 2019  ·  16Comments  ·  Source: flutterchina/dio

New Issue Checklist

  • [x] I have searched for a similar issue in the project and found none

Issue Info

| Info | Value |
| ------------------------------ | ----------------------------------------------------- |
| Platform Name | android |
| Platform Version | 9.0 |
| Dio Version | 3.0.1 |
| Android Studio | Android Studio 3.5 |
| Repro rate | all the time (100%) |
| Repro with our demo prj | no |
| Demo project link | no |

Flutter

[√] Flutter (Channel stable, v1.9.1+hotfix.2, on Microsoft Windows [versão 10.0.17763.737], locale pt-BR)
[√] Android toolchain - develop for Android devices (Android SDK version 28.0.3)
[√] Android Studio (version 3.5)
[√] Connected device (1 available)

Issue Description and Steps

The update token example is not working if repeat is performed with a Post / FormData.

1- Use an interceptor to update token, as in the interceptor_lock example;
2- Run a POST/FormData;
3- After updating the token, the request is resubmitted;

I/flutter ( 9472): DioError [DioErrorType.DEFAULT]: Bad state: Can't finalize a finalized MultipartFile.
I/flutter ( 9472): #0      FormData.finalize (package:dio/src/form_data.dart:124:7)
I/flutter ( 9472): #1      DioMixin._transformData (package:dio/src/dio.dart:987:23)
I/flutter ( 9472): <asynchronous suspension>
I/flutter ( 9472): #2      DioMixin._dispatchRequest (package:dio/src/dio.dart:897:26)
I/flutter ( 9472): <asynchronous suspension>
I/flutter ( 9472): #3      DioMixin._request._interceptorWrapper.<anonymous closure>.<anonymous closure>.<anonymous closure> (package:dio/src/dio.dart:828:37)
I/flutter ( 9472): #4      DioMixin.checkIfNeedEnqueue (package:dio/src/dio.dart:1100:22)
I/flutter ( 9472): #5      DioMixin._request._interceptorWrapper.<anonymous closure>.<anonymous closure> (package:dio/src/dio.dart:825:22)
I/flutter ( 9472): #6      new Future.<anonymous closure> (dart:async/future.dart:176:37)
I/flutter ( 9472): #7      _rootRun (dart:async/zone.dart:1120:38)
I/flutter ( 9472): #8      _CustomZone.run (dart:async/zone.dart:1021:19)
I/flutter ( 9472): #9      _CustomZone.runGuarded (dart:async/zone.dart:923:7)
I/flutter ( 9472): #10     _CustomZone.bindCallbackGuarded.<anonymous closure> (dart:async/zone.dart:963:23)
I/flutter ( 9472): #11     _rootRun (dart:async/zo
I/flutter ( 9472): #0      DioMixin._request._errorInterceptorWrapper.<anonymous closure> (package:dio/src/dio.dart:848:13)
I/flutter ( 9472): <asynchronous suspension>
I/flutter ( 9472): #1      _rootRunUnary (dart:async/zone.dart:1132:38)
I/flutter ( 9472): #2      _CustomZone.runUnary (dart:async/zone.dart:1029:19)
I/flutter ( 9472): #3      _FutureListener.handleError (dart:async/future_impl.dart:155:20)
I/flutter ( 9472): #4      Future._propagateToListeners.handleError (dart:async/future_impl.dart:690:47)
I/flutter ( 9472): #5      Future._propagateToListeners (dart:async/future_impl.dart:711:24)
I/flutter ( 9472): #6      Future._completeError (dart:async/future_impl.dart:530:5)
I/flutter ( 9472): #7      _SyncCompleter._completeError (dart:async/future_impl.dart:55:12)
I/flutter ( 9472): #8      _Completer.completeError (dart:async/future_impl.dart:27:5)
I/flutter ( 9472): #9      Future.any.<anonymous closure> (dart:async/future.dart:462:45)
I/flutter ( 9472): #10     _rootRunBinary (dart:async/zone.dart:1144:38)
I/flutter ( 9472): #11     _CustomZone.runBinary (dart:async/zone.dart:1037:19)
I/flutter ( 9472): #12     _FutureListener.handleError (dart:async/future_impl.dart:151:20)
I/flutter ( 9472): #13     Future._propagateToListeners.handleError (dart:async/future_impl.dart:6
stale

Most helpful comment

Because MultipartFile is based on Stream, and a Stream can be read only once, you should create a new MultipartFile when the request is resubmitted.

I tried this way, but the original MultipartFile does not have the file path to create a new FormData.

// The rest is the same of interceptor_lock example
}).then((e) {
  //repeat
  if (options.data is FormData) {
    FormData formData = FormData();
    formData.fields.addAll(options.data.fields);

    for (MapEntry mapFile in options.data.files) {
      formData.files.add(MapEntry(
          mapFile.key,
          MultipartFile.fromFileSync(???mapFile.value.FILE_PATH???,
              filename: mapFile.value.filename)));
    }
    options.data = formData;
  }
  return dio.request(options.path, options: options);
}

All 16 comments

Because MultipartFile is based on Stream, and a Stream can be read only once, you should create a new MultipartFile when the request is resubmitted.

Because MultipartFile is based on Stream, and a Stream can be read only once, you should create a new MultipartFile when the request is resubmitted.

I tried this way, but the original MultipartFile does not have the file path to create a new FormData.

// The rest is the same of interceptor_lock example
}).then((e) {
  //repeat
  if (options.data is FormData) {
    FormData formData = FormData();
    formData.fields.addAll(options.data.fields);

    for (MapEntry mapFile in options.data.files) {
      formData.files.add(MapEntry(
          mapFile.key,
          MultipartFile.fromFileSync(???mapFile.value.FILE_PATH???,
              filename: mapFile.value.filename)));
    }
    options.data = formData;
  }
  return dio.request(options.path, options: options);
}

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.

I have the same issue, any tips?

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.

Because MultipartFile is based on Stream, and a Stream can be read only once, you should create a new MultipartFile when the request is resubmitted.

I tried this way, but the original MultipartFile does not have the file path to create a new FormData.

// The rest is the same of interceptor_lock example
}).then((e) {
  //repeat
  if (options.data is FormData) {
  FormData formData = FormData();
  formData.fields.addAll(options.data.fields);

  for (MapEntry mapFile in options.data.files) {
    formData.files.add(MapEntry(
        mapFile.key,
        MultipartFile.fromFileSync(???mapFile.value.FILE_PATH???,
            filename: mapFile.value.filename)));
  }
  options.data = formData;
  }
  return dio.request(options.path, options: options);
}

thx!saved my day!

Because MultipartFile is based on Stream, and a Stream can be read only once, you should create a new MultipartFile when the request is resubmitted.

I tried this way, but the original MultipartFile does not have the file path to create a new FormData.

// The rest is the same of interceptor_lock example
}).then((e) {
  //repeat
  if (options.data is FormData) {
    FormData formData = FormData();
    formData.fields.addAll(options.data.fields);

    for (MapEntry mapFile in options.data.files) {
      formData.files.add(MapEntry(
          mapFile.key,
          MultipartFile.fromFileSync(???mapFile.value.FILE_PATH???,
              filename: mapFile.value.filename)));
    }
    options.data = formData;
  }
  return dio.request(options.path, options: options);
}

thx!saved my day!

how to get the multipartFile file path ???

Why is this issue closed? it's still relevant today.

Possible solutions

  1. Add a class derived from MultipartFile and has an extra field filePath.
  2. Add a field to MultipartFile class called extra, much like the one in Options.

Workaround

If anyone is looking for a workaround, one can:

  1. Use option 1 in possible solution, but create the derived class your self. When retrying the operation, check if the multipart file is of your derived class, and get the path if so.
  2. Use the Options.extra field and pass a map between the keys in FormData to a path

@matanshukry you can easily add field path to your request header part, and use it in repeat request.

@liweiwh Sending extra data over the network when it's not needed (it's used locally only) is a bad option

@matanshukry you can delete it when you do the request.

if (_options.data is FormData) {
  FormData formData = FormData();
  formData.fields.addAll(_options.data.fields);

  for (MapEntry mapFile in _options.data.files) {
    formData.files.add(MapEntry(
        mapFile.key,
        await MultipartFile.fromFile(_options.headers['path'],
            filename: mapFile.value.filename)));
  }
  _options.data = formData;
}
// empty the path field
_options.headers['path'] = null;
return dio.request(path, options: _options);

If you delete it from the request, how will it exist in the interceptor?

@matanshukry oh, sorry, my mistake, I use it in my refresh token process, but when the first time do the request the path is in the header. Not solve your problem, sorry.

If you are resending the same formData in a nested dio request eg
FormData formData = ...
Dio().post('url', data:formData).then((_){
//re-initialise form data here
Dio().post('url', data:formData).then((_){
...
});

}).catch( ...

be sure to re-initiate form data because it is already finalized

I made my own custom multipart class that can store file path, the solution as @matanshukry pointed out, if anyone is interested:

import 'dart:async';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:http_parser/http_parser.dart';
import 'package:path/path.dart' as p;

class MultipartFileExtended extends MultipartFile {
  final String filePath;  //this one!

  MultipartFileExtended(
    Stream<List<int>> stream,
    length, {
    filename,
    this.filePath,
    contentType,
  }) : super(stream, length, filename: filename, contentType: contentType);

  static MultipartFileExtended fromFileSync(
    String filePath, {
    String filename,
    MediaType contentType,
  }) => multipartFileFromPathSync(filePath, filename: filename, contentType: contentType);
}


MultipartFileExtended multipartFileFromPathSync(
  String filePath, {
  String filename,
  MediaType contentType,
}) {
  filename ??= p.basename(filePath);
  var file = File(filePath);
  var length = file.lengthSync();
  var stream = file.openRead();
  return MultipartFileExtended(
    stream,
    length,
    filename: filename,
    contentType: contentType,
    filePath: filePath,
  );
}

Now you can call request like this:

Dio dio = new Dio();
FormData formData = FormData.fromMap({
  "file": MultipartFileExtended.fromFileSync("./example/upload.txt",
          filename: "upload.txt")
});

var response = await dio.post("/info", data: formData).then((e) {
  FormData newFormData = FormData();
  newFormData.fields.addAll(formData.fields);

  for (MapEntry mapFile in formData.files) {
    newFormData.files.add(MapEntry(
        mapFile.key,
        MultipartFileExtended.fromFileSync(mapFile.value.filePath, //fixed!
            filename: mapFile.value.filename)));
  }

  return dio.post("/info", data: newFormData);
});

thanks @indokan2001 , your solution works for my case.
here is the full code

}).then((e) {
              // repeat
              if (options.data is FormData) {
                // https://github.com/flutterchina/dio/issues/482
                FormData formData = FormData();
                formData.fields.addAll(options.data.fields);
                for (MapEntry mapFile in options.data.files) {
                  formData.files.add(MapEntry(
                      mapFile.key,
                      MultipartFileExtended.fromFileSync(mapFile.value.filePath,
                      filename: mapFile.value.filename)));
                }
                options.data = formData;
              }
              return dio.request(options.path, options: options);
            });
Was this page helpful?
0 / 5 - 0 ratings