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

[BUG] fileName cannot be provided with List<int> partFile #605

Closed
mtsrdr opened this issue May 16, 2023 · 4 comments
Closed

[BUG] fileName cannot be provided with List<int> partFile #605

mtsrdr opened this issue May 16, 2023 · 4 comments
Assignees
Labels
bug Something isn't working Triage needed

Comments

@mtsrdr
Copy link

mtsrdr commented May 16, 2023

Describe the bug

For more information: lejard-h/chopper#408

I have a .NET Core WebApi with an action to upload files:

    [HttpPost("upload-file")]
    public IActionResult Upload(IFormFile file)
    {
        return Ok(new
        {
            fileName = file.FileName,
            contentType = file.ContentType,
        });
    }

This action expects a IFormFile and requires the FileName and ContentType of the file to work.

But the generator uses List<int>? parameter instead of MultipartFile so I can't send the FileName during the request.

How can I configure the generator to use MultipartFile instead of List<int>?

To Reproduce
swagger.yaml

openapi: 3.0.1
info:
  title: SampleApi
  version: '1.0'
paths:
  /File/upload-file:
    post:
      tags:
        - File
      requestBody:
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
            encoding:
              file:
                style: form
      responses:
        '200':
          description: Success
components: { }

lib/generated_code/swagger_api.swagger.chopper.dart

  @override
  Future<Response<dynamic>> _fileUploadFilePost({List<int>? file}) {
    final Uri $url = Uri.parse('/File/upload-file');
    final List<PartValue> $parts = <PartValue>[
      PartValueFile<List<int>?>(
        'file',
        file,
      )
    ];
    final Request $request = Request(
      'POST',
      $url,
      client.baseUrl,
      parts: $parts,
      multipart: true,
    );
    return client.send<dynamic, dynamic>($request);
  }

Expected behavior
I want the generated code to be like this:

  @override
  Future<Response<dynamic>> _fileUploadMultipartPost(
      {required MultipartFile file}) {
    final Uri $url = Uri.parse('/File/upload-multipart');
    final List<PartValue> $parts = <PartValue>[
      PartValueFile<MultipartFile>(
        'file',
        file,
      )
    ];
    final Request $request = Request(
      'POST',
      $url,
      client.baseUrl,
      parts: $parts,
      multipart: true,
    );
    return client.send<dynamic, dynamic>($request);
  }

Library version used:

dependencies:
  chopper: ^6.1.1
  json_annotation: ^4.8.0

dev_dependencies:
  build_runner: ^2.3.3
  chopper_generator: ^6.0.0
  json_serializable: ^6.6.1
  swagger_dart_code_generator: ^2.10.4
@mtsrdr mtsrdr added bug Something isn't working Triage needed labels May 16, 2023
@fryette
Copy link
Contributor

fryette commented May 22, 2023

@mtsrdr is it related to the #608 ?
Looks like it's fixed in latest version

@mtsrdr
Copy link
Author

mtsrdr commented May 25, 2023

@mtsrdr is it related to the #608 ? Looks like it's fixed in latest version

No, it's not. FileName cannot be provided with List<int> because List<int> is just binary data.

The fileName is required for some apis to work.

The generated code does not include a parameter to send the fileName nor the contentType of the file.

Looking at the code in chopper's Request class we can see clearly that the fileName is ignored if we send the file as List<int>

https://github.com/lejard-h/chopper/blob/08781c999565ec5f1e562f74c47fcdb0acd256e2/chopper/lib/src/request.dart#L158-L195

   /// Convert this [Request] to a [http.MultipartRequest] 
   @visibleForTesting 
   Future<http.MultipartRequest> toMultipartRequest() async { 
     final http.MultipartRequest request = http.MultipartRequest(method, url) 
       ..headers.addAll(headers); 
  
     for (final PartValue part in parts) { 
       if (part.value == null) continue; 
  
       if (part.value is http.MultipartFile) { 
         request.files.add(part.value); 
       } else if (part.value is Iterable<http.MultipartFile>) { 
         request.files.addAll(part.value); 
       } else if (part is PartValueFile) { 
         if (part.value is List<int>) { 
           request.files.add( 
             http.MultipartFile.fromBytes(part.name, part.value), 
           ); 
         } else if (part.value is String) { 
           request.files.add( 
             await http.MultipartFile.fromPath(part.name, part.value), 
           ); 
         } else { 
           throw ArgumentError( 
             'Type ${part.value.runtimeType} is not a supported type for PartFile' 
             'Please use one of the following types' 
             ' - List<int>' 
             ' - String (path of your file) ' 
             ' - MultipartFile (from package:http)', 
           ); 
         } 
       } else { 
         request.fields[part.name] = part.value.toString(); 
       } 
     } 
  
     return request; 
   } 

When the type is List<int> the code creates a MultipartFile without a fileName:

  request.files.add(
    http.MultipartFile.fromBytes(part.name, part.value),
  );

Note that part.name is just the name of the field in the form request. The fileName is the third parameter and is never provided:

  request.files.add(
    http.MultipartFile.fromBytes(
      part.name,
      part.value,
      fileName: "FileName.png",
      contentType: "image/png",
    ),
  );

I manually changed the generated code to use http.MultipartFile instead of List<int> and the request works fine.
Is there a way to tell the generator to always use http.MultipartFile?

@ko16a46
Copy link

ko16a46 commented Aug 25, 2023

I ran into this issue as well.

Based on lejard-h/chopper#160 (comment), if the generated chopper client code uses http.Multipartfile instead of List<int>, it seems chopper can handle it, and the generated Dart code could allow us to include the filename and content type.

Without these, our backend .Net Core web API was receiving null for the IFormFile. We couldn't even get the raw bytes.

In the meantime, I wrote an extension method that manually uses MultipartFile, to avoid overwriting the generated code. Something like this

// Represents a file with byte data
class FileReference {
  FileReference({
    required this.value,
    required this.contentType,
    required this.filename,
  });

  final List<int> value;
  final String contentType;
  final String filename;
}

extension MyApiExtensions on MyApi {
  Future<Response<dynamic>> apiV1UploadFixed({
    FileReference? file,
    String? description,
  }) {
    final Uri $url = Uri.parse('/api/v1/upload');
    final List<PartValue> $parts = <PartValue>[
      PartValue<String?>(
        'description',
        description,
      ),
      PartValue<MultipartFile?>(
        'file',
        file != null
            ? MultipartFile.fromBytes(
                'file',
                file.value,
                filename: file.filename,
                contentType: MediaType.parse(file.contentType),
              )
            : null,
      ),
    ];
    final Request $request = Request(
      'POST',
      $url,
      client.baseUrl,
      parts: $parts,
      multipart: true,
    );
    return client.send<dynamic, dynamic>($request);
  }
}

Would be nice to have this fixed tho. It's a breaking change if all List<int> files were turned into MultipartFile, so perhaps a setting in a config would work.

@n0mad-d3v
Copy link
Contributor

Hello,
I ran into the same today, with the same config (.NET API + generation with this package).
While reading the code, I found the line this.multipartFileType = 'List<int>', in GeneratorOptions class which does not seem to be documented yet (I'll do a PR for that to help others or avoid having the same issue later myself again 😜).

Then, by creating a build.yml file as explained in the documentation with the following content:

targets:
  $default:
    builders:
      swagger_dart_code_generator:
        options:
          input_folder: 'lib/swaggers'
          output_folder: 'lib/generated'
          multipart_file_type: 'MultipartFile' 

The generated client is correct and I'm able to assign a file

var response = await _api.myMethodPost(
        [... others params ...]
        file: MultipartFile.fromBytes(
          "file",
          myFile.readAsBytesSync(),
          filename: myFile.filename,
        ),
      );

The file is correctly sent to the API, including the filename.

I hope it helps! 🤞

Have a nice day!

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

No branches or pull requests

5 participants