Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/RestSharp/Extensions/StringExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,9 @@ internal IEnumerable<string> GetNameVariants(CultureInfo culture) {
}
}

internal static string? ToStringValue(this object? value, CultureInfo? culture = null)
=> value is IFormattable f ? f.ToString(null, culture ?? CultureInfo.InvariantCulture) : value?.ToString();

internal static bool IsEmpty([NotNullWhen(false)] this string? value) => string.IsNullOrWhiteSpace(value);

internal static bool IsNotEmpty([NotNullWhen(true)] this string? value) => !string.IsNullOrWhiteSpace(value);
Expand Down
5 changes: 4 additions & 1 deletion src/RestSharp/Parameters/ObjectParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@

using System.Reflection;

using System.Globalization;
using RestSharp.Extensions;

namespace RestSharp;

static class ObjectParser {
Expand Down Expand Up @@ -72,7 +75,7 @@ IEnumerable<ParsedParameter> GetArray(PropertyInfo propertyInfo, object? value)
bool IsAllowedProperty(string propertyName)
=> includedProperties.Length == 0 || includedProperties.Length > 0 && includedProperties.Contains(propertyName);

string? ParseValue(string? format, object? value) => format == null ? value?.ToString() : string.Format($"{{0:{format}}}", value);
string? ParseValue(string? format, object? value) => format == null ? value.ToStringValue() : string.Format(CultureInfo.InvariantCulture, $"{{0:{format}}}", value);
}
}

Expand Down
9 changes: 5 additions & 4 deletions src/RestSharp/Parameters/Parameter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// limitations under the License.

using System.Diagnostics;
using RestSharp.Extensions;

namespace RestSharp;

Expand Down Expand Up @@ -76,10 +77,10 @@ protected Parameter(string? name, object? value, ParameterType type, bool encode
public static Parameter CreateParameter(string? name, object? value, ParameterType type, bool encode = true)
// ReSharper disable once SwitchExpressionHandlesSomeKnownEnumValuesWithExceptionInDefault
=> type switch {
ParameterType.GetOrPost => new GetOrPostParameter(Ensure.NotEmptyString(name, nameof(name)), value?.ToString(), encode),
ParameterType.UrlSegment => new UrlSegmentParameter(Ensure.NotEmptyString(name, nameof(name)), value?.ToString()!, encode),
ParameterType.HttpHeader => new HeaderParameter(name!, value?.ToString()!),
ParameterType.QueryString => new QueryParameter(Ensure.NotEmptyString(name, nameof(name)), value?.ToString(), encode),
ParameterType.GetOrPost => new GetOrPostParameter(Ensure.NotEmptyString(name, nameof(name)), value.ToStringValue(), encode),
ParameterType.UrlSegment => new UrlSegmentParameter(Ensure.NotEmptyString(name, nameof(name)), value.ToStringValue()!, encode),
ParameterType.HttpHeader => new HeaderParameter(name!, value.ToStringValue()!),
ParameterType.QueryString => new QueryParameter(Ensure.NotEmptyString(name, nameof(name)), value.ToStringValue(), encode),
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null)
};

Expand Down
15 changes: 11 additions & 4 deletions src/RestSharp/Request/RestRequestExtensions.Headers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.

using System.Globalization;
using RestSharp.Extensions;

namespace RestSharp;

public static partial class RestRequestExtensions {
Expand Down Expand Up @@ -42,12 +45,14 @@ public RestRequest AddHeader(string name, string value)

/// <summary>
/// Adds a header to the request. RestSharp will try to separate request and content headers when calling the resource.
/// The value will be converted to string using the specified culture, or <see cref="CultureInfo.InvariantCulture"/> by default.
/// </summary>
/// <param name="name">Header name</param>
/// <param name="value">Header value</param>
/// <param name="culture">Culture to use for formatting the value, defaults to <see cref="CultureInfo.InvariantCulture"/></param>
/// <returns></returns>
public RestRequest AddHeader<T>(string name, T value) where T : struct
=> request.AddHeader(name, Ensure.NotNull(value.ToString(), nameof(value)));
public RestRequest AddHeader<T>(string name, T value, CultureInfo? culture = null) where T : struct
=> request.AddHeader(name, Ensure.NotNull(value.ToStringValue(culture), nameof(value)));

/// <summary>
/// Adds or updates the request header. RestSharp will try to separate request and content headers when calling the resource.
Expand All @@ -62,12 +67,14 @@ public RestRequest AddOrUpdateHeader(string name, string value)
/// <summary>
/// Adds or updates the request header. RestSharp will try to separate request and content headers when calling the resource.
/// The existing header with the same name will be replaced.
/// The value will be converted to string using the specified culture, or <see cref="CultureInfo.InvariantCulture"/> by default.
/// </summary>
/// <param name="name">Header name</param>
/// <param name="value">Header value</param>
/// <param name="culture">Culture to use for formatting the value, defaults to <see cref="CultureInfo.InvariantCulture"/></param>
/// <returns></returns>
public RestRequest AddOrUpdateHeader<T>(string name, T value) where T : struct
=> request.AddOrUpdateHeader(name, Ensure.NotNull(value.ToString(), nameof(value)));
public RestRequest AddOrUpdateHeader<T>(string name, T value, CultureInfo? culture = null) where T : struct
=> request.AddOrUpdateHeader(name, Ensure.NotNull(value.ToStringValue(culture), nameof(value)));

/// <summary>
/// Adds multiple headers to the request, using the key-value pairs provided.
Expand Down
9 changes: 7 additions & 2 deletions src/RestSharp/Request/RestRequestExtensions.Query.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.

using System.Globalization;
using RestSharp.Extensions;

namespace RestSharp;

public static partial class RestRequestExtensions {
Expand All @@ -31,12 +34,14 @@ public RestRequest AddQueryParameter(string name, string? value, bool encode = t
/// <summary>
/// Adds a query string parameter to the request. The request resource should not contain any placeholders for this parameter.
/// The parameter will be added to the request URL as a query string using name=value format.
/// The value will be converted to string using the specified culture, or <see cref="CultureInfo.InvariantCulture"/> by default.
/// </summary>
/// <param name="name">Parameter name</param>
/// <param name="value">Parameter value</param>
/// <param name="encode">Encode the value or not, default true</param>
/// <param name="culture">Culture to use for formatting the value, defaults to <see cref="CultureInfo.InvariantCulture"/></param>
/// <returns></returns>
public RestRequest AddQueryParameter<T>(string name, T value, bool encode = true) where T : struct
=> request.AddQueryParameter(name, value.ToString(), encode);
public RestRequest AddQueryParameter<T>(string name, T value, bool encode = true, CultureInfo? culture = null) where T : struct
=> request.AddQueryParameter(name, value.ToStringValue(culture), encode);
}
}
9 changes: 7 additions & 2 deletions src/RestSharp/Request/RestRequestExtensions.Url.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.

using System.Globalization;
using RestSharp.Extensions;

namespace RestSharp;

public static partial class RestRequestExtensions {
Expand All @@ -31,12 +34,14 @@ public RestRequest AddUrlSegment(string name, string? value, bool encode = true)
/// <summary>
/// Adds a URL segment parameter to the request. The resource URL must have a placeholder for the parameter for it to work.
/// For example, if you add a URL segment parameter with the name "id", the resource URL should contain {id} in its path.
/// The value will be converted to string using the specified culture, or <see cref="CultureInfo.InvariantCulture"/> by default.
/// </summary>
/// <param name="name">Name of the parameter; must be matching a placeholder in the resource URL as {name}</param>
/// <param name="value">Value of the parameter</param>
/// <param name="encode">Encode the value or not, default true</param>
/// <param name="culture">Culture to use for formatting the value, defaults to <see cref="CultureInfo.InvariantCulture"/></param>
/// <returns></returns>
public RestRequest AddUrlSegment<T>(string name, T value, bool encode = true) where T : struct
=> request.AddUrlSegment(name, value.ToString(), encode);
public RestRequest AddUrlSegment<T>(string name, T value, bool encode = true, CultureInfo? culture = null) where T : struct
=> request.AddUrlSegment(name, value.ToStringValue(culture), encode);
}
}
18 changes: 12 additions & 6 deletions src/RestSharp/Request/RestRequestExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.

using System.Globalization;
using RestSharp.Extensions;

namespace RestSharp;

[PublicAPI]
Expand Down Expand Up @@ -45,14 +48,15 @@ public RestRequest AddParameter(string? name, object value, ParameterType type,

/// <summary>
/// Adds a HTTP parameter to the request (QueryString for GET, DELETE, OPTIONS and HEAD; Encoded form for POST and PUT).
/// The value will be converted to string.
/// The value will be converted to string using the specified culture, or <see cref="CultureInfo.InvariantCulture"/> by default.
/// </summary>
/// <param name="name">Name of the parameter</param>
/// <param name="value">Value of the parameter</param>
/// <param name="encode">Encode the value or not, default true</param>
/// <param name="culture">Culture to use for formatting the value, defaults to <see cref="CultureInfo.InvariantCulture"/></param>
/// <returns>This request</returns>
public RestRequest AddParameter<T>(string name, T value, bool encode = true) where T : struct
=> request.AddParameter(name, value.ToString(), encode);
public RestRequest AddParameter<T>(string name, T value, bool encode = true, CultureInfo? culture = null) where T : struct
=> request.AddParameter(name, value.ToStringValue(culture), encode);
Comment on lines +58 to +59

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Binary-breaking api signature change 🐞 Bug ⛯ Reliability

Several public generic RestRequest extension methods were changed to add a CultureInfo parameter
instead of adding new overloads. This breaks binary compatibility: consumers compiled against prior
versions can hit MissingMethodException at runtime when loading the updated assembly.
Agent Prompt
## Issue description
Public generic extension methods changed signatures by adding a new `CultureInfo? culture` parameter. This is source-compatible but **not binary-compatible**: existing compiled clients expecting the old signatures may fail at runtime with `MissingMethodException`.

## Issue Context
These methods are public API surface. Optional parameters do not preserve binary compatibility when the parameter is newly added (signature changes).

## Fix Focus Areas
- src/RestSharp/Request/RestRequestExtensions.cs[58-81]
- src/RestSharp/Request/RestRequestExtensions.Headers.cs[54-77]
- src/RestSharp/Request/RestRequestExtensions.Query.cs[44-45]
- src/RestSharp/Request/RestRequestExtensions.Url.cs[44-45]

## Suggested implementation sketch
- Add back overloads with the old signatures, e.g.:
  - `AddParameter<T>(string name, T value, bool encode = true)` calls `AddParameter(name, value, encode, culture: null)`.
  - Keep the new overload with `CultureInfo? culture = null`.
- Repeat for other affected generic methods.
- (Optional) Mark the old overloads `[Obsolete]` if you want to guide users toward the new culture-aware overloads, but keep them for binary compatibility.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


/// <summary>
/// Adds or updates a HTTP parameter to the request (QueryString for GET, DELETE, OPTIONS and HEAD; Encoded form for POST and PUT)
Expand All @@ -65,14 +69,16 @@ public RestRequest AddOrUpdateParameter(string name, string? value, bool encode
=> request.AddOrUpdateParameter(new GetOrPostParameter(name, value, encode));

/// <summary>
/// Adds or updates a HTTP parameter to the request (QueryString for GET, DELETE, OPTIONS and HEAD; Encoded form for POST and PUT)
/// Adds or updates a HTTP parameter to the request (QueryString for GET, DELETE, OPTIONS and HEAD; Encoded form for POST and PUT).
/// The value will be converted to string using the specified culture, or <see cref="CultureInfo.InvariantCulture"/> by default.
/// </summary>
/// <param name="name">Name of the parameter</param>
/// <param name="value">Value of the parameter</param>
/// <param name="encode">Encode the value or not, default true</param>
/// <param name="culture">Culture to use for formatting the value, defaults to <see cref="CultureInfo.InvariantCulture"/></param>
/// <returns>This request</returns>
public RestRequest AddOrUpdateParameter<T>(string name, T value, bool encode = true) where T : struct
=> request.AddOrUpdateParameter(name, value.ToString(), encode);
public RestRequest AddOrUpdateParameter<T>(string name, T value, bool encode = true, CultureInfo? culture = null) where T : struct
=> request.AddOrUpdateParameter(name, value.ToStringValue(culture), encode);

RestRequest AddParameters(IEnumerable<Parameter> parameters) {
request.Parameters.AddParameters(parameters);
Expand Down
6 changes: 4 additions & 2 deletions src/RestSharp/RestClient.Async.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,13 @@ public async Task<RestResponse> ExecuteAsync(RestRequest request, CancellationTo
}

static RestResponse GetErrorResponse(RestRequest request, Exception exception, CancellationToken timeoutToken) {
var timedOut = exception is OperationCanceledException && TimedOut();

var response = new RestResponse(request) {
ResponseStatus = exception is OperationCanceledException
? TimedOut() ? ResponseStatus.TimedOut : ResponseStatus.Aborted
? timedOut ? ResponseStatus.TimedOut : ResponseStatus.Aborted
: ResponseStatus.Error,
ErrorMessage = exception.GetBaseException().Message,
ErrorMessage = timedOut ? "The request timed out." : exception.GetBaseException().Message,
ErrorException = exception
};

Expand Down
98 changes: 98 additions & 0 deletions test/RestSharp.Tests/InvariantCultureTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
using System.Globalization;

namespace RestSharp.Tests;

public class InvariantCultureTests {
[Fact]
public void AddParameter_uses_invariant_culture_for_double() {
var originalCulture = CultureInfo.CurrentCulture;

try {
CultureInfo.CurrentCulture = new CultureInfo("da-DK");
var request = new RestRequest().AddParameter("value", 1.234);

var parameter = request.Parameters.FirstOrDefault(p => p.Name == "value");
parameter.Should().NotBeNull();
parameter!.Value.Should().Be("1.234");
}
finally {
CultureInfo.CurrentCulture = originalCulture;
}
}

[Fact]
public void AddParameter_can_use_specific_culture() {
var request = new RestRequest().AddParameter("value", 1.234, culture: new CultureInfo("da-DK"));

var parameter = request.Parameters.FirstOrDefault(p => p.Name == "value");
parameter.Should().NotBeNull();
parameter!.Value.Should().Be("1,234");
}

[Fact]
public void AddOrUpdateParameter_uses_invariant_culture_for_double() {
var originalCulture = CultureInfo.CurrentCulture;

try {
CultureInfo.CurrentCulture = new CultureInfo("da-DK");
var request = new RestRequest().AddOrUpdateParameter("value", 1.234);

var parameter = request.Parameters.FirstOrDefault(p => p.Name == "value");
parameter.Should().NotBeNull();
parameter!.Value.Should().Be("1.234");
}
finally {
CultureInfo.CurrentCulture = originalCulture;
}
}

[Fact]
public void AddQueryParameter_uses_invariant_culture_for_decimal() {
var originalCulture = CultureInfo.CurrentCulture;

try {
CultureInfo.CurrentCulture = new CultureInfo("fr-FR");
var request = new RestRequest().AddQueryParameter("price", 99.95m);

var parameter = request.Parameters.FirstOrDefault(p => p.Name == "price");
parameter.Should().NotBeNull();
parameter!.Value.Should().Be("99.95");
}
finally {
CultureInfo.CurrentCulture = originalCulture;
}
}

[Fact]
public void AddUrlSegment_uses_invariant_culture_for_float() {
var originalCulture = CultureInfo.CurrentCulture;

try {
CultureInfo.CurrentCulture = new CultureInfo("de-DE");
var request = new RestRequest("{id}").AddUrlSegment("id", 3.14f);

var parameter = request.Parameters.FirstOrDefault(p => p.Name == "id");
parameter.Should().NotBeNull();
parameter!.Value.Should().Be("3.14");
}
finally {
CultureInfo.CurrentCulture = originalCulture;
}
}

[Fact]
public void CreateParameter_uses_invariant_culture_for_object_value() {
var originalCulture = CultureInfo.CurrentCulture;

try {
CultureInfo.CurrentCulture = new CultureInfo("da-DK");
object value = 1.234;
var parameter = Parameter.CreateParameter("value", value, ParameterType.QueryString);

parameter.Value.Should().Be("1.234");
}
finally {
CultureInfo.CurrentCulture = originalCulture;
}
}
}
11 changes: 6 additions & 5 deletions test/RestSharp.Tests/ObjectParserTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
// ReSharper disable PropertyCanBeMadeInitOnly.Local
using System.Globalization;

namespace RestSharp.Tests;

public class ObjectParserTests {
Expand All @@ -17,11 +19,10 @@ public void ShouldUseRequestProperty() {

var parsed = request.GetProperties().ToDictionary(x => x.Name, x => x.Value);
parsed["some_data"].Should().Be(request.SomeData);
parsed["SomeDate"].Should().Be(request.SomeDate.ToString("d"));
parsed["Plain"].Should().Be(request.Plain.ToString());
// ReSharper disable once SpecifyACultureInStringConversionExplicitly
parsed["PlainArray"].Should().Be(string.Join(",", dates.Select(x => x.ToString())));
parsed["dates"].Should().Be(string.Join(",", dates.Select(x => x.ToString("d"))));
parsed["SomeDate"].Should().Be(request.SomeDate.ToString("d", CultureInfo.InvariantCulture));
parsed["Plain"].Should().Be(request.Plain.ToString(CultureInfo.InvariantCulture));
parsed["PlainArray"].Should().Be(string.Join(",", dates.Select(x => x.ToString(CultureInfo.InvariantCulture))));
parsed["dates"].Should().Be(string.Join(",", dates.Select(x => x.ToString("d", CultureInfo.InvariantCulture))));
}

[Fact]
Expand Down
Loading