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 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/docs/versioned_docs/version-v111/intro.md b/docs/versioned_docs/version-v111/intro.md index 1aa03999c..8144a61cd 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. @@ -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); diff --git a/docs/versioned_docs/version-v111/usage/example.md b/docs/versioned_docs/version-v111/usage/example.md index 6182d8ff3..3e1bc8ec2 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 { @@ -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); } ``` @@ -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. 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/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/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/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() ?? []; 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 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