Skip to content

#2375 #2376 Map non-ASCII header HttpRequestException to 400 Bad Request#2379

Open
Majdi-Zlitni wants to merge 15 commits intoThreeMammals:developfrom
Majdi-Zlitni:fix-invalid-header-502
Open

#2375 #2376 Map non-ASCII header HttpRequestException to 400 Bad Request#2379
Majdi-Zlitni wants to merge 15 commits intoThreeMammals:developfrom
Majdi-Zlitni:fix-invalid-header-502

Conversation

@Majdi-Zlitni
Copy link
Copy Markdown

@Majdi-Zlitni Majdi-Zlitni commented Apr 8, 2026

Fixes #2376

Discussed in #2375

Changes

  • Created a new BadRequestError mapped to HTTP 400 (Bad Request).
  • Updated HttpExceptionToErrorMapper to specifically catch HttpRequestException instances complaining about "only ASCII characters".
  • Mapped this specific exception to BadRequestError instead of falling back to the generic ConnectionToDownstreamServiceError (HTTP 502).

Why it was done

When a client sends a request containing invalid header characters (for example an emoji 💀), Ocelot forwards the request as-is.
While building the outbound request, the underlying .NET HttpClient throws the following exception:

Previously, Ocelot's error handler mapped this exception to HTTP 502 Bad Gateway, which incorrectly implies a failure in the downstream service.

By intercepting this specific exception (Late Catch), the gateway correctly identifies it as a malformed client request and returns the industry-standard HTTP 400 Bad Request instead.


Architectural note

The solution uses a Late Catch strategy in the exception mapper rather than introducing early validation middleware.

This avoids the CPU overhead of iterating through and validating every incoming request header, preserving Ocelot’s philosophy of acting as a transparent, high-performance proxy.

Copy link
Copy Markdown
Member

@raman-m raman-m left a comment

Choose a reason for hiding this comment

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

@Majdi-Zlitni, please work on the suggestions.
This is the first round of pair programming.

Comment thread src/Ocelot/Requester/HttpExceptionToErrorMapper.cs Outdated
Comment thread src/Ocelot/Middleware/OcelotPipelineExtensions.cs
Comment thread src/Ocelot/Requester/BadRequestError.cs Outdated
Comment thread src/Ocelot/Requester/BadRequestError.cs Outdated
Comment thread src/Ocelot/Responder/ErrorsToHttpStatusCodeMapper.cs Outdated
@raman-m raman-m changed the title Fix: Map non-ASCII header HttpRequestException to 400 Bad Request # #2376 Map non-ASCII header HttpRequestException to 400 Bad Request Apr 9, 2026
@raman-m
Copy link
Copy Markdown
Member

raman-m commented Apr 9, 2026

Unit tests have failed → build 543

Did you run the tests locally? Please pay attention to the tests, since submitting code with failed tests turns the pull request into draft status.

@raman-m raman-m marked this pull request as draft April 9, 2026 10:45
@raman-m raman-m added bug Identified as a potential bug Error Handling Ocelot feature: Error Handling Responder Ocelot feature: Responder labels Apr 9, 2026
@Majdi-Zlitni
Copy link
Copy Markdown
Author

Majdi-Zlitni commented Apr 9, 2026

I will run the tests locally and check

@Majdi-Zlitni Majdi-Zlitni marked this pull request as ready for review April 9, 2026 13:12
@Majdi-Zlitni Majdi-Zlitni requested a review from raman-m April 9, 2026 13:13
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 9, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.75%. Comparing base (5eeb9cc) to head (43f6aee).

Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##           develop    #2379      +/-   ##
===========================================
+ Coverage    91.55%   91.75%   +0.19%     
===========================================
  Files          296      297       +1     
  Lines         5876     5882       +6     
  Branches       799      801       +2     
===========================================
+ Hits          5380     5397      +17     
+ Misses         385      379       -6     
+ Partials       111      106       -5     
Flag Coverage Δ
unittests 91.75% <100.00%> (+0.19%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/Ocelot/Requester/BadRequestError.cs 100.00% <100.00%> (ø)
src/Ocelot/Requester/HttpExceptionToErrorMapper.cs 100.00% <100.00%> (ø)
...c/Ocelot/Responder/ErrorsToHttpStatusCodeMapper.cs 85.18% <100.00%> (+1.18%) ⬆️

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@coveralls
Copy link
Copy Markdown
Collaborator

coveralls commented Apr 9, 2026

Coverage Status

coverage: 92.618% (+0.09%) from 92.525% — Majdi-Zlitni:fix-invalid-header-502 into ThreeMammals:develop

Majdi ZLITNI added 2 commits April 10, 2026 10:38
Add unit tests covering multiple edge cases in PollKube:

- GetAsync early exit when instance is disposed
- PollAsync behavior when queue size > 3
- ObjectDisposedException handling
- cancellation token scenarios
- finalizer dispose path
@raman-m raman-m added the Requester Ocelot feature: Requester label Apr 12, 2026
@raman-m
Copy link
Copy Markdown
Member

raman-m commented Apr 12, 2026

Majdi, thanks for being proactive. 💪

The next step is writing an acceptance test for the linked bug #2376. We must add at least one test to be able to close the bug officially.
I will re-check the previous code review and re-think the current bug fix, which looks simple, but the upcoming acceptance testing will uncover the next steps for this PR.

@Majdi-Zlitni
Copy link
Copy Markdown
Author

Thank you @raman-m, I’ve added the acceptance test for #2376 to cover the bug end to end, so the fix is now backed by regression coverage.

@raman-m raman-m added high High priority NET10 .NET 10 release labels Apr 14, 2026
@raman-m raman-m added this to the .NET 10 milestone Apr 14, 2026
@raman-m
Copy link
Copy Markdown
Member

raman-m commented Apr 14, 2026

I've changed the priorities, and this PR has been included in the current release as a patch. Thus, the patch will be a part of .NET 10 release. I'm going to perform a final code review...

@Majdi-Zlitni
Copy link
Copy Markdown
Author

Thanks @raman-m for the update and for including this in the release. I really appreciate the opportunity to contribute and learn. Looking forward to your final review.

@raman-m raman-m added the RFC-7230 RFC 7230 standard titled "Hypertext Transfer Protocol (HTTP/1.1): Message Syntax and Routing" label Apr 14, 2026
@raman-m raman-m moved this from Todo to In Progress in RFC 7230 Standard Apr 14, 2026
Copy link
Copy Markdown
Member

@raman-m raman-m left a comment

Choose a reason for hiding this comment

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

This is the second round, and my suggestions are below.
I will push a commit tomorrow with my review comments, if not fixed by you.

My current concern is that the acceptance test requires refactoring to use helpers from the Ocelot.Testing namespace. The test should be shorter and easier to read.
After that, we will be able to verify the bug fix, continue debugging, and find an optimal solution.

P.S. AI helps, but it sometimes produces redundant and unusual code that doesn’t match the current style and libraries.

Comment thread test/Ocelot.AcceptanceTests/Requester/InvalidHeaderValueTests.cs Outdated
Comment thread test/Ocelot.AcceptanceTests/Requester/InvalidHeaderValueTests.cs Outdated
Comment thread test/Ocelot.AcceptanceTests/Requester/InvalidHeaderValueTests.cs Outdated
Comment thread test/Ocelot.AcceptanceTests/Requester/InvalidHeaderValueTests.cs Outdated
Comment thread test/Ocelot.AcceptanceTests/Requester/InvalidHeaderValueTests.cs Outdated
Comment thread test/Ocelot.AcceptanceTests/Requester/InvalidHeaderValueTests.cs Outdated
Comment thread test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs Outdated
Comment thread test/Ocelot.UnitTests/Kubernetes/PollKubeTests.cs
// Late Catch: Map the HttpRequestException to a 400 Bad Request.
// If the header format is invalid, HttpClient throws this exception locally.
// By catching it here, we ensure Ocelot returns a clean 4xx client error instead of a generic 502 Bad Gateway.
if (exception is HttpRequestException && exception.Message.Contains("only ASCII characters"))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It’s hard to judge this solution until a valid acceptance test has been written.

I believe we can improve this area, since the Message.Contains method handles a specific user scenario. But what about codes, internal exceptions, and so on? We should inspect the generated exception and reuse its state rather than relying only on messages.

We might also consider covering more scenarios that return a BadRequestError response.
Given the purpose of BadRequestError, it should cover all exceptions that result in a 400 Bad Request status. So, I'd expect more if-else block(s).
You (we) might be inspired by this #2375 (reply in thread).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Appreciate the guidance

Comment thread test/Ocelot.AcceptanceTests/Requester/InvalidHeaderValueTests.cs Outdated
@raman-m raman-m changed the title #2376 Map non-ASCII header HttpRequestException to 400 Bad Request #2375 #2376 Map non-ASCII header HttpRequestException to 400 Bad Request Apr 14, 2026
Majdi-Zlitni and others added 2 commits April 15, 2026 00:50
addresses PR feedback by replacing complex TCP stream and manual port assignments with Ocelot's native BDD steps

Co-authored-by: Raman Maksimchuk <dotnet044@gmail.com>
@Majdi-Zlitni Majdi-Zlitni force-pushed the fix-invalid-header-502 branch from f062bb8 to d1246cb Compare April 15, 2026 00:33
@Majdi-Zlitni
Copy link
Copy Markdown
Author

Ocelot is really cool 😎 but found it a little hard for me, I really enjoy learning from it and hope to keep on learning, also I need to improve my AI gen code review to contribute more effectively.
Thanks @raman-m for your patience🙏

@raman-m raman-m removed their assignment Apr 15, 2026
@raman-m
Copy link
Copy Markdown
Member

raman-m commented Apr 15, 2026

@Majdi-Zlitni Please stop pushing commits! You've done a lot from your side. Thank you!
I will do the rest of the work.

Comment on lines +36 to +39
catch (HttpRequestException)
{
response = new HttpResponseMessage(HttpStatusCode.BadRequest);
}
Copy link
Copy Markdown
Member

@raman-m raman-m Apr 15, 2026

Choose a reason for hiding this comment

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

Yeah, that is funny.
This life hack — which catches the HttpClient exception early by adjusting the response to return a 400 status — actually triggers the exception too early. Here is the call stack it produces:

   at System.Net.Http.HttpConnection.<WriteString>g__ThrowForInvalidCharEncoding|55_0()
   at System.Net.Http.HttpConnection.WriteString(String s, Encoding encoding)
   at System.Net.Http.HttpConnection.WriteHeaderCollection(HttpHeaders headers, String cookiesFromContainer)
   at System.Net.Http.HttpConnection.WriteHeaders(HttpRequestMessage request)
   at System.Net.Http.HttpConnection.<SendAsync>d__56.MoveNext()
   at System.Net.Http.HttpConnection.<SendAsync>d__56.MoveNext()
   at System.Net.Http.HttpConnectionPool.<SendWithVersionDetectionAndRetryAsync>d__52.MoveNext()
   at System.Net.Http.DiagnosticsHandler.<SendAsyncCore>d__10.MoveNext()
   at System.Runtime.CompilerServices.ConfiguredValueTaskAwaitable`1.ConfiguredValueTaskAwaiter.GetResult()
   at System.Net.Http.RedirectHandler.<SendAsync>d__4.MoveNext()
   at System.Net.Http.SocketsHttpHandler.<<SendAsync>g__CreateHandlerAndSendAsync|115_0>d.MoveNext()
   at System.Net.Http.HttpClient.<<SendAsync>g__Core|83_0>d.MoveNext()
   at Ocelot.Testing.AcceptanceSteps.<WhenIGetUrlOnTheApiGateway>d__86.MoveNext() in D:\github\Ocelot\Majdi-Zlitni\Ocelot\testing\AcceptanceSteps.cs:line 368
   at Ocelot.AcceptanceTests.Requester.InvalidHeaderValueTests.<WhenIGetUrlOnTheApiGatewaySafely>d__1.MoveNext() in D:\github\Ocelot\Majdi-Zlitni\Ocelot\test\Ocelot.AcceptanceTests\Requester\InvalidHeaderValueTests.cs:line 34

However, the original bug #2374 has a different call stack:

         at System.Net.Http.HttpConnection.<WriteString>g__ThrowForInvalidCharEncoding|55_0()
         at System.Net.Http.HttpConnection.WriteString(String s, Encoding encoding)
         at System.Net.Http.HttpConnection.WriteHeaderCollection(HttpHeaders headers, String cookiesFromContainer)
         at System.Net.Http.HttpConnection.WriteHeaders(HttpRequestMessage request)
         at System.Net.Http.HttpConnection.SendAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
         at System.Net.Http.HttpConnection.SendAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
         at System.Net.Http.HttpConnectionPool.SendWithVersionDetectionAndRetryAsync(HttpRequestMessage request, Boolean async, Boolean doRequestAuth, CancellationToken cancellationToken)
         at System.Net.Http.DiagnosticsHandler.SendAsyncCore(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
         at Ocelot.Requester.TimeoutDelegatingHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) in /home/laura/scratch_pad/Ocelot/src/Ocelot/Requester/TimeoutDelegatingHandler.cs:line 30
         at Ocelot.Requester.MessageInvokerHttpRequester.GetResponse(HttpContext httpContext) in /home/laura/scratch_pad/Ocelot/src/Ocelot/Requester/MessageInvokerHttpRequester.cs:line 29

As a result, we are reproducing the same bug on the HttpClient side, and the actual request never reaches the Ocelot side. This happened because the acceptance test was changed when I asked to use our testing helpers.

I was wrong — the original test version actually used the correct approach to emulate a curl request, based on TcpClient streams.

Tomorrow I will rewrite the test based on the original version. The AI coding agent was right when it suggested how to properly emulate curl requests in .NET apps.
In the end, we must use plain TcpClient streams only if we want to reliably reproduce the bug.
This is my fault 🙃

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Identified as a potential bug Error Handling Ocelot feature: Error Handling high High priority NET10 .NET 10 release Requester Ocelot feature: Requester Responder Ocelot feature: Responder RFC-7230 RFC 7230 standard titled "Hypertext Transfer Protocol (HTTP/1.1): Message Syntax and Routing"

Projects

Status: In Progress

3 participants