Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bad state: Can't finalize a finalized MultipartFile #482

Closed
1 task done
claudiowagner opened this issue Sep 24, 2019 · 36 comments
Closed
1 task done

Bad state: Can't finalize a finalized MultipartFile #482

claudiowagner opened this issue Sep 24, 2019 · 36 comments
Labels
e: good for newcomers Good for newcomers fixed p: dio Targeting `dio` package s: best practise It's the best way to do something so far

Comments

@claudiowagner
Copy link

New Issue Checklist

  • 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
@wendux
Copy link
Contributor

wendux commented Sep 25, 2019

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.

@claudiowagner
Copy link
Author

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

@stale
Copy link

stale bot commented Oct 25, 2019

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.

@stale stale bot added the stale label Oct 25, 2019
@ramonweiss
Copy link

I have the same issue, any tips?

@stale stale bot removed the stale label Oct 25, 2019
@stale
Copy link

stale bot commented Nov 24, 2019

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.

@stale stale bot added the stale label Nov 24, 2019
@stale stale bot closed this as completed Dec 1, 2019
@AllenWen
Copy link

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!

@quan-zai
Copy link

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 ???

@matanshukry
Copy link

matanshukry commented May 24, 2020

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

@liweiwh
Copy link

liweiwh commented Jun 4, 2020

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

@matanshukry
Copy link

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

@liweiwh
Copy link

liweiwh commented Jun 5, 2020

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

@matanshukry
Copy link

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

@liweiwh
Copy link

liweiwh commented Jun 5, 2020

@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.

@felixivance
Copy link

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

@madfatihid
Copy link

madfatihid commented Jul 17, 2020

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

@dotw
Copy link

dotw commented Nov 22, 2020

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

@Refaat-taha
Copy link

the problem still exists
please, anyone, have a complete solution please share it with me

@binSaed
Copy link

binSaed commented Jul 5, 2021

after I review my code found
by mistake, I send the same Multipart file twice

I know this not closely related to the issue, but I think my comment will help someone

temp

@befirst
Copy link

befirst commented Jul 14, 2021

just create a function which returns dio request and use it again when you came across this error

@akj-seq
Copy link

akj-seq commented Apr 4, 2022

just replace file path "/" with a "@" and make it as a filename in multipartfile and after refresh token resending request use this filename and restore it by replacing "@" with "/" now path is ready create a new multipartfile with that path. solved!!.

@jeankpoti
Copy link

jeankpoti commented Jun 23, 2022

You can just reset the list in which you are storing the multipartfile after the request is made.

@MingSern
Copy link

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

How about MultipartFile that are from bytes?

@aweiand
Copy link

aweiand commented Oct 17, 2022

Anyone using dio_smart_retry and have the same issue? I'm using it with dio , and when get second or third retry, causes this error....

@Lars-Sommer
Copy link

Anyone using dio_smart_retry and have the same issue? I'm using it with dio , and when get second or third retry, causes this error....

I am using dio_smart_retry and got this error. But on the initial request, and the subsequent.
But only got it once on a Motorola device I do not own, and I cannot reproduce it.

@tinyc0der
Copy link

Anyone using dio_smart_retry and have the same issue? I'm using it with dio , and when get second or third retry, causes this error....

i got same one

tinyc0der added a commit to dica-solution/dio_smart_retry that referenced this issue Nov 22, 2022
@tinyc0der
Copy link

tinyc0der commented Nov 22, 2022

@Lars-Sommer
Copy link

@vimaxwell I have not seen this error in a while, but will try your fix if I see it again and are able to reproduce it.
Thanks 👍

@aweiand
Copy link

aweiand commented Nov 22, 2022

Hi @vimaxwell ,

After test in a test server I own, the issue persists...

@rodion-m
Copy link
Contributor

rodion-m commented Dec 2, 2022

Anyone using dio_smart_retry and have the same issue? I'm using it with dio , and when get second or third retry, causes this error....

Starting from version 1.4.0 dio_smart_retry package supports retrying for requests with multipart/form-data.
Just use the MultipartFileRecreatable class instead of MultipartFile to make retry available. Details: https://github.com/rodion-m/dio_smart_retry#retry-requests-with-multipartform-data

@rinlv
Copy link

rinlv commented Feb 10, 2023

Anyone using dio_smart_retry and have the same issue? I'm using it with dio , and when get second or third retry, causes this error....

Starting from version 1.4.0 dio_smart_retry package supports retrying for requests with multipart/form-data. Just use the MultipartFileRecreatable class instead of MultipartFile to make retry available. Details: https://github.com/rodion-m/dio_smart_retry#retry-requests-with-multipartform-data

Hi @rodion-m, how to MultipartFileRecreatable work with retrofit?

@CodeFoxLk
Copy link

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

How about MultipartFile that are from bytes?

class MultipartFileExtended extends MultipartFile {
  MultipartFileExtended(
    Stream<List<int>> stream,
    int length, {
    String? filename,
    Map<String, List<String>>? headers,
    MediaType? contentType,
    List<int>? byteValue,
  })  : _byteValue = byteValue,
        super(stream, length, filename: filename, contentType: contentType);

  List<int>? _byteValue;
  List<int>? get byteValue => _byteValue;

  factory MultipartFileExtended.fromBytes(
    List<int> value, {
    String? filename,
    MediaType? contentType,
    final Map<String, List<String>>? headers,
  }) {
    final stream = Stream.fromIterable([value]);

    return MultipartFileExtended(stream, value.length,
        filename: filename, contentType: contentType, headers: headers, byteValue: value);
  }
}

AlexV525 added a commit that referenced this issue Mar 6, 2023
…1714)

Help issues like #482 to understand what happened exactly.

### New Pull Request Checklist

- [x] I have read the
[Documentation](https://pub.dev/documentation/dio/latest/)
- [x] I have searched for a similar pull request in the
[project](https://github.com/cfug/dio/pulls) and found none
- [x] I have updated this branch with the latest `main` branch to avoid
conflicts (via merge from master or rebase)
- [ ] I have added the required tests to prove the fix/feature I'm
adding
- [x] I have updated the documentation (if necessary)
- [x] I have run the tests without failures
- [ ] I have updated the `CHANGELOG.md` in the corresponding package

### Additional context and info (if any)

As an improvement with comments and docs, it doesn't require a version
change.

---------

Signed-off-by: Alex Li <[email protected]>
Co-authored-by: Peter Leibiger <[email protected]>
@Sai7xp
Copy link

Sai7xp commented Mar 10, 2023

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

Awesome. It worked

...
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(
                    (mapFile.value as MultipartFile).filename ?? "",
                    filename: "${(mapFile.value as MultipartFile).filename}"),
              ));
            }
            options.data = formData;
            final Response response = await dio.fetch(options);
            return handler.resolve(response);
          }

CoderJava added a commit to CoderJava/dipantau-desktop that referenced this issue Jul 1, 2023
…a 401

Penyebabnya adalah karena Bad state: Can't finalize a finalized MultipartFile. Arti dari pesan ini ialah si Dio tidak bisa menggunakan form data yang sama untuk request yang berulang-ulang. Jadi, solusinya adalah buat ulang form data-nya menggunakan solusi dari ini cfug/dio#482 (comment).
@gabrielaraujoz
Copy link
Contributor

gabrielaraujoz commented Jul 14, 2023

Hi there! This PR should solve this problem without extra workarounds. Hope you enjoy it <3

#1889

@AlexV525 AlexV525 added the fixed label Jul 16, 2023
@YeeWan0217
Copy link

YeeWan0217 commented Jul 3, 2024

Take a long time to find, this work for me. The type of mapFile.value is MultipartFile which can use clone() method from MultipartFile Class Library

if (options.data is FormData) {
    FormData formData = FormData();

    for (MapEntry mapFile in options.data.files) {
      formData.files.add(MapEntry(
        mapFile.key,
        mapFile.value.clone(),
      ));
    }
    options.data = formData;
  }

@hosseinhp1378
Copy link

Take a long time to find, this work for me. The type of mapFile.value is MultipartFile which can use clone() method from MultipartFile Class Library

if (options.data is FormData) {
    FormData formData = FormData();

    for (MapEntry mapFile in options.data.files) {
      formData.files.add(MapEntry(
        mapFile.key,
        mapFile.value.clone(),
      ));
    }
    options.data = formData;
  }

this worked perfectly
Thank you

@AlexV525
Copy link
Member

/// Clone MultipartFile, returning a new instance of the same object.
/// This is useful if your request failed and you wish to retry it,
/// such as an unauthorized exception can be solved by refreshing the token.
MultipartFile clone() {
return MultipartFile.fromStream(
_dataBuilder,
length,
filename: filename,
contentType: contentType,
headers: headers,
);

// Convenience method to clone finalized FormData when retrying requests.
FormData clone() {
final clone = FormData();
clone.fields.addAll(fields);
for (final file in files) {
clone.files.add(MapEntry(file.key, file.value.clone()));
}
return clone;
}

@AlexV525 AlexV525 added e: good for newcomers Good for newcomers p: dio Targeting `dio` package s: best practise It's the best way to do something so far labels Jul 15, 2024
@cfug cfug locked as resolved and limited conversation to collaborators Jul 15, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
e: good for newcomers Good for newcomers fixed p: dio Targeting `dio` package s: best practise It's the best way to do something so far
Projects
None yet
Development

No branches or pull requests