Skip to content

Nullable requesthandler#131

Merged
twogood merged 10 commits intomainfrom
nullable-requesthandler
Oct 28, 2025
Merged

Nullable requesthandler#131
twogood merged 10 commits intomainfrom
nullable-requesthandler

Conversation

@twogood
Copy link
Owner

@twogood twogood commented Oct 27, 2025

Summary by CodeRabbit

  • Bug Fixes

    • Unified error handling: failures now surface as a single client exception type and expose HTTP status codes.
  • Refactor

    • Major overhaul of the request/response pipeline with improved parameter binding, multipart/form-data and per-part serialization, richer logging, and stricter content-type behavior (a default content type is required; setting Content-Type via generic headers is disallowed).
  • Tests

    • Domain-exception tests updated to assert status-code-based behavior, reflect the unified exception type, and include a renamed test.

@twogood twogood self-assigned this Oct 27, 2025
@coderabbitai
Copy link

coderabbitai bot commented Oct 27, 2025

Walkthrough

This PR adds an internal RequestHandler implementing a request/response pipeline, makes RestClientContext.DefaultContentType non-nullable, updates RestClientBuilder to enforce/default the content type and forbid setting Content-Type via generic header methods, and updates domain-related tests to expect RestClientException and remove MyDomainErrorEnum parameters (one test renamed).

Changes

Cohort / File(s) Summary
Domain exception test updates
Activout.RestClient.Newtonsoft.Json.Test/DomainExceptions/DefaultDomainExceptionErrorObjectTests.cs, Activout.RestClient.Newtonsoft.Json.Test/DomainExceptions/DomainExceptionErrorObjectTests.cs
Removed MyDomainErrorEnum parameters from parametrized test methods, adjusted assertions to use StatusCode where applicable, changed expected exception types to RestClientException (inner-exception assertions adjusted or removed), and renamed TestNoDeserializerFoundTestErrorResponseNotCompatibleWithHtml.
Request handler implementation
Activout.RestClient/Implementation/RequestHandler.cs
Added internal RequestHandler class centralizing parameter binding (path/query/form/header/parts), template expansion, serializer selection, multipart/form-data and form-url-encoded content creation, request assembly and headers, send orchestration, response handling/deserialization, and domain-exception mapping/error handling.
Context & builder updates
Activout.RestClient/Implementation/RestClientContext.cs, Activout.RestClient/Implementation/RestClientBuilder.cs
RestClientContext.DefaultContentType changed from MediaType? to MediaType (non-nullable); RestClientBuilder now prevents setting Content-Type via generic header method and enforces/defaults DefaultContentType at build time (throws if unset).

Sequence Diagram(s)

sequenceDiagram
    participant Caller
    participant RequestHandler
    participant Serializer
    participant HttpClient
    participant Response as HTTP Response

    Caller->>RequestHandler: Send(methodInfo, args)
    RequestHandler->>RequestHandler: Bind params (path, query, form, header, parts)
    RequestHandler->>RequestHandler: Expand template & build URI
    RequestHandler->>RequestHandler: Build headers & query string

    alt Body required
        RequestHandler->>RequestHandler: Create HttpContent (Serialized / FormUrlEncoded / Multipart)
        opt Multipart
            RequestHandler->>Serializer: Serialize parts as needed
            RequestHandler->>RequestHandler: Compose multipart content
        end
        RequestHandler->>RequestHandler: Attach content
    end

    RequestHandler->>HttpClient: SendAsync(HttpRequestMessage)
    HttpClient->>Response: HTTP response

    alt 2xx Success
        Response-->>RequestHandler: Response message
        RequestHandler->>Serializer: Deserialize (if applicable)
        RequestHandler-->>Caller: Return typed result
    else 4xx/5xx Error
        Response-->>RequestHandler: Error response
        RequestHandler->>RequestHandler: Invoke domain-exception mapper or create RestClientException
        RequestHandler-->>Caller: Throw mapped/RestClientException
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Areas to focus during review:

  • RequestHandler.cs parameter binding, per-parameter converter lookup, array/dictionary handling, and missing-converter error paths.
  • Multipart/form-data part serialization and content-type determination.
  • Serializer selection/fallback and deserialization error wrapping.
  • Domain exception mapper wiring and construction of RestClientException with StatusCode and inner exceptions.
  • RestClientContext non-null DefaultContentType initialization and RestClientBuilder enforcement (including header-name rejection logic).

Possibly related PRs

Poem

🐇 I hopped through headers, parts, and route,
I bound each value, made templates compute,
I stitched multipart seams and picked a type,
I turned stray errors into tidy type,
A little rabbit nods — the pipeline's astute.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.33% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title Check ❓ Inconclusive The PR title "Nullable requesthandler" is vague and ambiguous, making it difficult to understand the actual changes from the title alone. The changeset introduces a new internal RequestHandler class with significant refactoring of request/response handling logic, modifies DefaultContentType from nullable to non-nullable in RestClientContext, and updates related tests. The title uses generic terminology that doesn't clearly convey these main objectives—it's unclear whether "Nullable requesthandler" refers to making RequestHandler nullable (it doesn't), making components within it nullable, or something else entirely. While the branch name suggests some thematic connection, the title itself fails to meaningfully summarize the primary changes. Consider revising the title to clearly describe the main objective, such as "Extract request handling logic into RequestHandler class" or "Refactor request/response handling with RequestHandler" to better convey the primary changes to reviewers scanning the PR history.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch nullable-requesthandler

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🧹 Nitpick comments (5)
Activout.RestClient.Newtonsoft.Json.Test/DomainExceptions/DefaultDomainExceptionErrorObjectTests.cs (1)

88-90: Good shift to StatusCode; consider asserting error detail as well.

Optional: also assert that ErrorResponse contains “No deserializer found for application/foobar” to tighten the expectation without coupling to specific exception types.
Example:

-Assert.Equal(httpStatusCode, exception.StatusCode);
+Assert.Equal(httpStatusCode, exception.StatusCode);
+Assert.Contains("No deserializer found for application/foobar", exception.Message);

Also applies to: 98-103

Activout.RestClient/Implementation/RequestHandler.cs (4)

242-243: Method naming guideline: avoid “Async” suffix.

Rename SendAsync to a non-Async name (e.g., SendRequest) and update the call site.
Apply:

-        var task = SendAsync(request, cancellationToken);
+        var task = SendRequest(request, cancellationToken);
-    private async Task<object?> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+    private async Task<object?> SendRequest(HttpRequestMessage request, CancellationToken cancellationToken)

Also applies to: 476-476


40-41: Adopt collection expressions for field initialization.

Use [] list syntax per guidelines.

-    private readonly List<KeyValuePair<string, object>> _requestHeaders = new List<KeyValuePair<string, object>>();
+    private readonly List<KeyValuePair<string, object>> _requestHeaders = [];

215-218: Query assembly is naive with existing query in template.

If BaseTemplate/Path already contains '?', this will produce an invalid URI. Consider a proper query builder or UriBuilder.
Example approach:

  • Use UriBuilder with NameValueCollection.
  • Or use WebUtilities.QueryHelpers to add parameters.

450-458: TODO: resolve part serializer upfront (perf).

Cache serializer per part content type to avoid repeated lookups in hot paths.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 378da2d and 9e8c473.

📒 Files selected for processing (4)
  • Activout.RestClient.Newtonsoft.Json.Test/DomainExceptions/DefaultDomainExceptionErrorObjectTests.cs (1 hunks)
  • Activout.RestClient.Newtonsoft.Json.Test/DomainExceptions/DomainExceptionErrorObjectTests.cs (1 hunks)
  • Activout.RestClient/Implementation/RequestHandler.cs (1 hunks)
  • Activout.RestClient/Implementation/RestClientContext.cs (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.cs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.cs: Use [] list syntax for collections
Use file-scoped namespaces
Use primary constructors wherever possible
Use records for data transfer objects (DTOs) and immutable data structures
Use var for local variable declarations when possible
Use expression-bodied members for simple methods and properties
Use pattern matching for type checks and deconstruction
Use using statements for resource management
Use async and await for asynchronous programming
Never include "Async" in method names
Only include comments that explain why something is done, not what is done

Files:

  • Activout.RestClient.Newtonsoft.Json.Test/DomainExceptions/DefaultDomainExceptionErrorObjectTests.cs
  • Activout.RestClient.Newtonsoft.Json.Test/DomainExceptions/DomainExceptionErrorObjectTests.cs
  • Activout.RestClient/Implementation/RequestHandler.cs
  • Activout.RestClient/Implementation/RestClientContext.cs
🧬 Code graph analysis (3)
Activout.RestClient.Newtonsoft.Json.Test/DomainExceptions/DefaultDomainExceptionErrorObjectTests.cs (2)
Activout.RestClient/Implementation/RequestHandler.cs (3)
  • Task (476-530)
  • Task (532-565)
  • Task (567-572)
Activout.RestClient.Newtonsoft.Json/NewtonsoftJsonDeserializer.cs (1)
  • Task (20-33)
Activout.RestClient/Implementation/RequestHandler.cs (8)
Activout.RestClient/Implementation/RestClient.cs (3)
  • RestClient (10-47)
  • RestClient (18-22)
  • IEnumerable (24-27)
Activout.RestClient/Helpers/ITaskConverterFactory.cs (1)
  • ITaskConverter (7-7)
Activout.RestClient/Helpers/Implementation/TaskConverter3Factory.cs (1)
  • ITaskConverter (9-14)
Activout.RestClient/Serialization/Implementation/SerializationManager.cs (3)
  • ISerializer (43-47)
  • SerializationManager (8-48)
  • SerializationManager (30-35)
Activout.RestClient/ParamConverter/Implementation/ParamConverterManager.cs (3)
  • IParamConverter (19-30)
  • ParamConverterManager (7-31)
  • ParamConverterManager (13-17)
Activout.RestClient/RestClientException.cs (4)
  • ToString (34-37)
  • RestClientException (7-38)
  • RestClientException (9-14)
  • RestClientException (16-22)
Activout.RestClient/Part.cs (2)
  • Part (4-9)
  • Part (11-18)
Activout.RestClient/Serialization/ISerializer.cs (1)
  • HttpContent (9-9)
Activout.RestClient/Implementation/RestClientContext.cs (1)
Activout.RestClient/MediaType.cs (2)
  • MediaType (8-11)
  • MediaType (15-22)
🪛 GitHub Check: devskim
Activout.RestClient/Implementation/RequestHandler.cs

[notice] 452-452: A "TODO" or similar was left in source code, possibly indicating incomplete functionality
Suspicious comment

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build (10.0.x)
  • GitHub Check: Analyze (csharp)
🔇 Additional comments (2)
Activout.RestClient.Newtonsoft.Json.Test/DomainExceptions/DomainExceptionErrorObjectTests.cs (1)

113-115: Switch to RestClientException aligns with new pipeline.

The expectation change is correct and matches RequestHandler’s error path.

Activout.RestClient.Newtonsoft.Json.Test/DomainExceptions/DefaultDomainExceptionErrorObjectTests.cs (1)

69-71: Incorrect review comment—StringDeserializer explicitly handles text/html.

The assertion on inner exception is stable. StringDeserializer's CanDeserialize method returns true for any text/* media type (via mediaType.Value.StartsWith("text/")), so text/html is matched. When deserialization fails—e.g., Activator.CreateInstance throws MissingMethodException due to missing constructor—RequestHandler catches and wraps it in RestClientException, making the InnerException non-null and the assertion correct. The code is working as intended.

Likely an incorrect or invalid review comment.

Most notably dispose HttpResponseMessage if possible
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (4)
Activout.RestClient/Implementation/RequestHandler.cs (4)

343-348: Path parameter null handling.

While line 313-316 continues when rawValue is null, the past review flagged line 345 for explicit null validation to provide a clearer error message when path parameters are unexpectedly null.

If you'd like to maintain the current flow, consider adding a comment explaining that null values are filtered earlier. Otherwise, apply the previously suggested explicit null check for clarity.


420-424: Default path substitution null handling.

Similar to the path parameter case, line 422 calls ToString(rawValue) where rawValue should be non-null due to the earlier continue statement. This was previously flagged for explicit null guarding.

The current flow relies on the null check at line 313-316. Consider adding a clarifying comment or explicit null check here as well for defensive programming.


491-500: Debug logging may expose sensitive data.

Lines 498-499 and 514-515 log request/response content at debug level. As previously noted, this can leak PII/PHI even at debug level. Consider the previously suggested opt-in configuration and sanitization approach if this is a concern for your application.

Also applies to: 509-516


327-334: Array casting fails for value-type arrays.

Line 329 casts rawValue to object?[]? which will throw InvalidCastException for value-type arrays (e.g., int[], byte[]). This was previously flagged.

Apply this diff to handle all array types:

                 if (_parameters[i].ParameterType.IsArray)
                 {
-                    var items = (object?[]?)rawValue;
-                    if (items != null)
+                    if (rawValue is IEnumerable enumerable and not string)
                     {
-                        parts.AddRange(items.SelectMany(item =>
-                            GetPartNameAndHttpContent(partAttribute, parameterName, item)));
+                        foreach (var item in enumerable)
+                        {
+                            parts.AddRange(GetPartNameAndHttpContent(partAttribute, parameterName, item));
+                        }
                     }
                 }
🧹 Nitpick comments (5)
Activout.RestClient/Implementation/RequestHandler.cs (4)

40-40: Consider using collection expression syntax per coding guidelines.

Line 40 uses new List<KeyValuePair<string, object>>() instead of the collection expression syntax [] specified in the coding guidelines.

Apply this diff to align with the coding guidelines:

-    private readonly List<KeyValuePair<string, object>> _requestHeaders = new List<KeyValuePair<string, object>>();
+    private readonly List<KeyValuePair<string, object>> _requestHeaders = [];

As per coding guidelines.


244-249: Synchronous Result access may deadlock in some contexts.

Line 248 uses task.Result which blocks the calling thread and can deadlock if called from a UI context or with certain synchronization contexts. Consider documenting this behavior or ensuring callers are aware of the synchronous blocking.


457-457: TODO comment indicates potential optimization.

The TODO suggests preparing part serializers in advance rather than per-part lookup. This could improve performance for multipart requests with multiple parts.

Would you like me to suggest an implementation that caches part serializers during initialization, or would you prefer to track this separately?


205-211: Use collection expression syntax per coding guidelines.

Lines 205-211 use traditional new List<>() and new Dictionary<>() syntax. The coding guidelines specify using [] collection expression syntax.

Apply this diff:

-        var headers = new List<KeyValuePair<string, object>>();
+        var headers = new List<KeyValuePair<string, object>>();
         headers.AddRange(_requestHeaders);
 
-        var routeParams = new Dictionary<string, object>();
-        var queryParams = new List<string>();
-        var formParams = new List<KeyValuePair<string, string>>();
-        var partParams = new List<Part<HttpContent>>();
+        var routeParams = new Dictionary<string, object>();
+        var queryParams = new List<string>();
+        var formParams = new List<KeyValuePair<string, string>>();
+        var partParams = new List<Part<HttpContent>>();

Note: For headers, you may need to keep the constructor form if AddRange requires a concrete type, or use: List<KeyValuePair<string, object>> headers = [.. _requestHeaders];

As per coding guidelines.

Activout.RestClient/Implementation/RestClientBuilder.cs (1)

166-166: Redundant null check - already guaranteed non-null.

Line 150 sets _defaultContentType ??= DefaultContentType, ensuring it's non-null. The null-coalescing with throw on line 166 can never execute and is redundant.

Apply this diff to remove the dead code:

-            DefaultContentType: _defaultContentType ?? throw new InvalidOperationException("DefaultContentType is not set."),
+            DefaultContentType: _defaultContentType,

Alternatively, add a null-forgiving operator if you want to be explicit: _defaultContentType!

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9e8c473 and 1ea457f.

📒 Files selected for processing (2)
  • Activout.RestClient/Implementation/RequestHandler.cs (1 hunks)
  • Activout.RestClient/Implementation/RestClientBuilder.cs (2 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.cs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.cs: Use [] list syntax for collections
Use file-scoped namespaces
Use primary constructors wherever possible
Use records for data transfer objects (DTOs) and immutable data structures
Use var for local variable declarations when possible
Use expression-bodied members for simple methods and properties
Use pattern matching for type checks and deconstruction
Use using statements for resource management
Use async and await for asynchronous programming
Never include "Async" in method names
Only include comments that explain why something is done, not what is done

Files:

  • Activout.RestClient/Implementation/RequestHandler.cs
  • Activout.RestClient/Implementation/RestClientBuilder.cs
🧬 Code graph analysis (1)
Activout.RestClient/Implementation/RequestHandler.cs (11)
Activout.RestClient/Implementation/RestClient.cs (3)
  • RestClient (10-47)
  • RestClient (18-22)
  • IEnumerable (24-27)
Activout.RestClient/Helpers/ITaskConverterFactory.cs (1)
  • ITaskConverter (7-7)
Activout.RestClient/Helpers/Implementation/TaskConverter3Factory.cs (1)
  • ITaskConverter (9-14)
Activout.RestClient/Serialization/Implementation/SerializationManager.cs (3)
  • ISerializer (43-47)
  • SerializationManager (8-48)
  • SerializationManager (30-35)
Activout.RestClient/ParamConverter/Implementation/ParamConverterManager.cs (3)
  • IParamConverter (19-30)
  • ParamConverterManager (7-31)
  • ParamConverterManager (13-17)
Activout.RestClient/Implementation/HeaderListExtensions.cs (1)
  • AddOrReplaceHeader (8-18)
Activout.RestClient/RestClientException.cs (4)
  • ToString (34-37)
  • RestClientException (7-38)
  • RestClientException (9-14)
  • RestClientException (16-22)
Activout.RestClient/Serialization/IDeserializer.cs (1)
  • Task (10-10)
Activout.RestClient/Part.cs (2)
  • Part (4-9)
  • Part (11-18)
Activout.RestClient/Serialization/ISerializer.cs (1)
  • HttpContent (9-9)
Activout.RestClient/Implementation/StringExtensions.cs (1)
  • SafeSubstring (5-20)
🔇 Additional comments (3)
Activout.RestClient/Implementation/RequestHandler.cs (2)

521-572: Resource management properly implemented.

The response disposal issue previously flagged has been well addressed. The shouldDisposeResponse flag (line 528) combined with the finally block (lines 565-571) ensures HttpResponseMessage is disposed except when returned to the caller or when returning raw HttpContent.


169-176: Guard against null return from ToString().

Line 173 calls entry.Value.ToString() without null handling. While rare, ToString() can return null, which would cause issues in the replacement.

Apply this diff to add null-coalescing:

 private string ExpandTemplate(Dictionary<string, object> routeParams)
 {
     var expanded = _template;
     foreach (var entry in routeParams)
-        expanded = expanded.Replace("{" + entry.Key + "}", entry.Value.ToString());
+        expanded = expanded.Replace("{" + entry.Key + "}", entry.Value.ToString() ?? string.Empty);
 
     return expanded;
 }

Likely an incorrect or invalid review comment.

Activout.RestClient/Implementation/RestClientBuilder.cs (1)

59-62: LGTM - Clear validation for Content-Type header.

Good validation to enforce using the ContentType() method instead of the generic Header() method. The case-insensitive check and clear error message provide excellent developer experience.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
Activout.RestClient.Newtonsoft.Json.Test/DomainExceptions/DomainExceptionErrorObjectTests.cs (1)

118-118: Remove or correct the MissingMethodException assertion.

This assertion is incorrect. When no deserializer is found for text/html, RequestHandler.Deserialize (line 576) throws RestClientException using the 3-parameter constructor which does NOT set an InnerException. The InnerException will be null, not MissingMethodException.

Apply this diff:

-            Assert.IsType<MissingMethodException>(exception.InnerException);
+            Assert.Null(exception.InnerException);

Alternatively, remove this assertion entirely if the inner exception is not relevant to this test case.

🧹 Nitpick comments (2)
Activout.RestClient.Newtonsoft.Json.Test/DomainExceptions/DomainExceptionErrorObjectTests.cs (2)

12-12: Convert to file-scoped namespace per coding guidelines.

The coding guidelines specify using file-scoped namespaces for all C# files.

As per coding guidelines.

Apply this diff:

-namespace Activout.RestClient.Newtonsoft.Json.Test.DomainExceptions
-{
+namespace Activout.RestClient.Newtonsoft.Json.Test.DomainExceptions;
+

Then remove the closing brace at the end of the file and reduce indentation for all members.


14-22: Consider using record type for this DTO.

MyDomainErrorObject is an immutable data structure that fits the pattern for a record type per coding guidelines.

As per coding guidelines.

Apply this diff to convert to a record with primary constructor:

-    internal class MyDomainErrorObject
-    {
-        public MyDomainErrorEnum ErrorEnum { get; }
-
-        public MyDomainErrorObject(MyDomainErrorEnum errorEnum)
-        {
-            ErrorEnum = errorEnum;
-        }
-    }
+    internal record MyDomainErrorObject(MyDomainErrorEnum ErrorEnum);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1ea457f and ddd26cb.

📒 Files selected for processing (1)
  • Activout.RestClient.Newtonsoft.Json.Test/DomainExceptions/DomainExceptionErrorObjectTests.cs (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.cs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.cs: Use [] list syntax for collections
Use file-scoped namespaces
Use primary constructors wherever possible
Use records for data transfer objects (DTOs) and immutable data structures
Use var for local variable declarations when possible
Use expression-bodied members for simple methods and properties
Use pattern matching for type checks and deconstruction
Use using statements for resource management
Use async and await for asynchronous programming
Never include "Async" in method names
Only include comments that explain why something is done, not what is done

Files:

  • Activout.RestClient.Newtonsoft.Json.Test/DomainExceptions/DomainExceptionErrorObjectTests.cs
🧬 Code graph analysis (1)
Activout.RestClient.Newtonsoft.Json.Test/DomainExceptions/DomainExceptionErrorObjectTests.cs (2)
Activout.RestClient/Implementation/RequestHandler.cs (4)
  • Task (481-485)
  • Task (487-519)
  • Task (521-572)
  • Task (574-595)
Activout.RestClient.Newtonsoft.Json/NewtonsoftJsonDeserializer.cs (1)
  • Task (20-33)
🔇 Additional comments (1)
Activout.RestClient.Newtonsoft.Json.Test/DomainExceptions/DomainExceptionErrorObjectTests.cs (1)

105-105: LGTM: Test correctly reflects the updated exception behavior.

The test method has been appropriately renamed and updated to expect RestClientException with the correct StatusCode assertion, aligning with the behavior when no deserializer is found for the HTML content type.

Also applies to: 113-113, 117-117

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
Activout.RestClient/Implementation/RequestHandler.cs (2)

115-118: Consider expression-bodied members for simple methods.

Per coding guidelines, these simple methods could use expression-bodied syntax for brevity.

As per coding guidelines.

Apply this diff:

-    private bool IsHttpMethodWithBody()
-    {
-        return _httpMethod == HttpMethod.Post || _httpMethod == HttpMethod.Put || _httpMethod == HttpMethod.Patch;
-    }
+    private bool IsHttpMethodWithBody() =>
+        _httpMethod == HttpMethod.Post || _httpMethod == HttpMethod.Put || _httpMethod == HttpMethod.Patch;

-    private static HttpMethod GetHttpMethod(HttpMethodAttribute attribute)
-    {
-        return attribute.HttpMethod;
-    }
+    private static HttpMethod GetHttpMethod(HttpMethodAttribute attribute) => attribute.HttpMethod;

-    private bool IsVoidTask()
-    {
-        return _returnType == typeof(Task);
-    }
+    private bool IsVoidTask() => _returnType == typeof(Task);

-    private bool IsGenericTask()
-    {
-        return _returnType.BaseType == typeof(Task) && _returnType.IsGenericType;
-    }
+    private bool IsGenericTask() => _returnType.BaseType == typeof(Task) && _returnType.IsGenericType;

Also applies to: 133-136, 150-153, 155-158


350-372: Remove redundant null check.

Line 364 checks rawValue != null, but line 313 already continues when rawValue == null, making this check redundant. Similar pattern appears in FormParamAttribute (line 387) and HeaderParamAttribute (line 410) handling.

Apply this diff:

                 else if (attribute is QueryParamAttribute queryParamAttribute)
                 {
                     if (rawValue is IDictionary dictionary)
                     {
                         foreach (DictionaryEntry entry in dictionary)
                         {
                             var key = entry.Key.ToString();
                             var value = ConvertValueToString(entry.Value, _parameters[i]);
                             if (key != null && value != null)
                             {
                                 queryParams.Add(Uri.EscapeDataString(key) + "=" + Uri.EscapeDataString(value));
                             }
                         }
                     }
-                    else if (rawValue != null)
+                    else
                     {
                         var stringValue = _paramConverters[i].ToString(rawValue);
                         queryParams.Add(Uri.EscapeDataString(queryParamAttribute.Name ?? parameterName) + "=" +
                                         Uri.EscapeDataString(stringValue));
                     }
 
                     handled = true;
                 }

Apply similar changes to FormParamAttribute (line 387) and HeaderParamAttribute (line 410) branches.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ddd26cb and 1ce31be.

📒 Files selected for processing (1)
  • Activout.RestClient/Implementation/RequestHandler.cs (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.cs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.cs: Use [] list syntax for collections
Use file-scoped namespaces
Use primary constructors wherever possible
Use records for data transfer objects (DTOs) and immutable data structures
Use var for local variable declarations when possible
Use expression-bodied members for simple methods and properties
Use pattern matching for type checks and deconstruction
Use using statements for resource management
Use async and await for asynchronous programming
Never include "Async" in method names
Only include comments that explain why something is done, not what is done

Files:

  • Activout.RestClient/Implementation/RequestHandler.cs
🧬 Code graph analysis (1)
Activout.RestClient/Implementation/RequestHandler.cs (14)
Activout.RestClient/Implementation/RestClient.cs (3)
  • RestClient (10-47)
  • RestClient (18-22)
  • IEnumerable (24-27)
Activout.RestClient/Helpers/ITaskConverterFactory.cs (1)
  • ITaskConverter (7-7)
Activout.RestClient/Helpers/Implementation/TaskConverter3Factory.cs (1)
  • ITaskConverter (9-14)
Activout.RestClient/Serialization/Implementation/SerializationManager.cs (3)
  • ISerializer (43-47)
  • SerializationManager (8-48)
  • SerializationManager (30-35)
Activout.RestClient/ParamConverter/Implementation/ParamConverterManager.cs (3)
  • IParamConverter (19-30)
  • ParamConverterManager (7-31)
  • ParamConverterManager (13-17)
Activout.RestClient/Implementation/HeaderListExtensions.cs (1)
  • AddOrReplaceHeader (8-18)
Activout.RestClient/RestClientException.cs (4)
  • ToString (34-37)
  • RestClientException (7-38)
  • RestClientException (9-14)
  • RestClientException (16-22)
Activout.RestClient/Serialization/IDeserializer.cs (1)
  • Task (10-10)
Activout.RestClient/Part.cs (2)
  • Part (4-9)
  • Part (11-18)
Activout.RestClient/Serialization/ISerializer.cs (1)
  • HttpContent (9-9)
Activout.RestClient/Serialization/Implementation/StringSerializer.cs (1)
  • HttpContent (13-14)
Activout.RestClient/Serialization/Implementation/ByteArraySerializer.cs (1)
  • HttpContent (11-23)
Activout.RestClient/Serialization/Implementation/FormUrlEncodedSerializer.cs (1)
  • HttpContent (18-39)
Activout.RestClient/Implementation/StringExtensions.cs (1)
  • SafeSubstring (5-20)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (4)
Activout.RestClient/Implementation/RequestHandler.cs (4)

1-16: Route content headers to request.Content.Headers and use TryAddWithoutValidation.

Adding e.g., Content-Type via request.Headers.Add(...) throws. Handle it explicitly and avoid strict validation for custom headers.

Apply:

 using System.Net.Http;
+using System.Net.Http.Headers;

 ...

-    private void SetHeaders(HttpRequestMessage request, List<KeyValuePair<string, object>> headers)
-    {
-        headers.ForEach(p => request.Headers.Add(p.Key, p.Value.ToString()));
-    }
+    private void SetHeaders(HttpRequestMessage request, List<KeyValuePair<string, object>> headers)
+    {
+        foreach (var header in headers)
+        {
+            var key = header.Key;
+            var value = header.Value?.ToString();
+            if (string.IsNullOrWhiteSpace(value)) continue;
+
+            // Content headers must go to Content.Headers
+            if (string.Equals(key, "Content-Type", StringComparison.OrdinalIgnoreCase))
+            {
+                request.Content ??= new ByteArrayContent(Array.Empty<byte>());
+                request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(value);
+                continue;
+            }
+
+            // Everything else: best-effort add
+            request.Headers.TryAddWithoutValidation(key, value);
+        }
+    }

To confirm there are no usages trying to set Content-Type via attributes:

#!/bin/bash
# Find attribute usages setting "Content-Type"
rg -nP --type=cs -C2 '\[(Header|HeaderParam)Attribute?\s*\(\s*"Content-Type"\s*,'

Also applies to: 276-279


494-505: Gate and sanitize body logging to avoid PII/PHI leakage.

Even at Debug, emitting raw bodies risks sensitive data exposure. Add an opt‑in flag in context (e.g., _context.LogBodies) and sanitize/truncate before logging.

Also applies to: 512-519


460-465: TODO left in code — track or resolve.

“prepare part serializer in advance” should be implemented or tracked in an issue; avoid TODOs in source.


315-318: Reject null path parameters explicitly; don’t silently skip.

Current early continue hides nulls and leaves {tokens} unresolved. Throw a clear error for path params (annotated and default).

-            if (rawValue == null)
-            {
-                continue;
-            }
+            // Defer null handling to attribute-specific branches so path params can be validated

 ...
                 else if (attribute is PathParamAttribute pathParamAttribute)
                 {
-                    var stringValue = _paramConverters[i].ToString(rawValue);
-                    pathParams[pathParamAttribute.Name ?? parameterName] = Uri.EscapeDataString(stringValue);
+                    if (rawValue is null)
+                        throw new InvalidOperationException(
+                            $"Path parameter '{pathParamAttribute.Name ?? parameterName}' cannot be null.");
+                    var stringValue = _paramConverters[i].ToString(rawValue)!;
+                    pathParams[pathParamAttribute.Name ?? parameterName] = Uri.EscapeDataString(stringValue);
                     handled = true;
                 }

 ...

             if (!handled)
             {
-                var stringValue = _paramConverters[i].ToString(rawValue);
-                pathParams[parameterName] = Uri.EscapeDataString(stringValue);
+                if (rawValue is null)
+                    throw new InvalidOperationException($"Path parameter '{parameterName}' cannot be null.");
+                var stringValue = _paramConverters[i].ToString(rawValue)!;
+                pathParams[parameterName] = Uri.EscapeDataString(stringValue);
             }

Also applies to: 346-351, 423-427

🧹 Nitpick comments (6)
Activout.RestClient/Implementation/RequestHandler.cs (6)

169-178: Validate unresolved route tokens after expansion.

Throw if any {...} remain to catch missing/nullable path params early.

-        return expanded;
+        if (expanded.IndexOf('{') >= 0 || expanded.IndexOf('}') >= 0)
+            throw new InvalidOperationException(
+                $"Unresolved route parameters in template '{_template}'. Expanded: '{expanded}'");
+        return expanded;

52-56: Remove redundant _serializer assignment.

Assigned from context at Line 52 and overwritten at Lines 100‑102. Keep only the latter.

-        _serializer = context.DefaultSerializer;
 ...
         _serializer = context.SerializationManager.GetSerializer(_contentType) ??
                       throw new InvalidOperationException("No serializer found for content type: " + _contentType);

Also applies to: 100-102


300-304: Clarify condition precedence.

Parentheses improve readability of the arg-count check.

-        if (_parameters.Length > 0 && args == null || _parameters.Length != args?.Length)
+        if ((_parameters.Length > 0 && args == null) || (_parameters.Length != args?.Length))

133-137: Use expression‑bodied members for simple helpers.

Aligns with guidelines.
[As per coding guidelines]

-    private static HttpMethod GetHttpMethod(HttpMethodAttribute attribute)
-    {
-        return attribute.HttpMethod;
-    }
+    private static HttpMethod GetHttpMethod(HttpMethodAttribute attribute) => attribute.HttpMethod;

-    private bool IsVoidTask()
-    {
-        return _returnType == typeof(Task);
-    }
+    private bool IsVoidTask() => _returnType == typeof(Task);

-    private bool IsGenericTask()
-    {
-        return _returnType.BaseType == typeof(Task) && _returnType.IsGenericType;
-    }
+    private bool IsGenericTask() => _returnType.BaseType == typeof(Task) && _returnType.IsGenericType;

Also applies to: 150-158


40-41: Prefer C# collection expressions ([]) for lists.

Simplifies initialization; keeps types explicit.
[As per coding guidelines]

-    private readonly List<KeyValuePair<string, object>> _requestHeaders = new List<KeyValuePair<string, object>>();
+    private readonly List<KeyValuePair<string, object>> _requestHeaders = [];

 ...
-        var headers = new List<KeyValuePair<string, object>>();
-        headers.AddRange(_requestHeaders);
-        var routeParams = new Dictionary<string, object>();
-        var queryParams = new List<string>();
-        var formParams = new List<KeyValuePair<string, string>>();
-        var partParams = new List<Part<HttpContent>>();
+        List<KeyValuePair<string, object>> headers = [.._requestHeaders];
+        var routeParams = new Dictionary<string, object>();
+        List<string> queryParams = [];
+        List<KeyValuePair<string, string>> formParams = [];
+        List<Part<HttpContent>> partParams = [];

Also applies to: 207-214


185-199: Improve exception messages in PrepareRequestMessage.

Throw actionable messages when BaseUri/RequestUri are missing to aid diagnostics.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1ce31be and e25ed1d.

📒 Files selected for processing (1)
  • Activout.RestClient/Implementation/RequestHandler.cs (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.cs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.cs: Use [] list syntax for collections
Use file-scoped namespaces
Use primary constructors wherever possible
Use records for data transfer objects (DTOs) and immutable data structures
Use var for local variable declarations when possible
Use expression-bodied members for simple methods and properties
Use pattern matching for type checks and deconstruction
Use using statements for resource management
Use async and await for asynchronous programming
Never include "Async" in method names
Only include comments that explain why something is done, not what is done

Files:

  • Activout.RestClient/Implementation/RequestHandler.cs
🧬 Code graph analysis (1)
Activout.RestClient/Implementation/RequestHandler.cs (12)
Activout.RestClient/Implementation/RestClient.cs (3)
  • RestClient (10-47)
  • RestClient (18-22)
  • IEnumerable (24-27)
Activout.RestClient/Helpers/ITaskConverterFactory.cs (1)
  • ITaskConverter (7-7)
Activout.RestClient/Helpers/Implementation/TaskConverter3Factory.cs (1)
  • ITaskConverter (9-14)
Activout.RestClient/Serialization/Implementation/SerializationManager.cs (3)
  • ISerializer (43-47)
  • SerializationManager (8-48)
  • SerializationManager (30-35)
Activout.RestClient/ParamConverter/Implementation/ParamConverterManager.cs (3)
  • IParamConverter (19-30)
  • ParamConverterManager (7-31)
  • ParamConverterManager (13-17)
Activout.RestClient/Implementation/HeaderListExtensions.cs (1)
  • AddOrReplaceHeader (8-18)
Activout.RestClient/ParamConverter/IParamConverter.cs (1)
  • ToString (10-10)
Activout.RestClient/RestClientException.cs (4)
  • ToString (34-37)
  • RestClientException (7-38)
  • RestClientException (9-14)
  • RestClientException (16-22)
Activout.RestClient/Serialization/IDeserializer.cs (1)
  • Task (10-10)
Activout.RestClient/Part.cs (2)
  • Part (4-9)
  • Part (11-18)
Activout.RestClient/Serialization/ISerializer.cs (1)
  • HttpContent (9-9)
Activout.RestClient/Implementation/StringExtensions.cs (1)
  • SafeSubstring (5-20)
🔇 Additional comments (2)
Activout.RestClient/Implementation/RequestHandler.cs (2)

329-337: Good fix: avoid iterating strings as collections.

and not string prevents char-by-char multipart parts. LGTM.


531-575: Response disposal logic is correct.

Disposes HttpResponseMessage except when returning message or HttpContent. Prevents handler/socket exhaustion.

@twogood twogood merged commit 67391d3 into main Oct 28, 2025
9 checks passed
@twogood twogood deleted the nullable-requesthandler branch October 28, 2025 08:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

Comments