From 4ddda247c8c2f9a2d0657d1f1f9384480c97083e Mon Sep 17 00:00:00 2001
From: Alexey Zimarev <alex@zimarev.com>
Date: Thu, 11 Jul 2024 19:37:24 +0200
Subject: [PATCH] Header value null check (#2241)

* Throw an exception when adding a header with `null` value
* Add multiple header values properly
* Default headers should allow multiple values
---
 Directory.Packages.props                      | 14 +++----
 gen/SourceGenerator/ImmutableGenerator.cs     | 25 +++++++-----
 .../Authenticators/JwtAuthenticator.cs        |  5 ++-
 .../Authenticators/OAuth/OAuthWorkflow.cs     | 12 +++---
 src/RestSharp/Ensure.cs                       |  7 ++--
 src/RestSharp/Options/RestClientOptions.cs    |  1 +
 src/RestSharp/Parameters/DefaultParameters.cs |  9 ++---
 src/RestSharp/Parameters/HeaderParameter.cs   | 11 ++++-
 src/RestSharp/Parameters/Parameter.cs         | 40 +++++++++++++++++--
 .../Parameters/ParametersCollection.cs        | 20 +++++-----
 src/RestSharp/Parameters/RequestParameters.cs |  2 +-
 .../Parameters/UrlSegmentParameter.cs         |  2 +-
 .../Request/HttpRequestMessageExtensions.cs   | 12 +++---
 src/RestSharp/Request/RequestHeaders.cs       | 16 ++++----
 src/RestSharp/Request/RestRequest.cs          |  4 +-
 .../Request/RestRequestExtensions.Headers.cs  | 19 ++++++++-
 src/RestSharp/Request/UriExtensions.cs        |  2 +-
 src/RestSharp/Response/RestResponseBase.cs    |  2 +-
 src/RestSharp/RestClient.cs                   | 17 ++++----
 .../HttpHeadersTests.cs                       |  3 +-
 .../SampleClasses/twitter.cs                  |  8 ++--
 test/RestSharp.Tests/RequestHeaderTests.cs    | 35 +++++++++++-----
 22 files changed, 172 insertions(+), 94 deletions(-)

diff --git a/Directory.Packages.props b/Directory.Packages.props
index 64df33b50..61c85f4c3 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -16,8 +16,8 @@
     <PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
     <PackageVersion Include="CsvHelper" Version="33.0.1" />
     <PackageVersion Include="PolySharp" Version="1.14.1" />
-    <PackageVersion Include="System.Text.Json" Version="8.0.3" />
-    <PackageVersion Include="WireMock.Net" Version="1.5.51" />
+    <PackageVersion Include="System.Text.Json" Version="8.0.4" />
+    <PackageVersion Include="WireMock.Net" Version="1.5.60" />
     <PackageVersion Include="WireMock.Net.FluentAssertions" Version="1.5.51" />
   </ItemGroup>
   <ItemGroup Label="Compile dependencies">
@@ -28,7 +28,7 @@
     <PackageVersion Include="Nullable" Version="1.3.1" />
     <PackageVersion Include="Microsoft.NETFramework.ReferenceAssemblies.net472" Version="1.0.3" />
     <PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
-    <PackageVersion Include="JetBrains.Annotations" Version="2023.3.0" />
+    <PackageVersion Include="JetBrains.Annotations" Version="2024.2.0" />
   </ItemGroup>
   <ItemGroup Label="Testing dependencies">
     <PackageVersion Include="AutoFixture" Version="4.18.1" />
@@ -36,14 +36,14 @@
     <PackageVersion Include="FluentAssertions" Version="6.12.0" />
     <PackageVersion Include="HttpTracer" Version="2.1.1" />
     <PackageVersion Include="Microsoft.AspNetCore.TestHost" Version="$(MicrosoftTestHostVer)" />
-    <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
+    <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
     <PackageVersion Include="Moq" Version="4.20.70" />
-    <PackageVersion Include="Polly" Version="8.3.1" />
+    <PackageVersion Include="Polly" Version="8.4.1" />
     <PackageVersion Include="rest-mock-core" Version="0.7.12" />
     <PackageVersion Include="RichardSzalay.MockHttp" Version="7.0.0" />
     <PackageVersion Include="System.Net.Http.Json" Version="8.0.0" />
     <PackageVersion Include="Xunit.Extensions.Logging" Version="1.1.0" />
-    <PackageVersion Include="xunit.runner.visualstudio" Version="2.5.7" PrivateAssets="All" />
-    <PackageVersion Include="xunit" Version="2.8.1" />
+    <PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" PrivateAssets="All" />
+    <PackageVersion Include="xunit" Version="2.9.0" />
   </ItemGroup>
 </Project>
\ No newline at end of file
diff --git a/gen/SourceGenerator/ImmutableGenerator.cs b/gen/SourceGenerator/ImmutableGenerator.cs
index 54ea9101b..7576359bd 100644
--- a/gen/SourceGenerator/ImmutableGenerator.cs
+++ b/gen/SourceGenerator/ImmutableGenerator.cs
@@ -32,22 +32,24 @@ public void Execute(GeneratorExecutionContext context) {
 
     static string GenerateImmutableClass(TypeDeclarationSyntax mutableClass, Compilation compilation) {
         var containingNamespace = compilation.GetSemanticModel(mutableClass.SyntaxTree).GetDeclaredSymbol(mutableClass)!.ContainingNamespace;
-
-        var namespaceName = containingNamespace.ToDisplayString();
-
-        var className = mutableClass.Identifier.Text;
-
-        var usings = mutableClass.SyntaxTree.GetCompilationUnitRoot().Usings.Select(u => u.ToString());
+        var namespaceName       = containingNamespace.ToDisplayString();
+        var className           = mutableClass.Identifier.Text;
+        var usings              = mutableClass.SyntaxTree.GetCompilationUnitRoot().Usings.Select(u => u.ToString());
 
         var properties = GetDefinitions(SyntaxKind.SetKeyword)
-            .Select(prop => $"    public {prop.Type} {prop.Identifier.Text} {{ get; }}")
+            .Select(
+                prop => {
+                    var xml = prop.GetLeadingTrivia().FirstOrDefault(x => x.IsKind(SyntaxKind.SingleLineDocumentationCommentTrivia)).GetStructure();
+                    return $"/// {xml}    public {prop.Type} {prop.Identifier.Text} {{ get; }}";
+                }
+            )
             .ToArray();
 
         var props = GetDefinitions(SyntaxKind.SetKeyword).ToArray();
 
         const string argName = "inner";
-        var mutableProperties = props
-            .Select(prop => $"        {prop.Identifier.Text} = {argName}.{prop.Identifier.Text};");
+
+        var mutableProperties = props.Select(prop => $"        {prop.Identifier.Text} = {argName}.{prop.Identifier.Text};");
 
         var constructor = $$"""
                                 public ReadOnly{{className}}({{className}} {{argName}}) {
@@ -85,7 +87,8 @@ IEnumerable<PropertyDeclarationSyntax> GetDefinitions(SyntaxKind kind)
                 .OfType<PropertyDeclarationSyntax>()
                 .Where(
                     prop =>
-                        prop.AccessorList!.Accessors.Any(accessor => accessor.Keyword.IsKind(kind)) && prop.AttributeLists.All(list => list.Attributes.All(attr => attr.Name.ToString() != "Exclude"))
+                        prop.AccessorList!.Accessors.Any(accessor => accessor.Keyword.IsKind(kind)) &&
+                        prop.AttributeLists.All(list => list.Attributes.All(attr => attr.Name.ToString() != "Exclude"))
                 );
     }
-}
+}
\ No newline at end of file
diff --git a/src/RestSharp/Authenticators/JwtAuthenticator.cs b/src/RestSharp/Authenticators/JwtAuthenticator.cs
index 1b90bdd60..cadbd2a64 100644
--- a/src/RestSharp/Authenticators/JwtAuthenticator.cs
+++ b/src/RestSharp/Authenticators/JwtAuthenticator.cs
@@ -12,7 +12,7 @@
 //   See the License for the specific language governing permissions and
 //   limitations under the License. 
 
-namespace RestSharp.Authenticators; 
+namespace RestSharp.Authenticators;
 
 /// <summary>
 /// JSON WEB TOKEN (JWT) Authenticator class.
@@ -26,7 +26,8 @@ public class JwtAuthenticator(string accessToken) : AuthenticatorBase(GetToken(a
     [PublicAPI]
     public void SetBearerToken(string accessToken) => Token = GetToken(accessToken);
 
-    static string GetToken(string accessToken) => Ensure.NotEmpty(accessToken, nameof(accessToken)).StartsWith("Bearer ") ? accessToken : $"Bearer {accessToken}";
+    static string GetToken(string accessToken)
+        => Ensure.NotEmptyString(accessToken, nameof(accessToken)).StartsWith("Bearer ") ? accessToken : $"Bearer {accessToken}";
 
     protected override ValueTask<Parameter> GetAuthenticationParameter(string accessToken)
         => new(new HeaderParameter(KnownHeaders.Authorization, accessToken));
diff --git a/src/RestSharp/Authenticators/OAuth/OAuthWorkflow.cs b/src/RestSharp/Authenticators/OAuth/OAuthWorkflow.cs
index 9b7a41837..894bb24f9 100644
--- a/src/RestSharp/Authenticators/OAuth/OAuthWorkflow.cs
+++ b/src/RestSharp/Authenticators/OAuth/OAuthWorkflow.cs
@@ -48,7 +48,7 @@ sealed class OAuthWorkflow {
     /// <param name="parameters">Any existing, non-OAuth query parameters desired in the request</param>
     /// <returns></returns>
     public OAuthParameters BuildRequestTokenSignature(string method, WebPairCollection parameters) {
-        Ensure.NotEmpty(ConsumerKey, nameof(ConsumerKey));
+        Ensure.NotEmptyString(ConsumerKey, nameof(ConsumerKey));
 
         var allParameters = new WebPairCollection();
         allParameters.AddRange(parameters);
@@ -76,8 +76,8 @@ public OAuthParameters BuildRequestTokenSignature(string method, WebPairCollecti
     /// <param name="method">The HTTP method for the intended request</param>
     /// <param name="parameters">Any existing, non-OAuth query parameters desired in the request</param>
     public OAuthParameters BuildAccessTokenSignature(string method, WebPairCollection parameters) {
-        Ensure.NotEmpty(ConsumerKey, nameof(ConsumerKey));
-        Ensure.NotEmpty(Token, nameof(Token));
+        Ensure.NotEmptyString(ConsumerKey, nameof(ConsumerKey));
+        Ensure.NotEmptyString(Token, nameof(Token));
 
         var allParameters = new WebPairCollection();
         allParameters.AddRange(parameters);
@@ -105,8 +105,8 @@ public OAuthParameters BuildAccessTokenSignature(string method, WebPairCollectio
     /// <param name="method">The HTTP method for the intended request</param>
     /// <param name="parameters">Any existing, non-OAuth query parameters desired in the request</param>
     public OAuthParameters BuildClientAuthAccessTokenSignature(string method, WebPairCollection parameters) {
-        Ensure.NotEmpty(ConsumerKey, nameof(ConsumerKey));
-        Ensure.NotEmpty(ClientUsername, nameof(ClientUsername));
+        Ensure.NotEmptyString(ConsumerKey, nameof(ConsumerKey));
+        Ensure.NotEmptyString(ClientUsername, nameof(ClientUsername));
 
         var allParameters = new WebPairCollection();
         allParameters.AddRange(parameters);
@@ -127,7 +127,7 @@ public OAuthParameters BuildClientAuthAccessTokenSignature(string method, WebPai
     }
 
     public OAuthParameters BuildProtectedResourceSignature(string method, WebPairCollection parameters) {
-        Ensure.NotEmpty(ConsumerKey, nameof(ConsumerKey));
+        Ensure.NotEmptyString(ConsumerKey, nameof(ConsumerKey));
 
         var allParameters = new WebPairCollection();
         allParameters.AddRange(parameters);
diff --git a/src/RestSharp/Ensure.cs b/src/RestSharp/Ensure.cs
index 5d985c860..dbdb56f9d 100644
--- a/src/RestSharp/Ensure.cs
+++ b/src/RestSharp/Ensure.cs
@@ -17,11 +17,10 @@ namespace RestSharp;
 static class Ensure {
     public static T NotNull<T>(T? value, [InvokerParameterName] string name) => value ?? throw new ArgumentNullException(name);
 
-    public static string NotEmpty(string? value, [InvokerParameterName] string name)
-        => string.IsNullOrWhiteSpace(value) ? throw new ArgumentNullException(name) : value!;
-
     public static string NotEmptyString(object? value, [InvokerParameterName] string name) {
         var s = value as string ?? value?.ToString();
-        return string.IsNullOrWhiteSpace(s) ? throw new ArgumentNullException(name) : s!;
+        if (s == null) throw new ArgumentNullException(name);
+
+        return string.IsNullOrWhiteSpace(s) ? throw new ArgumentException("Parameter cannot be an empty string", name) : s;
     }
 }
\ No newline at end of file
diff --git a/src/RestSharp/Options/RestClientOptions.cs b/src/RestSharp/Options/RestClientOptions.cs
index 5e924d09f..74fde4ef0 100644
--- a/src/RestSharp/Options/RestClientOptions.cs
+++ b/src/RestSharp/Options/RestClientOptions.cs
@@ -220,6 +220,7 @@ public int MaxTimeout {
 
     /// <summary>
     /// Set to true to allow multiple default parameters with the same name. Default is false.
+    /// This setting doesn't apply to headers as multiple header values for the same key is allowed.
     /// </summary>
     public bool AllowMultipleDefaultParametersWithSameName { get; set; }
 
diff --git a/src/RestSharp/Parameters/DefaultParameters.cs b/src/RestSharp/Parameters/DefaultParameters.cs
index 8ba19ccdc..a65b41846 100644
--- a/src/RestSharp/Parameters/DefaultParameters.cs
+++ b/src/RestSharp/Parameters/DefaultParameters.cs
@@ -28,12 +28,11 @@ public sealed class DefaultParameters(ReadOnlyRestClientOptions options) : Param
     [MethodImpl(MethodImplOptions.Synchronized)]
     public DefaultParameters AddParameter(Parameter parameter) {
         if (parameter.Type == ParameterType.RequestBody)
-            throw new NotSupportedException(
-                "Cannot set request body using default parameters. Use Request.AddBody() instead."
-            );
+            throw new NotSupportedException("Cannot set request body using default parameters. Use Request.AddBody() instead.");
 
         if (!options.AllowMultipleDefaultParametersWithSameName &&
-            !MultiParameterTypes.Contains(parameter.Type)        &&
+            parameter.Type != ParameterType.HttpHeader &&
+            !MultiParameterTypes.Contains(parameter.Type) &&
             this.Any(x => x.Name == parameter.Name)) {
             throw new ArgumentException("A default parameters with the same name has already been added", nameof(parameter));
         }
@@ -70,4 +69,4 @@ public DefaultParameters ReplaceParameter(Parameter parameter)
                 .AddParameter(parameter);
 
     static readonly ParameterType[] MultiParameterTypes = [ParameterType.QueryString, ParameterType.GetOrPost];
-}
+}
\ No newline at end of file
diff --git a/src/RestSharp/Parameters/HeaderParameter.cs b/src/RestSharp/Parameters/HeaderParameter.cs
index 55830a63f..1607eaeda 100644
--- a/src/RestSharp/Parameters/HeaderParameter.cs
+++ b/src/RestSharp/Parameters/HeaderParameter.cs
@@ -21,5 +21,14 @@ public record HeaderParameter : Parameter {
     /// </summary>
     /// <param name="name">Parameter name</param>
     /// <param name="value">Parameter value</param>
-    public HeaderParameter(string? name, string? value) : base(name, value, ParameterType.HttpHeader, false) { }
+    public HeaderParameter(string name, string value)
+        : base(
+            Ensure.NotEmptyString(name, nameof(name)),
+            Ensure.NotNull(value, nameof(value)),
+            ParameterType.HttpHeader,
+            false
+        ) { }
+
+    public new string Name  => base.Name!;
+    public new string Value => (string)base.Value!;
 }
\ No newline at end of file
diff --git a/src/RestSharp/Parameters/Parameter.cs b/src/RestSharp/Parameters/Parameter.cs
index 93de83479..903b33402 100644
--- a/src/RestSharp/Parameters/Parameter.cs
+++ b/src/RestSharp/Parameters/Parameter.cs
@@ -12,32 +12,66 @@
 //   See the License for the specific language governing permissions and
 //   limitations under the License. 
 
+using System.Diagnostics;
+
 namespace RestSharp;
 
 /// <summary>
 /// Parameter container for REST requests
 /// </summary>
-public abstract record Parameter(string? Name, object? Value, ParameterType Type, bool Encode) {
+[DebuggerDisplay($"{{{nameof(DebuggerDisplay)}()}}")]
+public abstract record Parameter {
+    /// <summary>
+    /// Parameter container for REST requests
+    /// </summary>
+    protected Parameter(string? name, object? value, ParameterType type, bool encode) {
+        Name   = name;
+        Value  = value;
+        Type   = type;
+        Encode = encode;
+    }
+
     /// <summary>
     /// MIME content type of the parameter
     /// </summary>
     public ContentType ContentType { get; protected init; } = ContentType.Undefined;
+    public string?       Name   { get; }
+    public object?       Value  { get; }
+    public ParameterType Type   { get; }
+    public bool          Encode { get; }
 
     /// <summary>
     /// Return a human-readable representation of this parameter
     /// </summary>
     /// <returns>String</returns>
-    public sealed override string ToString() => Value == null ? $"{Name}" : $"{Name}={Value}";
+    public sealed override string ToString() => Value == null ? $"{Name}" : $"{Name}={ValueString}";
+
+    protected virtual string ValueString => Value?.ToString() ?? "null";
 
     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.HttpHeader  => new HeaderParameter(name!, value?.ToString()!),
             ParameterType.QueryString => new QueryParameter(Ensure.NotEmptyString(name, nameof(name)), value?.ToString(), encode),
             _                         => throw new ArgumentOutOfRangeException(nameof(type), type, null)
         };
+
+    [PublicAPI]
+    public void Deconstruct(out string? name, out object? value, out ParameterType type, out bool encode) {
+        name   = Name;
+        value  = Value;
+        type   = Type;
+        encode = Encode;
+    }
+
+    /// <summary>
+    /// Assists with debugging by displaying in the debugger output
+    /// </summary>
+    /// <returns></returns>
+    [UsedImplicitly]
+    protected string DebuggerDisplay() => $"{GetType().Name.Replace("Parameter", "")} {ToString()}";
 }
 
 public record NamedParameter : Parameter {
diff --git a/src/RestSharp/Parameters/ParametersCollection.cs b/src/RestSharp/Parameters/ParametersCollection.cs
index 2d675820e..299f2d498 100644
--- a/src/RestSharp/Parameters/ParametersCollection.cs
+++ b/src/RestSharp/Parameters/ParametersCollection.cs
@@ -17,25 +17,27 @@
 
 namespace RestSharp;
 
-public abstract class ParametersCollection : IReadOnlyCollection<Parameter> {
-    protected readonly List<Parameter> Parameters = [];
+public abstract class ParametersCollection<T> : IReadOnlyCollection<T> where T : Parameter {
+    protected readonly List<T> Parameters = [];
 
     // public ParametersCollection(IEnumerable<Parameter> parameters) => _parameters.AddRange(parameters);
 
-    static readonly Func<Parameter, string?, bool> SearchPredicate = (p, name)
+    static readonly Func<T, string?, bool> SearchPredicate = (p, name)
         => p.Name != null && p.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase);
 
-    public bool Exists(Parameter parameter) => Parameters.Any(p => SearchPredicate(p, parameter.Name) && p.Type == parameter.Type);
+    public bool Exists(T parameter) => Parameters.Any(p => SearchPredicate(p, parameter.Name) && p.Type == parameter.Type);
 
-    public Parameter? TryFind(string parameterName) => Parameters.FirstOrDefault(x => SearchPredicate(x, parameterName));
+    public T? TryFind(string parameterName) => Parameters.FirstOrDefault(x => SearchPredicate(x, parameterName));
 
-    public IEnumerable<Parameter> GetParameters(ParameterType parameterType) => Parameters.Where(x => x.Type == parameterType);
-
-    public IEnumerable<T> GetParameters<T>() where T : class => Parameters.OfType<T>();
+    public IEnumerable<TParameter> GetParameters<TParameter>() where TParameter : class, T => Parameters.OfType<TParameter>();
 
-    public IEnumerator<Parameter> GetEnumerator() => Parameters.GetEnumerator();
+    public IEnumerator<T> GetEnumerator() => Parameters.GetEnumerator();
 
     IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
 
     public int Count => Parameters.Count;
+}
+
+public abstract class ParametersCollection : ParametersCollection<Parameter> {
+    public IEnumerable<Parameter> GetParameters(ParameterType parameterType) => Parameters.Where(x => x.Type == parameterType);
 }
\ No newline at end of file
diff --git a/src/RestSharp/Parameters/RequestParameters.cs b/src/RestSharp/Parameters/RequestParameters.cs
index 96d5c8a98..2d673f9bd 100644
--- a/src/RestSharp/Parameters/RequestParameters.cs
+++ b/src/RestSharp/Parameters/RequestParameters.cs
@@ -42,7 +42,7 @@ public ParametersCollection AddParameters(IEnumerable<Parameter> parameters) {
     }
 
     /// <summary>
-    /// Add parameters from another parameters collection
+    /// Add parameters from another parameter collection
     /// </summary>
     /// <param name="parameters"></param>
     /// <returns></returns>
diff --git a/src/RestSharp/Parameters/UrlSegmentParameter.cs b/src/RestSharp/Parameters/UrlSegmentParameter.cs
index 82d6dc2fd..b9da7752a 100644
--- a/src/RestSharp/Parameters/UrlSegmentParameter.cs
+++ b/src/RestSharp/Parameters/UrlSegmentParameter.cs
@@ -29,7 +29,7 @@ public partial record UrlSegmentParameter : NamedParameter {
     public UrlSegmentParameter(string name, string value, bool encode = true)
         : base(
             name,
-            RegexPattern.Replace(Ensure.NotEmpty(value, nameof(value)), "/"),
+            RegexPattern.Replace(Ensure.NotEmptyString(value, nameof(value)), "/"),
             ParameterType.UrlSegment,
             encode
         ) { }
diff --git a/src/RestSharp/Request/HttpRequestMessageExtensions.cs b/src/RestSharp/Request/HttpRequestMessageExtensions.cs
index 00601d2d5..923f9364f 100644
--- a/src/RestSharp/Request/HttpRequestMessageExtensions.cs
+++ b/src/RestSharp/Request/HttpRequestMessageExtensions.cs
@@ -20,16 +20,16 @@ namespace RestSharp;
 
 static class HttpRequestMessageExtensions {
     public static void AddHeaders(this HttpRequestMessage message, RequestHeaders headers) {
-        var headerParameters = headers.Parameters.Where(x => !KnownHeaders.IsContentHeader(x.Name!));
+        var headerParameters = headers.Where(x => !KnownHeaders.IsContentHeader(x.Name));
 
-        headerParameters.ForEach(x => AddHeader(x, message.Headers));
+        headerParameters.GroupBy(x => x.Name).ForEach(x => AddHeader(x, message.Headers));
         return;
 
-        void AddHeader(Parameter parameter, HttpHeaders httpHeaders) {
-            var parameterStringValue = parameter.Value!.ToString();
+        void AddHeader(IGrouping<string, HeaderParameter> group, HttpHeaders httpHeaders) {
+            var parameterStringValues = group.Select(x => x.Value);
 
-            httpHeaders.Remove(parameter.Name!);
-            httpHeaders.TryAddWithoutValidation(parameter.Name!, parameterStringValue);
+            httpHeaders.Remove(group.Key);
+            httpHeaders.TryAddWithoutValidation(group.Key, parameterStringValues);
         }
     }
 }
\ No newline at end of file
diff --git a/src/RestSharp/Request/RequestHeaders.cs b/src/RestSharp/Request/RequestHeaders.cs
index 9458cad47..10677d6e3 100644
--- a/src/RestSharp/Request/RequestHeaders.cs
+++ b/src/RestSharp/Request/RequestHeaders.cs
@@ -19,19 +19,17 @@
 
 namespace RestSharp;
 
-class RequestHeaders {
-    public RequestParameters Parameters { get; } = new();
-
+class RequestHeaders : ParametersCollection<HeaderParameter> {
     public RequestHeaders AddHeaders(ParametersCollection parameters) {
-        Parameters.AddParameters(parameters.GetParameters<HeaderParameter>());
+        Parameters.AddRange(parameters.GetParameters<HeaderParameter>());
         return this;
     }
 
     // Add Accept header based on registered deserializers if the caller has set none.
     public RequestHeaders AddAcceptHeader(string[] acceptedContentTypes) {
-        if (Parameters.TryFind(KnownHeaders.Accept) == null) {
+        if (TryFind(KnownHeaders.Accept) == null) {
             var accepts = acceptedContentTypes.JoinToString(", ");
-            Parameters.AddParameter(new HeaderParameter(KnownHeaders.Accept, accepts));
+            Parameters.Add(new(KnownHeaders.Accept, accepts));
         }
 
         return this;
@@ -46,13 +44,13 @@ public RequestHeaders AddCookieHeaders(Uri uri, CookieContainer? cookieContainer
         if (string.IsNullOrWhiteSpace(cookies)) return this;
 
         var newCookies = SplitHeader(cookies);
-        var existing   = Parameters.GetParameters<HeaderParameter>().FirstOrDefault(x => x.Name == KnownHeaders.Cookie);
+        var existing   = GetParameters<HeaderParameter>().FirstOrDefault(x => x.Name == KnownHeaders.Cookie);
 
         if (existing?.Value != null) {
-            newCookies = newCookies.Union(SplitHeader(existing.Value.ToString()!));
+            newCookies = newCookies.Union(SplitHeader(existing.Value!));
         }
 
-        Parameters.AddParameter(new HeaderParameter(KnownHeaders.Cookie, string.Join("; ", newCookies)));
+        Parameters.Add(new(KnownHeaders.Cookie, string.Join("; ", newCookies)));
 
         return this;
 
diff --git a/src/RestSharp/Request/RestRequest.cs b/src/RestSharp/Request/RestRequest.cs
index e0e696517..eaad89972 100644
--- a/src/RestSharp/Request/RestRequest.cs
+++ b/src/RestSharp/Request/RestRequest.cs
@@ -16,8 +16,8 @@
 using RestSharp.Authenticators;
 using RestSharp.Extensions;
 using RestSharp.Interceptors;
-// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global
 
+// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global
 // ReSharper disable UnusedAutoPropertyAccessor.Global
 
 namespace RestSharp;
@@ -190,7 +190,7 @@ public RestRequest(Uri resource, Method method = Method.Get)
     internal void IncreaseNumberOfAttempts() => Attempts++;
 
     /// <summary>
-    /// How many attempts were made to send this Request
+    /// The number of attempts that were made to send this request
     /// </summary>
     /// <remarks>
     /// This number is incremented each time the RestClient sends the request.
diff --git a/src/RestSharp/Request/RestRequestExtensions.Headers.cs b/src/RestSharp/Request/RestRequestExtensions.Headers.cs
index dfd2d8b44..8091a1a50 100644
--- a/src/RestSharp/Request/RestRequestExtensions.Headers.cs
+++ b/src/RestSharp/Request/RestRequestExtensions.Headers.cs
@@ -17,6 +17,21 @@
 namespace RestSharp;
 
 public static partial class RestRequestExtensions {
+    /// <summary>
+    /// Adds a header to the request. RestSharp will try to separate request and content headers when calling the resource.
+    /// </summary>
+    /// <param name="request">Request instance</param>
+    /// <param name="name">Header name</param>
+    /// <param name="values">Header values</param>
+    /// <returns></returns>
+    public static RestRequest AddHeader(this RestRequest request, string name, string[] values) {
+        foreach (var value in values) {
+            AddHeader(request, name, value);
+        }
+
+        return request;
+    }
+
     /// <summary>
     /// Adds a header to the request. RestSharp will try to separate request and content headers when calling the resource.
     /// </summary>
@@ -41,7 +56,7 @@ public static RestRequest AddHeader<T>(this RestRequest request, string name, T
 
     /// <summary>
     /// Adds or updates the request header. RestSharp will try to separate request and content headers when calling the resource.
-    /// Existing header with the same name will be replaced.
+    /// The existing header with the same name will be replaced.
     /// </summary>
     /// <param name="request">Request instance</param>
     /// <param name="name">Header name</param>
@@ -54,7 +69,7 @@ public static RestRequest AddOrUpdateHeader(this RestRequest request, string nam
 
     /// <summary>
     /// Adds or updates the request header. RestSharp will try to separate request and content headers when calling the resource.
-    /// Existing header with the same name will be replaced.
+    /// The existing header with the same name will be replaced.
     /// </summary>
     /// <param name="request">Request instance</param>
     /// <param name="name">Header name</param>
diff --git a/src/RestSharp/Request/UriExtensions.cs b/src/RestSharp/Request/UriExtensions.cs
index f6e414f42..5aa9bd3cd 100644
--- a/src/RestSharp/Request/UriExtensions.cs
+++ b/src/RestSharp/Request/UriExtensions.cs
@@ -40,7 +40,7 @@ public static Uri AddQueryString(this Uri uri, string? query) {
         var absoluteUri = uri.AbsoluteUri;
         var separator   = absoluteUri.Contains('?') ? "&" : "?";
 
-        return new Uri($"{absoluteUri}{separator}{query}");
+        return new($"{absoluteUri}{separator}{query}");
     }
 
     public static UrlSegmentParamsValues GetUrlSegmentParamsValues(
diff --git a/src/RestSharp/Response/RestResponseBase.cs b/src/RestSharp/Response/RestResponseBase.cs
index 1ba9ec509..383895fb4 100644
--- a/src/RestSharp/Response/RestResponseBase.cs
+++ b/src/RestSharp/Response/RestResponseBase.cs
@@ -21,7 +21,7 @@ namespace RestSharp;
 /// <summary>
 /// Base class for common properties shared by RestResponse and RestResponse[[T]]
 /// </summary>
-[DebuggerDisplay("{" + nameof(DebuggerDisplay) + "()}")]
+[DebuggerDisplay($"{{{nameof(DebuggerDisplay)}()}}")]
 public abstract class RestResponseBase {
     /// <summary>
     /// Default constructor
diff --git a/src/RestSharp/RestClient.cs b/src/RestSharp/RestClient.cs
index 0b3f58380..95fd432cb 100644
--- a/src/RestSharp/RestClient.cs
+++ b/src/RestSharp/RestClient.cs
@@ -74,8 +74,8 @@ public RestClient(
         }
 
         ConfigureSerializers(configureSerialization);
-        Options           = new ReadOnlyRestClientOptions(options);
-        DefaultParameters = new DefaultParameters(Options);
+        Options           = new(options);
+        DefaultParameters = new(Options);
 
         if (useClientFactory) {
             _disposeHttpClient = false;
@@ -121,8 +121,7 @@ public RestClient(
         ConfigureHeaders?       configureDefaultHeaders = null,
         ConfigureSerialization? configureSerialization  = null,
         bool                    useClientFactory        = false
-    )
-        : this(ConfigureOptions(new RestClientOptions(), configureRestClient), configureDefaultHeaders, configureSerialization, useClientFactory) { }
+    ) : this(ConfigureOptions(new(), configureRestClient), configureDefaultHeaders, configureSerialization, useClientFactory) { }
 
     /// <inheritdoc />
     /// <summary>
@@ -141,7 +140,7 @@ public RestClient(
         bool                    useClientFactory        = false
     )
         : this(
-            ConfigureOptions(new RestClientOptions { BaseUrl = baseUrl }, configureRestClient),
+            ConfigureOptions(new() { BaseUrl = baseUrl }, configureRestClient),
             configureDefaultHeaders,
             configureSerialization,
             useClientFactory
@@ -185,8 +184,8 @@ public RestClient(
         }
 
         var opt = options ?? new RestClientOptions();
-        Options           = new ReadOnlyRestClientOptions(opt);
-        DefaultParameters = new DefaultParameters(Options);
+        Options           = new(opt);
+        DefaultParameters = new(Options);
 
         if (options != null) {
             ConfigureHttpClient(httpClient, options);
@@ -207,7 +206,7 @@ public RestClient(
         ConfigureRestClient?    configureRestClient    = null,
         ConfigureSerialization? configureSerialization = null
     )
-        : this(httpClient, ConfigureOptions(new RestClientOptions(), configureRestClient), disposeHttpClient, configureSerialization) { }
+        : this(httpClient, ConfigureOptions(new(), configureRestClient), disposeHttpClient, configureSerialization) { }
 
     /// <summary>
     /// Creates a new instance of RestSharp using the message handler provided. By default, HttpClient disposes the provided handler
@@ -270,7 +269,7 @@ void ConfigureSerializers(ConfigureSerialization? configureSerialization) {
         var serializerConfig = new SerializerConfig();
         serializerConfig.UseDefaultSerializers();
         configureSerialization?.Invoke(serializerConfig);
-        Serializers          = new RestSerializers(serializerConfig);
+        Serializers          = new(serializerConfig);
         AcceptedContentTypes = Serializers.GetAcceptedContentTypes();
     }
 
diff --git a/test/RestSharp.Tests.Integrated/HttpHeadersTests.cs b/test/RestSharp.Tests.Integrated/HttpHeadersTests.cs
index 6d5618283..559c6435a 100644
--- a/test/RestSharp.Tests.Integrated/HttpHeadersTests.cs
+++ b/test/RestSharp.Tests.Integrated/HttpHeadersTests.cs
@@ -50,8 +50,7 @@ public async Task Should_use_both_default_and_request_headers() {
 
         _client.AddDefaultHeader(defaultHeader.Name, defaultHeader.Value);
 
-        var request = new RestRequest("/headers")
-            .AddHeader(requestHeader.Name, requestHeader.Value);
+        var request = new RestRequest("/headers").AddHeader(requestHeader.Name, requestHeader.Value);
 
         var response = await _client.ExecuteAsync<TestServerResponse[]>(request);
         CheckHeader(response, defaultHeader);
diff --git a/test/RestSharp.Tests.Serializers.Xml/SampleClasses/twitter.cs b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/twitter.cs
index 3a10375d9..1afe25fc3 100644
--- a/test/RestSharp.Tests.Serializers.Xml/SampleClasses/twitter.cs
+++ b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/twitter.cs
@@ -1,14 +1,15 @@
 using RestSharp.Serializers;
+
 // ReSharper disable InconsistentNaming
 // ReSharper disable UnusedMember.Global
 // ReSharper disable ClassNeverInstantiated.Global
+#pragma warning disable CS8981 // The type name only contains lower-cased ascii characters. Such names may become reserved for the language.
 #pragma warning disable CS8981
 
-namespace RestSharp.Tests.Serializers.Xml.SampleClasses; 
+namespace RestSharp.Tests.Serializers.Xml.SampleClasses;
 
 #pragma warning disable CS8981
 public class status {
-#pragma warning restore CS8981
     public bool truncated { get; set; }
 
     public string created_at { get; set; }
@@ -113,4 +114,5 @@ public class complexStatus {
     public long id { get; set; }
 
     public string text { get; set; }
-}
\ No newline at end of file
+}
+#pragma warning restore CS8981
\ No newline at end of file
diff --git a/test/RestSharp.Tests/RequestHeaderTests.cs b/test/RestSharp.Tests/RequestHeaderTests.cs
index 8752dca10..6fa6c8215 100644
--- a/test/RestSharp.Tests/RequestHeaderTests.cs
+++ b/test/RestSharp.Tests/RequestHeaderTests.cs
@@ -21,7 +21,7 @@ public void AddHeaders_SameCaseDuplicatesExist_ThrowsException() {
     [Fact]
     public void AddHeaders_DifferentCaseDuplicatesExist_ThrowsException() {
         var headers = _headers;
-        headers.Add(new KeyValuePair<string, string>(KnownHeaders.Accept, ContentType.Json));
+        headers.Add(new(KnownHeaders.Accept, ContentType.Json));
 
         var request = new RestRequest();
 
@@ -63,7 +63,6 @@ public void AddOrUpdateHeader_ShouldUpdateExistingHeader_WhenHeaderExist() {
         request.AddOrUpdateHeader(KnownHeaders.Accept, ContentType.Json);
 
         // Assert
-        var headers = GetHeaders(request);
         GetHeader(request, KnownHeaders.Accept).Should().Be(ContentType.Json);
     }
 
@@ -121,10 +120,8 @@ public void AddOrUpdateHeaders_ShouldUpdateHeaders_WhenAllExists() {
 
         // Assert
         var requestHeaders = GetHeaders(request);
-        HeaderParameter[] expected = [
-            new HeaderParameter(KnownHeaders.Accept, ContentType.Xml),
-            new HeaderParameter(KnownHeaders.KeepAlive, "400")
-        ];
+
+        HeaderParameter[] expected = [new(KnownHeaders.Accept, ContentType.Xml), new(KnownHeaders.KeepAlive, "400")];
         requestHeaders.Should().BeEquivalentTo(expected);
     }
 
@@ -151,13 +148,33 @@ public void AddOrUpdateHeaders_ShouldAddAndUpdateHeaders_WhenSomeExists() {
         var requestHeaders = GetHeaders(request);
 
         HeaderParameter[] expected = [
-            new HeaderParameter(KnownHeaders.Accept, ContentType.Xml),
-            new HeaderParameter(KnownHeaders.AcceptLanguage, "en-us,en;q=0.5"),
-            new HeaderParameter(KnownHeaders.KeepAlive, "300")
+            new(KnownHeaders.Accept, ContentType.Xml),
+            new(KnownHeaders.AcceptLanguage, "en-us,en;q=0.5"),
+            new(KnownHeaders.KeepAlive, "300")
         ];
         requestHeaders.Should().BeEquivalentTo(expected);
     }
 
+    [Fact]
+    public void Should_not_allow_null_header_value() {
+        string value   = null;
+        var    request = new RestRequest();
+        // ReSharper disable once AssignNullToNotNullAttribute
+        Assert.Throws<ArgumentNullException>("value", () => request.AddHeader("name", value));
+    }
+
+    [Fact]
+    public void Should_not_allow_null_header_name() {
+        var request = new RestRequest();
+        Assert.Throws<ArgumentNullException>("name", () => request.AddHeader(null!, "value"));
+    }
+
+    [Fact]
+    public void Should_not_allow_empty_header_name() {
+        var request = new RestRequest();
+        Assert.Throws<ArgumentException>("name", () => request.AddHeader("", "value"));
+    }
+
     static Parameter[] GetHeaders(RestRequest request) => request.Parameters.Where(x => x.Type == ParameterType.HttpHeader).ToArray();
 
     static string GetHeader(RestRequest request, string name) => request.Parameters.FirstOrDefault(x => x.Name == name)?.Value?.ToString();