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

List<int> is not sent as string in multipart request. #438

Closed
Simouche opened this issue May 28, 2023 · 19 comments
Closed

List<int> is not sent as string in multipart request. #438

Simouche opened this issue May 28, 2023 · 19 comments
Assignees
Labels
bug Something isn't working enhancement New feature or request

Comments

@Simouche
Copy link

I have this multipart request:

  @multipart
  Future<Response<Homework>> create_(
    @Part("teacher") int teacher,
    @Part("title") String title,
    @Part("description") String description,
    @Part("subject") int subject,
    @Part("classes") List<int> classes,
    @Part("dueDate") String dueDate,
    // @PartFile("homeworkFiles") List<PartValueFile<String>> homeworkFiles,
  );

the classes part, is being sent as a string, and causing the backend to respond with http 400 with this error:

{"classes":["Incorrect type. Expected pk value, received str."],"homeworkFiles":["This field is required."]}

anyway to fix this issue?

Also, how to send multiple files under the same field name? uncommenting the last line makes it fail (without throwing any errors).

@techouse
Copy link
Collaborator

techouse commented May 29, 2023

Hey 👋

You might want to look at this method for clues.

Would be great if you can come up with a test case here.

Also, this and this StackOverflow answer might be of some help.

I'll take a deeper look into it once I find some free time.

@techouse techouse added bug Something isn't working question Further information is requested and removed question Further information is requested labels May 29, 2023
@techouse techouse self-assigned this May 29, 2023
@techouse
Copy link
Collaborator

@Simouche can you check if this fixes it for you? #439

@Simouche
Copy link
Author

@techouse how do i test it? i tried referencing it like this:

  chopper:
    git:
      url: https://github.com/techouse/chopper.git
      ref: fix/issue-438-multipart-list

and i had this error:

Could not find a file named "pubspec.yaml" in https://github.com/techouse/chopper.git e70429782820caa346cc299909eec0a5dc1f28fb.
pub get failed
command: "C:\flutter\bin\cache\dart-sdk\bin\dart __deprecated_pub --directory . get --example"
pub env: {
  "FLUTTER_ROOT": "C:\flutter",
  "PUB_ENVIRONMENT": "flutter_cli:get",
  "PUB_CACHE": "C:\Users\pc\AppData\Local\Pub\Cache",
}
exit code: 1

@techouse
Copy link
Collaborator

techouse commented May 29, 2023

chopper:
git:
url: https://github.com/techouse/chopper.git
ref: fix/issue-438-multipart-list

You need to also specify the path.

chopper:
  git:
    url: https://github.com/techouse/chopper.git
    ref: fix/issue-438-multipart-list
    path: chopper

@Simouche
Copy link
Author

yeah right, i did that and i got this now:

Because chopper_generator 6.0.1 depends on chopper ^6.0.0 and no versions of chopper_generator match >6.0.1 <7.0.0, chopper_generator ^6.0.1 requires chopper from hosted.
So, because go_class_teacher depends on both chopper from git and chopper_generator ^6.0.1, version solving failed.
pub get failed
command: "C:\flutter\bin\cache\dart-sdk\bin\dart __deprecated_pub --directory . get --example"
pub env: {
  "FLUTTER_ROOT": "C:\flutter",
  "PUB_ENVIRONMENT": "flutter_cli:get",
  "PUB_CACHE": "C:\Users\pc\AppData\Local\Pub\Cache",
}
exit code: 1

even if i change the dependency of the generator to any, i still get another error for null sound safety.

@techouse
Copy link
Collaborator

You might need to put that into dependency_overrides, so

dependency_overrides:
  chopper:
    git:
      url: https://github.com/techouse/chopper.git
      ref: fix/issue-438-multipart-list
      path: chopper

@Simouche
Copy link
Author

Simouche commented May 29, 2023

This doesn't fix it, it seems your solution is changing the field name to contain an index, causing the backend to not detect the field.
i had this error 400:
"classes":["This list may not be empty."]

when I printed the request body on the backend i received this:

QueryDict: {'classes[0]': ['15'], 'classes[1]': ['12'], 'due_date': ['29/05/2023 18:05'], ...}>

but when i try with postman i receive this:

<QueryDict: {'classes': ['6', '7', '8'], ...}>

for reference that's how i send it from postman:
image

@techouse
Copy link
Collaborator

techouse commented May 29, 2023

That is the only solution.

fields[0]=123
fields[1]=456

It's pretty standard HTTP practice. If all of them have the same name they would overwrite each other.

As the values need to be essentially stringified, a List<int> like

final List<int> ints = [1, 2, 3];

would become ints=[1, 2, 3] which is maybe workable but if it's strings

final List<String> ints = [
  'lorem',
  'ipsum',
  'dolor, something after an in string comma',
];

it would become strings=[lorem, ipsum, dolor, something after an in string comma] which is kinda terrible.

@Simouche
Copy link
Author

Why is the case of postman working properly? The backend receives it as a list of strings as you can see, in my previous answer, and it works just fine... :/

@techouse
Copy link
Collaborator

techouse commented May 29, 2023

What's the actual HTTP request that postman makes?

And FYI I did try using something like fields[] as the name and it would still overwrite itself, so I had to go for fields[$i]

This is how they do it in retrofit.

@Simouche
Copy link
Author

this is the dart code that postman proposes:

var headers = {
  'Authorization': 'Token ********'
};
var request = http.MultipartRequest('POST', Uri.parse('http://127.0.0.1:8002/api/homeworks/'));
request.fields.addAll({
  'title': 'first home work',
  'description': 'random description',
  'dueDate': '20/04/2023 00:00',
  'subject': '1',
  'teacher': '9',
  'classes': '6',
  'classes': '7',
  'classes': '8'
});
request.files.add(await http.MultipartFile.fromPath('homework_files', '/C:/Users/pc/Pictures/wallpapers/wallpaper1.png'));
request.files.add(await http.MultipartFile.fromPath('homework_files', '/C:/Users/pc/Pictures/wallpapers/wallpaper3.png'));
request.files.add(await http.MultipartFile.fromPath('homework_files', '/C:/Users/pc/Pictures/wallpapers/wallpaper5.png'));
request.headers.addAll(headers);

http.StreamedResponse response = await request.send();

if (response.statusCode == 200) {
  print(await response.stream.bytesToString());
}
else {
  print(response.reasonPhrase);
}

@techouse
Copy link
Collaborator

Hmm, ok. I've updated the code. Pull the repo and try again.

@Simouche
Copy link
Author

I still get the same output as the previous one...

@techouse
Copy link
Collaborator

Yeah, I've only fixed it for the files. The dictionary with the integers, as in your example, can't have repeated keys.

What's the cURL code it generates?

@Simouche
Copy link
Author

cURL:

curl --location 'http://127.0.0.1:8002/api/homeworks/' \
--header 'Authorization: Token 21017a48e6e9156ae84299c59754f88b7ac5c803' \
--form 'title="first home work"' \
--form 'description="random description"' \
--form 'dueDate="20/04/2023 00:00"' \
--form 'subject="1"' \
--form 'teacher="9"' \
--form 'classes="6"' \
--form 'classes="7"' \
--form 'classes="8"' \
--form 'homework_files=@"/C:/Users/pc/Pictures/wallpapers/wallpaper1.png"' \
--form 'homework_files=@"/C:/Users/pc/Pictures/wallpapers/wallpaper3.png"' \
--form 'homework_files=@"/C:/Users/pc/Pictures/wallpapers/wallpaper5.png"'

@techouse
Copy link
Collaborator

While that will work with cURL it won't work with MultipartRequest fields because they are a dictionary and keys can't be repeated.

@Simouche
Copy link
Author

Then why does this snippet, which uses dio, works just fine?

  Future createDio(Map<String, dynamic> data, List<String> files) async {
    final fs = [];
    for (var file in files) {
      fs.add(await dio.MultipartFile.fromFile(file));
    }
    final formData = dio.FormData.fromMap({
      ...data,
      'homeworkFiles': fs,
    });
    final response =
        await dioClient.post('${baseURL}homeworks/', data: formData);
    return response;
  }

on the backend i receive this (just like postman):

<QueryDict: {'..., 'classes': ['6', '13'], 'teacher': ['9'], 'homework_files': [...]}>

@techouse
Copy link
Collaborator

techouse commented May 29, 2023

Because Dio doesn't use http like chopper does.

MultipartRequest.fields is a Map<String, String> and as such can not have duplicate keys.

This has been mentioned previously in #77 where @humazed provided a solution

@Post(path: "store/orders")
@multipart
Future<Response<Order>> createTripOrder(
  @Part("trip") int trip,
  @PartFile("images[]") List<MultipartFile> images,
);

What you want is not possible with http and has been discussed here dart-lang/http#277 and here dart-lang/http#24.

The solution I implemented is exactly the same as suggested here dart-lang/http#277 (comment)

As I said above, using this kind of annotation field[0], field[1], field[2] is pretty common and I'm not sure why your backend doesn't accept it.

Effectively you have 2 options:

  1. use Dio
  2. wait for 🐛 fix Multipart for List<int> and List<String> #439 to land and update your backend to accept field[0], field[1], field[2]

@techouse techouse added the enhancement New feature or request label May 29, 2023
@techouse
Copy link
Collaborator

techouse commented May 30, 2023

@Simouche please continue any further discussion in #439

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants