Skip to content

Commit

Permalink
Support file uploading without multipart/form-data (#2068)
Browse files Browse the repository at this point in the history
* Support file uploading without multipart/form-data

* AlwaysSingleFileAsContent flag

* ValidateParameters method

* ValidateParameters tests
  • Loading branch information
RomanSoloweow committed May 3, 2023
1 parent 71c73f8 commit 2facbeb
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 30 deletions.
79 changes: 50 additions & 29 deletions src/RestSharp/Request/RequestContent.cs
Expand Up @@ -36,38 +36,60 @@ class RequestContent : IDisposable {
_parameters = new RequestParameters(_request.Parameters.Union(_client.DefaultParameters));
}

public HttpContent BuildContent() {
AddFiles();
public HttpContent BuildContent()
{
var postParameters = _parameters.GetContentParameters(_request.Method).ToArray();
AddBody(postParameters.Length > 0);
AddPostParameters(postParameters);
AddHeaders();

return Content!;
}

void AddFiles() {
if (!_request.HasFiles() && !_request.AlwaysMultipartFormData) return;

var mpContent = CreateMultipartFormDataContent();
var postParametersExists = postParameters.Length > 0;
var bodyParametersExists = _request.TryGetBodyParameter(out var bodyParameter);
var filesExists = _request.Files.Any();

foreach (var file in _request.Files) {
var stream = file.GetFile();
_streams.Add(stream);
var fileContent = new StreamContent(stream);
if (filesExists)
AddFiles();

fileContent.Headers.ContentType = file.ContentType.AsMediaTypeHeaderValue;
if (bodyParametersExists)
AddBody(postParametersExists, bodyParameter!);

var dispositionHeader = file.Options.DisableFilenameEncoding
? ContentDispositionHeaderValue.Parse($"form-data; name=\"{file.Name}\"; filename=\"{file.FileName}\"")
: new ContentDispositionHeaderValue("form-data") { Name = $"\"{file.Name}\"", FileName = $"\"{file.FileName}\"" };
if (!file.Options.DisableFileNameStar) dispositionHeader.FileNameStar = file.FileName;
fileContent.Headers.ContentDisposition = dispositionHeader;
if (postParametersExists)
AddPostParameters(postParameters);

mpContent.Add(fileContent);
}
AddHeaders();

Content = mpContent;
return Content!;
}

void AddFiles()
{
// File uploading without multipart/form-data
if (_request.AlwaysSingleFileAsContent && _request.Files.Count == 1)
{
var fileParameter = _request.Files.First();
Content = ToStreamContent(fileParameter);
return;
}

var mpContent = new MultipartFormDataContent(GetOrSetFormBoundary());

foreach (var fileParameter in _request.Files)
mpContent.Add(ToStreamContent(fileParameter));

Content = mpContent;
}

StreamContent ToStreamContent(FileParameter fileParameter)
{
var stream = fileParameter.GetFile();
_streams.Add(stream);
var streamContent = new StreamContent(stream);

streamContent.Headers.ContentType = fileParameter.ContentType.AsMediaTypeHeaderValue;

var dispositionHeader = fileParameter.Options.DisableFilenameEncoding
? ContentDispositionHeaderValue.Parse($"form-data; name=\"{fileParameter.Name}\"; filename=\"{fileParameter.FileName}\"")
: new ContentDispositionHeaderValue("form-data") { Name = $"\"{fileParameter.Name}\"", FileName = $"\"{fileParameter.FileName}\"" };
if (!fileParameter.Options.DisableFileNameStar) dispositionHeader.FileNameStar = fileParameter.FileName;
streamContent.Headers.ContentDisposition = dispositionHeader;

return streamContent;
}

HttpContent Serialize(BodyParameter body) {
Expand Down Expand Up @@ -117,9 +139,8 @@ class RequestContent : IDisposable {
return mpContent;
}

void AddBody(bool hasPostParameters) {
if (!_request.TryGetBodyParameter(out var bodyParameter)) return;

void AddBody(bool hasPostParameters, BodyParameter bodyParameter)
{
var bodyContent = Serialize(bodyParameter);

// we need to send the body
Expand Down
7 changes: 6 additions & 1 deletion src/RestSharp/Request/RestRequest.cs
Expand Up @@ -81,7 +81,12 @@ public RestRequest(Uri resource, Method method = Method.Get)
/// Always send a multipart/form-data request - even when no Files are present.
/// </summary>
public bool AlwaysMultipartFormData { get; set; }


/// <summary>
/// Always send a file as request content without multipart/form-data request - even when the request contains only one file parameter
/// </summary>
public bool AlwaysSingleFileAsContent { get; set; }

/// <summary>
/// When set to true, parameter values in a multipart form data requests will be enclosed in
/// quotation marks. Default is false. Enable it if the remote endpoint requires parameters
Expand Down
17 changes: 17 additions & 0 deletions src/RestSharp/Request/RestRequestExtensions.cs
Expand Up @@ -501,4 +501,21 @@ public static RestRequest AddXmlBody<T>(this RestRequest request, T obj, Content

if (duplicateKeys.Any()) throw new ArgumentException($"Duplicate header names exist: {string.Join(", ", duplicateKeys)}");
}

public static void ValidateParameters(this RestRequest request) {

if (request.AlwaysSingleFileAsContent) {
var postParametersExists = request.Parameters.GetContentParameters(request.Method).Any();
var bodyParametersExists = request.Parameters.Any(p => p.Type == ParameterType.RequestBody);

if (request.AlwaysMultipartFormData)
throw new ArgumentException("Failed to put file as content because flag AlwaysMultipartFormData enabled");

if (postParametersExists)
throw new ArgumentException("Failed to put file as content because added post parameters");

if (bodyParametersExists)
throw new ArgumentException("Failed to put file as content because added body parameters");
}
}
}
1 change: 1 addition & 0 deletions src/RestSharp/RestClient.Async.cs
Expand Up @@ -77,6 +77,7 @@ public partial class RestClient {
throw new ObjectDisposedException(nameof(RestClient));
}

request.ValidateParameters();
var authenticator = request.Authenticator ?? Options.Authenticator;
if (authenticator != null) await authenticator.Authenticate(this, request).ConfigureAwait(false);

Expand Down
75 changes: 75 additions & 0 deletions test/RestSharp.Tests/RestRequestParametersTests.cs
@@ -0,0 +1,75 @@
// Copyright (c) .NET Foundation and Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

namespace RestSharp.Tests;

public class RestRequestValidateParametersTests {
[Fact]
public void RestRequest_AlwaysMultipartFormData_IsAllowed() {
var request = new RestRequest {
AlwaysMultipartFormData = true
};

request.ValidateParameters();
}

[Fact]
public void RestRequest_AlwaysSingleFileAsContent_IsAllowed() {
var request = new RestRequest {
AlwaysSingleFileAsContent = true
};

request.ValidateParameters();
}

[Fact]
public void RestRequest_AlwaysSingleFileAsContent_And_AlwaysMultipartFormData_IsNotAllowed() {
var request = new RestRequest {
AlwaysSingleFileAsContent = true,
AlwaysMultipartFormData = true
};

Assert.Throws<ArgumentException>(
() => { request.ValidateParameters(); }
);
}

[Fact]
public void RestRequest_AlwaysSingleFileAsContent_And_PostParameters_IsNotAllowed() {
var request = new RestRequest {
Method = Method.Post,
AlwaysSingleFileAsContent = true,
};

request.AddParameter("name", "value", ParameterType.GetOrPost);

Assert.Throws<ArgumentException>(
() => { request.ValidateParameters(); }
);
}

[Fact]
public void RestRequest_AlwaysSingleFileAsContent_And_BodyParameters_IsNotAllowed() {
var request = new RestRequest {
AlwaysSingleFileAsContent = true,
};

request.AddParameter("name", "value", ParameterType.RequestBody);

Assert.Throws<ArgumentException>(
() => { request.ValidateParameters(); }
);
}
}

0 comments on commit 2facbeb

Please sign in to comment.