-
Notifications
You must be signed in to change notification settings - Fork 4.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
API Request using Refit and System.Net.HTTP.DelegatingHandler / System.Net.Http.HttpClient fails with System.Net.Http.HttpIOException: The response ended prematurely. (ResponseEnded) and no Inner Exception #104296
Comments
Can you share more of the code? |
Sure - there is a lot of code and I was trying to make the report as concise as I could... GetApiResults (which is where the HttpClient is passed in to Refit and is then called):
StoreApiResults:
|
btw. this code is subtly wrong, you have individual tasks racing to increment the Maybe something like |
Yes, I also noticed when I was pasting that there are a couple of other issues with the parallelisation of the tasks (_auth.Logout() for example will use the DBContext and that's not thread safe) - plus pagecount could change unexpectedly. Plus I still need to look at the expected exception handling to ensure that all the expected error cases are properly handled. However, I'm reluctant to spend much more time on refactoring until I understand how to debug/fix the "response ended prematurely" Exception coming from HttpClient - since that has persisted through every parallel variant I've tried. |
I don't think I can use Parallel.ForAsync because I need to do the StoreApiResults part of the overall task (get -> store -> refresh cache) on one thread so that the DBContext is never used by two threads at the same time. |
"response ended prematurely" means the server closed the connection while we still expected data. There might be many reasons why we'd expect more data:
I think you need to simplify your program to figure out what's going on. Try to call the API without all the libraries just with HttpClient, collect packet captures to see what is exchanged between client and the server etc. There isn't much with which we could help right now... |
I'm still very much learning .net so I'm afraid I'm either going to need some guidance or some pointers to specific documentation (ideally with examples) - specifically: "Try to call the API without all the libraries" - Are you suggesting that I manually assemble the JSON string that is the request and assemble it into an HttpContent then use HttpClient.PostAsync to post it wrapped in a while loop that matches my current While/Any()/WhenAny() structure. " collect packet captures" - using Wireshark or similar? Or using something internal to dotnet? I'll give it a go, but its worth noting that if I switch off 'parallelMode' and let the tasks run sequentially there are no issues whatsoever - which makes me very tempted to just stick with the sequential implementation rather than rebuild the current version from the ground up until the issue presents itself. I'd be quite happy to get deeper into the SendAsync method, but when I try and disable 'justMyCode' VS Code tells me : Property justMyCode is not allowed. It feels like being able to debug the System.Net.Http libraries would be my quickest way forward but I'm not sure how to get to the source of a NuGet installed package to add breakpoints to it. Do I need to check out the whole dotnet repo and build it on my machine and then link it as a project in my solution? |
Yes.
Yes.
Feel free to do that, there's nothing wrong with that approach.
No, this should work from VS, use Step Into to open .NET source code, once that's opened, you can put break points in there. To add to that, you can turn on private logging for System.Net.Http and match the logging message with code to see what's going on, example code: https://github.com/dotnet/runtime/blob/main/src/libraries/Common/tests/TestUtilities/TestEventListener.cs Also the comment about the increment from @rzikm is very much true and you need to fix that in your parallel branch. |
Thanks for that! I will try the private logging for System.Net.Http first and see if that is illuminating. If that fails I will have a think about whether its worth trying to do a ground-up rebuild to try and get a minimal reproduction case - I'm still at the phase of learning where I make a minor change and spend hours trying to get to the bottom of one error message; so a full rewrite is quite daunting! Everything I can find on Stack Overflow etc says I should be able to step into the .NET source code but when I try I get: Step into: Stepping over non-user code 'ZymonicServices.HttpLoggingHandler.<>n__0' And it steps over the line instead - and trying to disable JustMyCode in launch.json gives a 'property not allowed error'. |
It might be because you're not stepping into .NET code but rather into some 3rd party library code.
I think that part of the problem is that you didn't start with minimal code and slowly build on that. You're starting with quite a bit of machinery there which obviously is hard to debug, change etc. You need to understand what's going on underneath first. |
To be fair to myself I did start with a command line version (which still works - and exhibits the same; "works in sequence/fails in parallelMode") and uses the same classes but it still uses Refit, Rx.Net and EFCore - because those were the examples I found that gave the functionality I needed and appeared well maintained - so even my first version wasn't bare bones. But I am gradually getting deeper into the machinery underneath as I go along... I've added in the private logging as follows:
That produces the attached log httplogginghandler_DetailedLog.txt - I set the debugger to stop on any exception and have confirmed that the exception it stopped on is: I added in the guid from the task to try and make sure that I only get messages from the async request that fails - however, I'm not ruling out that it includes messages from other tasks because of the way the http connection pool works. The last few lines of logging are:
And then it stops and the debugger shows it stopped at the exception - which leads me to theorise that the issue is not with the communications but with trying to generate the content of the request that's about to be sent to the server. I discovered a couple of days ago that because I'm serialising objects to generate requests I need to clone the object that contains the page number to change page (which is contained in another object that is actually serialised to form the reqest) - is it possible that I need to either deepclone / create a new object to being serialised to form the request body - and that not having done so is causing some kind of exception if two threads try and serialise the same object? |
Actually, double checking my code and each GetApiResults tasks has its own objects:
|
This has to be on the program level to log information from all paralel requests. Also make sure Run it without debugger to collect the logs so that you have a full picture. Fix that increment. Use Fiddler and/or WireShark to see whats happening on the network level. Either way, it's highly unlikely this is a bug in |
I moved the listener to the wrapping class and the trace output is much tidier and showed a 0 byte response from the server. I used Wireshark plugged into mitmproxy (since #37915 doesn't look to be available until release 9) - and discovered that it is indeed the server closing the connection - eventually tracked it down to an out of memory error server side; hence no server side log - and why it was occurring on sequential requests. Thanks for all the help; can happily confirm the HttpClient exception was indeed correct. |
Description
My code makes an API request to one of our servers using Refit with a DelegatingHandler to add authentication and a DelegatingHandler for logging - the UI is Avalonia with ReactiveUI, however, I can also reproduce the issue when I write a command line program that directly calls the API wrapping code.
The API returns results in 'pages' with the full data covering 510 'pages'. I have two branches of code one which requests each page sequentially and another which uses a List and WhenAny() / Any() to retrieve multiple pages at once.
The sequential version retrieves all 510 pages successfully. The parallel version (with max 6 tasks at a time) has 2 or 3 failing requests (they come in batches) every 10-20 pages.
The failing requests give the exception:
When I add a breakpoint in the debugger on the exception handling in the http logging handler (which is the innermost of my DelegatingHandlers) - the inner Exception is either the same as the outer or empty.
I'm relatively new to C# and .NET so quite prepared to be told that its my implementation that is wrong - also because of the fact our servers are not publicly available I can't easily create a minimal reproduction without starting from scratch.
The server logs and debugs report that the server responded with 200 status code and a complete JSON response.
Reproduction Steps
My Api is called as follows:
The HTTPClient is set-up as a static and initialised in the constructor (previously I was creating one each time the method above was called:
In Constructor:
The ZymonicAPIHandler has:
The logging handler is as follows:
Expected behavior
The expected behaviour is that following the initial request that gets a page count up to maximumFetchTasks run in parallel and retrieve the remaining pages.
Each response should look like:
Actual behavior
Every 10-20 pages then 2-3 of the following errors are reported:
The error and access logs on the server show:
Regression?
Not known...
Known Workarounds
Running the retrieval tasks sequentially.
Configuration
Dotnet version 8.0.104
Fedora Linux 40 (Workstation Edition)
Linux madai 6.9.5-200.fc40.x86_64 #1 SMP PREEMPT_DYNAMIC Sun Jun 16 15:47:09 UTC 2024 x86_64 GNU/Linux
Other information
My best guess is that its something related to multi-threading (as it doesn't happen with the single awaited variant) and that there is an exception being thrown somewhere that doesn't get caught properly because of my use of async / await - if I stop the code in the debugger then I see 'MoveNext' as part of the caller stack :
" at System.Net.Http.HttpConnection.d__57.MoveNext()"
I've spent some time watching videos and reading up on async/await to try and ensure I'm using good practices and this issue has persisted throughout my various attempts.
The only change I've been able to make that has completely worked around the issue is adding the parallelMode code branch and just doing one request at a time.
Finally, I've just noticed there's something wrong with the duration of both working and non-working request as they don't match the server side duration logged (even though the content of the logged response does match) - but I don't think that's significant.
The text was updated successfully, but these errors were encountered: