From fef25a56d57f6ac8377d34ce50451ef3be95e9e7 Mon Sep 17 00:00:00 2001 From: Alexey Zimarev Date: Tue, 30 Jul 2024 03:09:56 -0400 Subject: [PATCH 01/10] Allow setting parameter content type (#2249) * Allow setting parameter content type * Added a test * Add docs --- docs/docs/usage/request.md | 19 ++++++++++ src/RestSharp/Parameters/Parameter.cs | 37 ++++++++++++++++--- .../MultipartFormDataTests.cs | 28 +++++++++++++- 3 files changed, 77 insertions(+), 7 deletions(-) diff --git a/docs/docs/usage/request.md b/docs/docs/usage/request.md index fd8eef508..374ee14dd 100644 --- a/docs/docs/usage/request.md +++ b/docs/docs/usage/request.md @@ -71,11 +71,30 @@ request.AddParameter("name", "Væ üé", false); // don't encode the value If you have files, RestSharp will send a `multipart/form-data` request. Your parameters will be part of this request in the form: ``` +Content-Type: text/plain; charset=utf-8 Content-Disposition: form-data; name="parameterName" ParameterValue ``` +Sometimes, you need to override the default content type for the parameter when making a multipart form call. It's possible to do by setting the `ContentType` property of the parameter object. As an example, the code below will create a POST parameter with JSON value, and set the appropriate content type: + +```csharp +var parameter = new GetOrPostParameter("someJson", "{\"attributeFormat\":\"pdf\"}") { + ContentType = "application/json" +}; +request.AddParameter(parameter); +``` + +When the request is set to use multipart content, the parameter will be sent as part of the request with the specified content type: + +``` +Content-Type: application/json; charset=utf-8 +Content-Disposition: form-data; name="someJson" + +{"attributeFormat":"pdf"} +``` + You can also add `GetOrPost` parameter as a default parameter to the client. This will add the parameter to every request made by the client. ```csharp diff --git a/src/RestSharp/Parameters/Parameter.cs b/src/RestSharp/Parameters/Parameter.cs index 903b33402..b23c592dc 100644 --- a/src/RestSharp/Parameters/Parameter.cs +++ b/src/RestSharp/Parameters/Parameter.cs @@ -32,13 +32,29 @@ protected Parameter(string? name, object? value, ParameterType type, bool encode } /// - /// MIME content type of the parameter + /// Content type of the parameter. Normally applies to the body parameter, or POST parameter in multipart requests. /// - public ContentType ContentType { get; protected init; } = ContentType.Undefined; - public string? Name { get; } - public object? Value { get; } - public ParameterType Type { get; } - public bool Encode { get; } + public ContentType ContentType { get; set; } = ContentType.Undefined; + + /// + /// Parameter name + /// + public string? Name { get; } + + /// + /// Parameter value + /// + public object? Value { get; } + + /// + /// Parameter type + /// + public ParameterType Type { get; } + + /// + /// Indicates if the parameter value should be encoded or not. + /// + public bool Encode { get; } /// /// Return a human-readable representation of this parameter @@ -48,6 +64,15 @@ protected Parameter(string? name, object? value, ParameterType type, bool encode protected virtual string ValueString => Value?.ToString() ?? "null"; + /// + /// Creates a parameter object of based on the type + /// + /// Parameter name + /// Parameter value + /// Parameter type + /// Indicates if the parameter value should be encoded + /// + /// public static Parameter CreateParameter(string? name, object? value, ParameterType type, bool encode = true) // ReSharper disable once SwitchExpressionHandlesSomeKnownEnumValuesWithExceptionInDefault => type switch { diff --git a/test/RestSharp.Tests.Integrated/MultipartFormDataTests.cs b/test/RestSharp.Tests.Integrated/MultipartFormDataTests.cs index 389329235..28d0ac825 100644 --- a/test/RestSharp.Tests.Integrated/MultipartFormDataTests.cs +++ b/test/RestSharp.Tests.Integrated/MultipartFormDataTests.cs @@ -13,6 +13,7 @@ public MultipartFormDataTests(ITestOutputHelper output) { _server = WireMockServer.Start(); _capturer = _server.ConfigureBodyCapturer(Method.Post); + var options = new RestClientOptions($"{_server.Url!}{RequestBodyCapturer.Resource}") { ConfigureMessageHandler = handler => new HttpTracerHandler(handler, new OutputLogger(output), HttpMessageParts.All) }; @@ -180,7 +181,7 @@ public async Task MultipartFormDataAsync() { _capturer.Body.Should().Be(expected); } - + [Fact] public async Task MultipartFormData_Without_File_Creates_A_Valid_RequestBody() { using var client = new RestClient(_server.Url!); @@ -206,4 +207,29 @@ public async Task MultipartFormData_Without_File_Creates_A_Valid_RequestBody() { var actual = capturer.Body!.Replace("\n", string.Empty).Split('\r'); actual.Should().Contain(expectedBody); } + + [Fact] + public async Task PostParameter_contentType_in_multipart_form() { + using var client = new RestClient(_server.Url!); + + var request = new RestRequest(RequestBodyCapturer.Resource, Method.Post) { + AlwaysMultipartFormData = true + }; + var capturer = _server.ConfigureBodyCapturer(Method.Post); + + const string parameterName = "Arequest"; + const string parameterValue = "{\"attributeFormat\":\"pdf\"}"; + + var parameter = new GetOrPostParameter(parameterName, parameterValue) { + ContentType = "application/json" + }; + request.AddParameter(parameter); + + await client.ExecuteAsync(request); + + var actual = capturer.Body!.Replace("\n", string.Empty).Split('\r'); + actual[1].Should().Be("Content-Type: application/json; charset=utf-8"); + actual[2].Should().Be($"Content-Disposition: form-data; name={parameterName}"); + actual[4].Should().Be(parameterValue); + } } \ No newline at end of file From aa43b68851ed930712295e25150cf456feaf5d6e Mon Sep 17 00:00:00 2001 From: minhtaile2712 Date: Tue, 30 Jul 2024 14:11:51 +0700 Subject: [PATCH 02/10] Correct methods name (#2243) --- docs/versioned_docs/version-v111/intro.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/versioned_docs/version-v111/intro.md b/docs/versioned_docs/version-v111/intro.md index 1aa03999c..6d8d61664 100644 --- a/docs/versioned_docs/version-v111/intro.md +++ b/docs/versioned_docs/version-v111/intro.md @@ -63,7 +63,7 @@ var timeline = await client.GetAsync(request, cancellationToken); Both snippets above use the `GetAsync` extension, which is a wrapper about `ExecuteGetAsync`, which, in turn, is a wrapper around `ExecuteAsync`. All `ExecuteAsync` overloads and return the `RestResponse` or `RestResponse`. -The most important difference is that async methods named after HTTP methods (like `GetAsync` or `PostAsync`) return `Task` instead of `Task>`. It means that you won't get an error response if the request fails as those methods throw an exception for unsuccessful HTTP calls. For keeping the API consistent, non-generic functions like `GetAsync` or `PostAsync` also throw an exception if the request fails, although they return the `Task`. +The most important difference is that async methods named after HTTP methods (like `GetAsync` or `PostAsync`) return `Task` instead of `Task>`. It means that you won't get an error response if the request fails as those methods throw an exception for unsuccessful HTTP calls. For keeping the API consistent, non-generic functions like `GetAsync` or `PostAsync` also throw an exception if the request fails, although they return the `Task`. Read [here](advanced/error-handling.md) about how RestSharp handles exceptions. From 6c596fe9f1ab8e8662467ab465520352678d00a1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Jul 2024 09:12:09 +0200 Subject: [PATCH 03/10] Bump Polly from 8.3.1 to 8.4.1 (#2234) Bumps [Polly](https://github.com/App-vNext/Polly) from 8.3.1 to 8.4.1. - [Release notes](https://github.com/App-vNext/Polly/releases) - [Changelog](https://github.com/App-vNext/Polly/blob/main/CHANGELOG.md) - [Commits](https://github.com/App-vNext/Polly/compare/8.3.1...8.4.1) --- updated-dependencies: - dependency-name: Polly dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> From 8fee05ddd05365d1f21c43e189504bcfa4aee757 Mon Sep 17 00:00:00 2001 From: minhtaile2712 Date: Tue, 30 Jul 2024 14:12:39 +0700 Subject: [PATCH 04/10] Correct typo (#2244) --- docs/versioned_docs/version-v111/usage/example.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/versioned_docs/version-v111/usage/example.md b/docs/versioned_docs/version-v111/usage/example.md index 6182d8ff3..bb0644552 100644 --- a/docs/versioned_docs/version-v111/usage/example.md +++ b/docs/versioned_docs/version-v111/usage/example.md @@ -12,7 +12,7 @@ For example, let's look at a simple Twitter API v2 client, which uses OAuth2 mac Before implementing an API client, we need to have a model for it. The model includes an abstraction for the client, which has functions for the API calls we are interested to implement. In addition, the client model would include the necessary request and response models. Usually those are simple classes or records without logic, which are often referred to as DTOs (data transfer objects). -This example starts with a single function that retrieves one Twitter user. Lets being by defining the API client interface: +This example starts with a single function that retrieves one Twitter user. Let's begin by defining the API client interface: ```csharp public interface ITwitterClient { @@ -149,4 +149,4 @@ Sample code provided on this page is a production code. For example, the authent ## Final words -This page demonstrates how an API client can be implemented as a typed, configurable client with its own interface. Usage of the client in applications is not covered here as different application types and target frameworks have their own idiomatic ways to use HTTP clients. \ No newline at end of file +This page demonstrates how an API client can be implemented as a typed, configurable client with its own interface. Usage of the client in applications is not covered here as different application types and target frameworks have their own idiomatic ways to use HTTP clients. From 875849372d672f408f50a2e88fe882995324c305 Mon Sep 17 00:00:00 2001 From: minhtaile2712 Date: Tue, 30 Jul 2024 14:12:51 +0700 Subject: [PATCH 05/10] Correct typo (#2246) --- docs/versioned_docs/version-v111/usage/example.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/versioned_docs/version-v111/usage/example.md b/docs/versioned_docs/version-v111/usage/example.md index bb0644552..3e1bc8ec2 100644 --- a/docs/versioned_docs/version-v111/usage/example.md +++ b/docs/versioned_docs/version-v111/usage/example.md @@ -73,7 +73,7 @@ public class TwitterClientOptions(string ApiKey, string ApiSecret); public TwitterClient(IOptions options) { var opt = new RestClientOptions("https://api.twitter.com/2"); - _client = new RestClient(options); + _client = new RestClient(opt); } ``` From 75ccc0087efd78b3d1db0fe630e46582badfea43 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Jul 2024 09:13:04 +0200 Subject: [PATCH 06/10] Bump JetBrains.Annotations from 2023.3.0 to 2024.2.0 (#2237) Bumps JetBrains.Annotations from 2023.3.0 to 2024.2.0. --- updated-dependencies: - dependency-name: JetBrains.Annotations dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> From 6391624bfaaf3f4df56481d308e755e96b5eb1b9 Mon Sep 17 00:00:00 2001 From: "Jan Ivar Z. Carlsen" Date: Tue, 30 Jul 2024 09:13:59 +0200 Subject: [PATCH 07/10] Added extensions for getting content header values (#2247) --- .../Response/RestResponseExtensions.cs | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/RestSharp/Response/RestResponseExtensions.cs b/src/RestSharp/Response/RestResponseExtensions.cs index 4a039ed7b..32735f571 100644 --- a/src/RestSharp/Response/RestResponseExtensions.cs +++ b/src/RestSharp/Response/RestResponseExtensions.cs @@ -22,7 +22,7 @@ public static class RestResponseExtensions { /// Name of the header /// Header value or null if the header is not found in the response public static string? GetHeaderValue(this RestResponse response, string headerName) - => response.Headers?.FirstOrDefault(x => NameIs(x.Name, headerName))?.Value?.ToString(); + => response.Headers?.FirstOrDefault(x => NameIs(x.Name, headerName))?.Value.ToString(); /// /// Gets all the values of the header with the specified name. @@ -33,7 +33,29 @@ public static class RestResponseExtensions { public static string[] GetHeaderValues(this RestResponse response, string headerName) => response.Headers ?.Where(x => NameIs(x.Name, headerName)) - .Select(x => x.Value?.ToString() ?? "") + .Select(x => x.Value.ToString() ?? "") + .ToArray() ?? + []; + + /// + /// Gets the value of the content header with the specified name. + /// + /// Response object + /// Name of the header + /// Header value or null if the content header is not found in the response + public static string? GetContentHeaderValue(this RestResponse response, string headerName) + => response.ContentHeaders?.FirstOrDefault(x => NameIs(x.Name, headerName))?.Value.ToString(); + + /// + /// Gets all the values of the content header with the specified name. + /// + /// Response object + /// Name of the header + /// Array of header values or empty array if the content header is not found in the response + public static string[] GetContentHeaderValues(this RestResponse response, string headerName) + => response.ContentHeaders + ?.Where(x => NameIs(x.Name, headerName)) + .Select(x => x.Value.ToString() ?? "") .ToArray() ?? []; From 777bf194ec2d14271e7807cc704e73ec18fcaf7e Mon Sep 17 00:00:00 2001 From: Kenny Carneal Date: Tue, 27 Aug 2024 05:51:20 -0400 Subject: [PATCH 08/10] Fixed sentence to be easier to read (#2251) Fixed a sentence that accidentally used the word "only" twice. --- docs/versioned_docs/version-v111/intro.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/versioned_docs/version-v111/intro.md b/docs/versioned_docs/version-v111/intro.md index 6d8d61664..8144a61cd 100644 --- a/docs/versioned_docs/version-v111/intro.md +++ b/docs/versioned_docs/version-v111/intro.md @@ -84,7 +84,7 @@ There is no need to set the `Content-Type` or add the `DataFormat` parameter to RestSharp will also handle both XML and JSON responses and perform all necessary deserialization tasks, depending on the server response type. Therefore, you only need to add the `Accept` header if you want to deserialize the response manually. -For example, only you'd only need these lines to make a request with JSON body: +For example, you'd only need these lines to make a request with JSON body: ```csharp var request = new RestRequest("address/update").AddJsonBody(updatedAddress); From 7b7950b1b6cfb7f1251f6c2e4cf74062ace41e65 Mon Sep 17 00:00:00 2001 From: Sergey Nechaev <6499856+snechaev@users.noreply.github.com> Date: Thu, 29 Aug 2024 20:59:33 +0200 Subject: [PATCH 09/10] Fixed the Simple Factory example. Fixes #2255 (#2256) --- docs/docs/usage/client.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/docs/usage/client.md b/docs/docs/usage/client.md index 0207c3e25..e84b955f4 100644 --- a/docs/docs/usage/client.md +++ b/docs/docs/usage/client.md @@ -56,7 +56,8 @@ Constructor parameters to configure the `HttpMessageHandler` and default `HttpCl You need to set the `useClientFactory` parameter to `true` in the `RestClient` constructor to enable the factory. ```csharp -var client = new RestClient("https://api.twitter.com/2", true); +var options = new RestClientOptions("https://api.twitter.com/2"); +var client = new RestClient(options, useClientFactory: true); ``` ## Reusing HttpClient From 0fba5e727d241b1867bd71efc912594075c2934b Mon Sep 17 00:00:00 2001 From: Alexey Zimarev Date: Thu, 29 Aug 2024 21:04:54 +0200 Subject: [PATCH 10/10] Don't allow CRLF in headers (#2258) --- src/RestSharp/Parameters/HeaderParameter.cs | 61 +++++++++++++++++-- .../Request/RestRequestExtensions.Headers.cs | 32 ++-------- test/RestSharp.Tests/RequestHeaderTests.cs | 6 ++ 3 files changed, 65 insertions(+), 34 deletions(-) diff --git a/src/RestSharp/Parameters/HeaderParameter.cs b/src/RestSharp/Parameters/HeaderParameter.cs index 1607eaeda..7dce5df92 100644 --- a/src/RestSharp/Parameters/HeaderParameter.cs +++ b/src/RestSharp/Parameters/HeaderParameter.cs @@ -13,22 +13,71 @@ // limitations under the License. // +using System.Text; +using System.Text.RegularExpressions; + namespace RestSharp; -public record HeaderParameter : Parameter { +public partial record HeaderParameter : Parameter { /// /// Instantiates a header parameter /// - /// Parameter name - /// Parameter value - public HeaderParameter(string name, string value) + /// Header name + /// Header value + /// Set to true to encode header value according to RFC 2047. Default is false. + public HeaderParameter(string name, string value, bool encode = false) : base( - Ensure.NotEmptyString(name, nameof(name)), - Ensure.NotNull(value, nameof(value)), + EnsureValidHeaderString(Ensure.NotEmptyString(name, nameof(name)), "name"), + EnsureValidHeaderValue(name, value, encode), ParameterType.HttpHeader, false ) { } public new string Name => base.Name!; public new string Value => (string)base.Value!; + + static string EnsureValidHeaderValue(string name, string value, bool encode) { + CheckAndThrowsForInvalidHost(name, value); + + return EnsureValidHeaderString(GetValue(Ensure.NotNull(value, nameof(value)), encode), "value"); + } + + static string EnsureValidHeaderString(string value, string type) + => !IsInvalidHeaderString(value) ? value : throw new ArgumentException($"Invalid character found in header {type}: {value}"); + + static string GetValue(string value, bool encode) => encode ? GetBase64EncodedHeaderValue(value) : value; + + static string GetBase64EncodedHeaderValue(string value) => $"=?UTF-8?B?{Convert.ToBase64String(Encoding.UTF8.GetBytes(value))}?="; + + static bool IsInvalidHeaderString(string stringValue) { + // ReSharper disable once ForCanBeConvertedToForeach + for (var i = 0; i < stringValue.Length; i++) { + switch (stringValue[i]) { + case '\t': + case '\r': + case '\n': + return true; + } + } + + return false; + } + + static readonly Regex PortSplitRegex = PartSplit(); + + static void CheckAndThrowsForInvalidHost(string name, string value) { + if (name == KnownHeaders.Host && InvalidHost(value)) + throw new ArgumentException("The specified value is not a valid Host header string.", nameof(value)); + + return; + + static bool InvalidHost(string host) => Uri.CheckHostName(PortSplitRegex.Split(host)[0]) == UriHostNameType.Unknown; + } + +#if NET7_0_OR_GREATER + [GeneratedRegex(@":\d+")] + private static partial Regex PartSplit(); +#else + static Regex PartSplit() => new(@":\d+"); +#endif } \ No newline at end of file diff --git a/src/RestSharp/Request/RestRequestExtensions.Headers.cs b/src/RestSharp/Request/RestRequestExtensions.Headers.cs index 8091a1a50..d3e6b7815 100644 --- a/src/RestSharp/Request/RestRequestExtensions.Headers.cs +++ b/src/RestSharp/Request/RestRequestExtensions.Headers.cs @@ -12,8 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.Text.RegularExpressions; - namespace RestSharp; public static partial class RestRequestExtensions { @@ -39,10 +37,8 @@ public static RestRequest AddHeader(this RestRequest request, string name, strin /// Header name /// Header value /// - public static RestRequest AddHeader(this RestRequest request, string name, string value) { - CheckAndThrowsForInvalidHost(name, value); - return request.AddParameter(new HeaderParameter(name, value)); - } + public static RestRequest AddHeader(this RestRequest request, string name, string value) + => request.AddParameter(new HeaderParameter(name, value)); /// /// Adds a header to the request. RestSharp will try to separate request and content headers when calling the resource. @@ -62,10 +58,8 @@ public static RestRequest AddHeader(this RestRequest request, string name, T /// Header name /// Header value /// - public static RestRequest AddOrUpdateHeader(this RestRequest request, string name, string value) { - CheckAndThrowsForInvalidHost(name, value); - return request.AddOrUpdateParameter(new HeaderParameter(name, value)); - } + public static RestRequest AddOrUpdateHeader(this RestRequest request, string name, string value) + => request.AddOrUpdateParameter(new HeaderParameter(name, value)); /// /// Adds or updates the request header. RestSharp will try to separate request and content headers when calling the resource. @@ -121,22 +115,4 @@ static void CheckAndThrowsDuplicateKeys(ICollection throw new ArgumentException($"Duplicate header names exist: {string.Join(", ", duplicateKeys)}"); } } - - static readonly Regex PortSplitRegex = PartSplit(); - - static void CheckAndThrowsForInvalidHost(string name, string value) { - if (name == KnownHeaders.Host && InvalidHost(value)) - throw new ArgumentException("The specified value is not a valid Host header string.", nameof(value)); - - return; - - static bool InvalidHost(string host) => Uri.CheckHostName(PortSplitRegex.Split(host)[0]) == UriHostNameType.Unknown; - } - -#if NET7_0_OR_GREATER - [GeneratedRegex(@":\d+")] - private static partial Regex PartSplit(); -#else - static Regex PartSplit() => new(@":\d+"); -#endif } \ No newline at end of file diff --git a/test/RestSharp.Tests/RequestHeaderTests.cs b/test/RestSharp.Tests/RequestHeaderTests.cs index 6fa6c8215..bf8bc4c4b 100644 --- a/test/RestSharp.Tests/RequestHeaderTests.cs +++ b/test/RestSharp.Tests/RequestHeaderTests.cs @@ -174,6 +174,12 @@ public void Should_not_allow_empty_header_name() { var request = new RestRequest(); Assert.Throws("name", () => request.AddHeader("", "value")); } + + [Fact] + public void Should_not_allow_CRLF_in_header_value() { + var request = new RestRequest(); + Assert.Throws(() => request.AddHeader("name", "test\r\nUser-Agent: injected header!\r\n\r\nGET /smuggled HTTP/1.1\r\nHost: insert.some.site.here")); + } static Parameter[] GetHeaders(RestRequest request) => request.Parameters.Where(x => x.Type == ParameterType.HttpHeader).ToArray(); pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy