From 707f22b355a7c7b662ce1dd8431f7e4ac3e8f270 Mon Sep 17 00:00:00 2001 From: GMIKE Date: Sat, 22 Apr 2023 02:00:36 +0100 Subject: [PATCH 1/4] Support file uploading without multipart/form-data --- src/RestSharp/Request/RequestContent.cs | 73 ++++++++++++++++--------- 1 file changed, 47 insertions(+), 26 deletions(-) diff --git a/src/RestSharp/Request/RequestContent.cs b/src/RestSharp/Request/RequestContent.cs index 4df106cb1..5625884bc 100644 --- a/src/RestSharp/Request/RequestContent.cs +++ b/src/RestSharp/Request/RequestContent.cs @@ -36,38 +36,60 @@ class RequestContent : IDisposable { _parameters = new RequestParameters(_request.Parameters.Union(_client.DefaultParameters)); } - public HttpContent BuildContent() { - AddFiles(); - var postParameters = _parameters.GetContentParameters(_request.Method).ToArray(); - AddBody(postParameters.Length > 0); - AddPostParameters(postParameters); + public HttpContent BuildContent() + { + var postParameters = _parameters.GetContentParameters(_request.Method).ToArray(); + var postParametersExists = postParameters.Length > 0; + var bodyParametersExists = _request.TryGetBodyParameter(out var bodyParameter); + var filesExists = _request.Files.Any(); + + if (filesExists) + AddFiles(postParametersExists, bodyParametersExists); + + if (bodyParametersExists) + AddBody(postParametersExists, bodyParameter!); + + if (postParametersExists) + AddPostParameters(postParameters); + AddHeaders(); return Content!; } + + void AddFiles(bool postParametersExists, bool bodyParametersExists) + { + // File uploading without multipart/form-data + if (!postParametersExists && !bodyParametersExists && _request.Files.Count == 1 && !_request.AlwaysMultipartFormData) + { + var fileParameter = _request.Files.First(); + Content = ToStreamContent(fileParameter); + return; + } + + var mpContent = new MultipartFormDataContent(GetOrSetFormBoundary()); - void AddFiles() { - if (!_request.HasFiles() && !_request.AlwaysMultipartFormData) return; - - var mpContent = CreateMultipartFormDataContent(); - - foreach (var file in _request.Files) { - var stream = file.GetFile(); - _streams.Add(stream); - var fileContent = new StreamContent(stream); + foreach (var fileParameter in _request.Files) + mpContent.Add(ToStreamContent(fileParameter)); + + Content = mpContent; + } - fileContent.Headers.ContentType = file.ContentType.AsMediaTypeHeaderValue; + StreamContent ToStreamContent(FileParameter fileParameter) + { + var stream = fileParameter.GetFile(); + _streams.Add(stream); + var streamContent = new StreamContent(stream); - 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; + streamContent.Headers.ContentType = fileParameter.ContentType.AsMediaTypeHeaderValue; - mpContent.Add(fileContent); - } + 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; - Content = mpContent; + return streamContent; } HttpContent Serialize(BodyParameter body) { @@ -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 From a98f963c50663afc5165c5d6d6d916e5ed7846e9 Mon Sep 17 00:00:00 2001 From: GMIKE Date: Sat, 22 Apr 2023 11:04:42 +0100 Subject: [PATCH 2/4] AlwaysSingleFileAsContent flag --- src/RestSharp/Request/RequestContent.cs | 42 ++++++++++++++----------- src/RestSharp/Request/RestRequest.cs | 7 ++++- 2 files changed, 30 insertions(+), 19 deletions(-) diff --git a/src/RestSharp/Request/RequestContent.cs b/src/RestSharp/Request/RequestContent.cs index 5625884bc..f1155c2dc 100644 --- a/src/RestSharp/Request/RequestContent.cs +++ b/src/RestSharp/Request/RequestContent.cs @@ -38,6 +38,9 @@ class RequestContent : IDisposable { public HttpContent BuildContent() { + if (_request.AlwaysMultipartFormData && _request.AlwaysSingleFileAsContent) + throw new ArgumentException("Failed to put file as content because flag AlwaysMultipartFormData enabled"); + var postParameters = _parameters.GetContentParameters(_request.Method).ToArray(); var postParametersExists = postParameters.Length > 0; var bodyParametersExists = _request.TryGetBodyParameter(out var bodyParameter); @@ -57,24 +60,27 @@ public HttpContent BuildContent() return Content!; } - void AddFiles(bool postParametersExists, bool bodyParametersExists) - { - // File uploading without multipart/form-data - if (!postParametersExists && !bodyParametersExists && _request.Files.Count == 1 && !_request.AlwaysMultipartFormData) - { - 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; - } - + void AddFiles(bool postParametersExists, bool bodyParametersExists) + { + // File uploading without multipart/form-data + if (_request.AlwaysSingleFileAsContent && _request.Files.Count == 1) + { + 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"); + + 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(); diff --git a/src/RestSharp/Request/RestRequest.cs b/src/RestSharp/Request/RestRequest.cs index bc3b68382..9748fa9cf 100644 --- a/src/RestSharp/Request/RestRequest.cs +++ b/src/RestSharp/Request/RestRequest.cs @@ -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. /// public bool AlwaysMultipartFormData { get; set; } - + + /// + /// Always send a file as request content without multipart/form-data request - even when the request contains only one file parameter + /// + public bool AlwaysSingleFileAsContent { get; set; } + /// /// 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 From 91f25240a2c3f150360303216870320330424846 Mon Sep 17 00:00:00 2001 From: GMIKE Date: Sun, 23 Apr 2023 20:53:48 +0100 Subject: [PATCH 3/4] ValidateParameters method --- src/RestSharp/Request/RequestContent.cs | 22 +++++++------------ .../Request/RestRequestExtensions.cs | 17 ++++++++++++++ src/RestSharp/RestClient.Async.cs | 1 + 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/src/RestSharp/Request/RequestContent.cs b/src/RestSharp/Request/RequestContent.cs index f1155c2dc..5a19f94fb 100644 --- a/src/RestSharp/Request/RequestContent.cs +++ b/src/RestSharp/Request/RequestContent.cs @@ -38,36 +38,30 @@ class RequestContent : IDisposable { public HttpContent BuildContent() { - if (_request.AlwaysMultipartFormData && _request.AlwaysSingleFileAsContent) - throw new ArgumentException("Failed to put file as content because flag AlwaysMultipartFormData enabled"); - - var postParameters = _parameters.GetContentParameters(_request.Method).ToArray(); + var postParameters = _parameters.GetContentParameters(_request.Method).ToArray(); var postParametersExists = postParameters.Length > 0; var bodyParametersExists = _request.TryGetBodyParameter(out var bodyParameter); - var filesExists = _request.Files.Any(); - + var filesExists = _request.Files.Any(); + if (filesExists) - AddFiles(postParametersExists, bodyParametersExists); - + AddFiles(); + if (bodyParametersExists) AddBody(postParametersExists, bodyParameter!); - + if (postParametersExists) AddPostParameters(postParameters); - + AddHeaders(); return Content!; } - void AddFiles(bool postParametersExists, bool bodyParametersExists) + void AddFiles() { // File uploading without multipart/form-data if (_request.AlwaysSingleFileAsContent && _request.Files.Count == 1) { - 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"); - var fileParameter = _request.Files.First(); Content = ToStreamContent(fileParameter); return; diff --git a/src/RestSharp/Request/RestRequestExtensions.cs b/src/RestSharp/Request/RestRequestExtensions.cs index ce33b2611..b2ff8469e 100644 --- a/src/RestSharp/Request/RestRequestExtensions.cs +++ b/src/RestSharp/Request/RestRequestExtensions.cs @@ -501,4 +501,21 @@ public static RestRequest AddXmlBody(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"); + } + } } diff --git a/src/RestSharp/RestClient.Async.cs b/src/RestSharp/RestClient.Async.cs index c5da81007..d3701e221 100644 --- a/src/RestSharp/RestClient.Async.cs +++ b/src/RestSharp/RestClient.Async.cs @@ -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); From 1799a42cdd912beb24890a37c7f39c7ddd18f143 Mon Sep 17 00:00:00 2001 From: GMIKE Date: Sun, 23 Apr 2023 21:35:13 +0100 Subject: [PATCH 4/4] ValidateParameters tests --- .../RestRequestParametersTests.cs | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 test/RestSharp.Tests/RestRequestParametersTests.cs diff --git a/test/RestSharp.Tests/RestRequestParametersTests.cs b/test/RestSharp.Tests/RestRequestParametersTests.cs new file mode 100644 index 000000000..7370644d3 --- /dev/null +++ b/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( + () => { 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( + () => { request.ValidateParameters(); } + ); + } + + [Fact] + public void RestRequest_AlwaysSingleFileAsContent_And_BodyParameters_IsNotAllowed() { + var request = new RestRequest { + AlwaysSingleFileAsContent = true, + }; + + request.AddParameter("name", "value", ParameterType.RequestBody); + + Assert.Throws( + () => { request.ValidateParameters(); } + ); + } +} \ No newline at end of file