restsharp / RestSharp

Simple REST and HTTP API Client for .NET
https://restsharp.dev
Apache License 2.0
9.61k stars 2.34k forks source link

Post with file parameter order #2115

Closed kyugoa closed 1 year ago

kyugoa commented 1 year ago

DO NOT USE ISSUES FOR QUESTIONS

Describe the bug when posting to an endpoint with File attachment, if there are other input parameters in the body, the File parameter is always render first regardless of the order the parameter is added.

To Reproduce call an api endpoint with file attachment and additional parameter like fileName and fileType.

` using var fileServiceClient = GetFileServiceClient(); var request = new RestRequest("file/v1/files", Method.Post);

  //added these parameters first
  request.AddJsonBody(new FileUploadRequestModel { Type = "tps-volume-titles", FileName = fileName });

  //then the file parameter
  request.AddFile("file", fileContent, fileName, "text/csv");
  request.AlwaysMultipartFormData = true;

  var response = await fileServiceClient.PostAsync(request).ConfigureAwait(false);

  if (!response.IsSuccessStatusCode)
  {
    _logger.LogError($"Failed to upload file {fileName}");
  }

`

Expected behavior Expected the http request to be:

POST https://somedomain.com/file/v1/files HTTP/1.1 Host: file-service.adsp-uat.alberta.ca Authorization: Bearer eyxxx Accept: application/json, text/json, text/x-json, text/javascript, application/xml, text/xml User-Agent: RestSharp/110.2.0.0 Accept-Encoding: gzip, deflate, br Content-Type: multipart/form-data; boundary="0a1f4db8-6981-4592-aff5-7b12b4e00197" Content-Length: 115113

--0a1f4db8-6981-4592-aff5-7b12b4e00197 Content-Type: text/plain; charset=utf-8 Content-Disposition: form-data; name=type

tps-volume-titles --0a1f4db8-6981-4592-aff5-7b12b4e00197 Content-Type: text/plain; charset=utf-8 Content-Disposition: form-data; name=filename

1680672668.csv --0a1f4db8-6981-4592-aff5-7b12b4e00197 Content-Type: text/csv Content-Disposition: form-data; name="file"; filename="1680672668.csv"

"xxx","2023-07-13 09:28:48",,ON00001,,"242006882001","S","*",,"2023-07-11","Subdivision Plan",,,"N",,"T","Fee Simple","242 006 882 +2",,,"2023-07-11"

--0a1f4db8-6981-4592-aff5-7b12b4e00197--

Actual rendered http request

POST https://somedomain.com/file/v1/files HTTP/1.1 Host: file-service.adsp-uat.alberta.ca Authorization: Bearer eyxxx Accept: application/json, text/json, text/x-json, text/javascript, application/xml, text/xml User-Agent: RestSharp/110.2.0.0 Accept-Encoding: gzip, deflate, br Content-Type: multipart/form-data; boundary="0a1f4db8-6981-4592-aff5-7b12b4e00197" Content-Length: 115113

--0a1f4db8-6981-4592-aff5-7b12b4e00197 Content-Type: text/csv Content-Disposition: form-data; name="file"; filename="1680672668.csv"

"xxx","2023-07-13 09:28:48",,ON00001,,"242006882001","S","*",,"2023-07-11","Subdivision Plan",,,"N",,"T","Fee Simple","242 006 882 +2",,,"2023-07-11"

--0a1f4db8-6981-4592-aff5-7b12b4e00197 Content-Type: text/plain; charset=utf-8 Content-Disposition: form-data; name=type

tps-volume-titles --0a1f4db8-6981-4592-aff5-7b12b4e00197 Content-Type: text/plain; charset=utf-8 Content-Disposition: form-data; name=filename

1680672668.csv --0a1f4db8-6981-4592-aff5-7b12b4e00197--

Stack trace

Desktop (please complete the following information):

Additional context Add any other context about the problem here.

b166er commented 1 year ago

I have the same problem!

my first code, was very simple and naive. unfortunately it did not work as i expected:

// Create RestClient
RestClient client = new RestClient("https://api-server");

// Create RestRequest
RestRequest request = new RestRequest("path", Method.POST) {FormBoundary = "MySpecialBounderyIdentifierTest123"};

request.AddJsonBody(myJsonData);
request.AddFile(filename, fileContent, filename);

// overwrite Content-Type with my custom requirement
request.AddOrUpdateHeader("Content-Type", "multipart/mixed");

// Execute the request
IRestResponse response = client.Execute(request);

// Handle the response
if (response.IsSuccessful)
{
    Console.WriteLine("File uploaded successfully!");
}
else
{
    Console.WriteLine("File upload failed. Error: " + response.ErrorMessage);
} 

output with Version 110.2.0 is:

Authorization: Basic myloginauthkey
Accept: application/json, text/json, text/x-json, text/javascript, application/xml, text/xml
User-Agent: RestSharp/110.2.0.0
Content-Type: multipart/mixed; 
boundary="MySpecialBounderyIdentifierTest123"
Host: api-server
Content-Length: 123
Connection: Keep-Alive

--MySpecialBounderyIdentifierTest123
Content-Type: application/zip
Content-Disposition: form-data; name="mytest.zip"; filename="mytest.zip"
...binary-bytes...
--MySpecialBounderyIdentifierTest123
Content-Type: application/json; charset=utf-8
Content-Disposition: form-data; name="application/json"
{"meta": [{"title": "My Homework","author": {"name": "Tessa","adress": null},"created": 1550743894373}]}
--MySpecialBounderyIdentifierTest123--

my expected output was:

Authorization: Basic myloginauthkey
Accept: application/json, text/json, text/x-json, text/javascript, application/xml, text/xml
User-Agent: RestSharp/110.2.0.0
Content-Type: multipart/mixed; boundary="MySpecialBounderyIdentifierTest123"
Host: api-server
Content-Length: 123
Connection: Keep-Alive

--MySpecialBounderyIdentifierTest123
Content-Type: application/json; charset=utf-8
Content-Disposition: form-data; name="application/json"
{"meta": [{"title": "My Homework","author": {"name": "Tessa","adress": null},"created": 1550743894373}]}
--MySpecialBounderyIdentifierTest123
Content-Type: application/zip
Content-Disposition: form-data; name="mytest.zip"; filename="mytest.zip"
...binary-bytes...
--MySpecialBounderyIdentifierTest123--

Here is my ugly code to set the order manually. It would be so great if the output of the parameters were in the same order as the executing Add-Methods, or at least some simple functionality to set the order, e.g. an order parameter.


using System.Text;
using System.Linq;

class Program
{
    static void Main(string[] args)
    {
        // Create RestClient
        RestClient client = new RestClient("https://api-server");

        // Create RestRequest
        RestRequest request = new RestRequest("path", Method.POST);

        string boundary = "MySpecialBounderyIdentifierTest123";
        string filename = "mytest.zip";

        // set manually content and encode all data to bytes
        byte[] start = Encoding.UTF8.GetBytes($"--{boundary}{Environment.NewLine}");
        byte[] separator = Encoding.UTF8.GetBytes($"{Environment.NewLine}{boundary}--{Environment.NewLine}");
        byte[] end = Encoding.UTF8.GetBytes($"{Environment.NewLine}--{boundary}--{Environment.NewLine}");

        string json = @"{
            ""meta"": [{
                    ""title"": ""My Homework"",
                    ""author"": 
                    {
                        ""name"": ""Tessa"",
                        ""adress"": null
                    }, 
                    ""created"": 1550743894373
                }]
        }";
        // Content-Disposition is optional and not necessary for me/the api-server
        byte[] jsonMeta = Encoding.UTF8.GetBytes($"Content-Type: application/json{Environment.NewLine}{Environment.NewLine}{json}");
        byte[] fileMeta = Encoding.UTF8.GetBytes($"Content-Type: application/zip{Environment.NewLine}Content-Disposition: form-data; name=\"file\"; filename=\"{filename}\"{Environment.NewLine}{Environment.NewLine}");

        // use Linqs byte-concatenation
        IEnumerable<byte> requestBytes;

        // Combine first json data, then file content! the order is very important.
        requestBytes = start // start with boundary-start-sequence
            .Concat(jsonMeta) // important: json Data MUST be in the first position, before binary file payload
            .Concat(separator) // add boundary separator
            .Concat(fileMeta) // file data need as second parameter, after json data!
            .Concat(File.ReadAllBytes(filename)) // add file binary payload data as bytes
            .Concat(end) // add boundary-endsequence
        ;

        // in my personal case, I have to set an additional custom Content-Type Header
        // multipart/form-data does not work with my particular api-server!
        string header = $"multipart/mixin; boundary={boundary}";

        // set manually header and byte-content to parameter
        request.AddParameter(header, requestBytes.toArray(), ParameterType.RequestBody);

        // Execute the request
        IRestResponse response = client.Execute(request);

        // Handle the response
        if (response.IsSuccessful)
        {
            Console.WriteLine("File uploaded successfully!");
        }
        else
        {
            Console.WriteLine("File upload failed. Error: " + response.ErrorMessage);
        }       
    }
}
koo9 commented 1 year ago

just render the paramter by the order added would be sufficient. I believe you can set the file content type in the .AddFile() method but the overall content-type is still multipart/form-data

alexeyzimarev commented 1 year ago

Body parameters are added to the request message in the same order as they are added to RestRequest. But, files are added separately, as well as POST parameters when the content is a multipart form.

alexeyzimarev commented 1 year ago

Duplicate of #2098