Share via

HttpClient vs browser (Edge, Chrome) error/exception handling

Andrew Mercer 546 Reputation points
2026-04-03T22:52:05.91+00:00

My bottom line is I'd like to know how a mature browser (Edge, Chrome) retrieves a url. It is certainly not as .NET documentation describes with Net.Http.HttpClient (.NET v9).

In a VB stock market data scraping app, one website (https://stockanalysis.com) exhibits the problem. The problem is an unpredictable mixture of successful downloads and thrown AggregateExceptions. The problem does not occur with other websites (eg https://www.zacks.com/). The problem does not occur when Edge or Chrome accesses stockanalysis. The problem does not occur with a Selenium test jig driving Chrome. The problem occurs ONLY with Client as HttpClient executing Client.GetStringAsync(Url).Result.

The symptoms suggest something is different about stockanalysis.com versus other websites. Edge and Chrome cope with such differences, but HttpClient does not, and that is my objection. I don't care what is different about stockanalysis.com - if Edge copes, so too should HttpClient.

A possibly important detail is that successful downloads and AggregateExceptions come in batches. With repeated download attempts, I get 5 to 50 failures followed by 5 to 50 successes, and so on.

Inner exceptions identify an SSL exception, and much has been written about it. I rule the suggested configuration and componenent incompatibility problems. The reason is that HttpClient is intermittent while browsers always succeed. The exception says "One or more errors occurred. (The SSL connection could not be established ...". Stack trace indicates socket error 10054.

The general form of a stockanalysis url is https://stockanalysis.com/stocks/TTT/PAGE/. Replace TTT with a valid stock ticker such as MSFT or AAPL; there are many more. Replace PAGE with the word financials, forecast, statistics, or metrics; there are a few more. One such valid url is https://stockanalysis.com/stocks/MSFT/statistics/

The VB recipe has a single reusable HttpClient that endures until the program ends.

Client As New Net.Http.HttpClient() With {.Timeout = TimeSpan.FromSeconds(10)}

Assuming string variables UrlText and Url, you attempt a retrieval with

UrlText = Client.GetStringAsync(Url).Result

This line blocks until it returns html text or it raises an aggregate exception. This exception is detailed below (wide lines, sorry about that).

GetUrlBlocking Exception -- https://stockanalysis.com/stocks/IBM/statistics/

ex and inner exceptions

HResult 0x80131500 -- AggregateException -- One or more errors occurred. (The SSL connection could not be established, see inner exception.)

HResult 0x80131620 -- HttpRequestException -- The SSL connection could not be established, see inner exception.

HResult 0x80131620 -- IOException -- Unable to write data to the transport connection: An existing connection was forcibly closed by the remote host..

HResult 0x80004005 -- SocketException -- An existing connection was forcibly closed by the remote host.

ex.StackTrace

at System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions)

at System.Threading.Tasks.Task.Wait(Int32 millisecondsTimeout, CancellationToken cancellationToken)

at System.Threading.Tasks.Task.Wait()

at NewGetUrl.GetUrlModule.GetUrlBrokenDown(String Url) in C:\Users\andy\source\repos\NewGetUrl\NewGetUrl\Form1.vb:line 126

ex.ToString truncated

System.AggregateException: One or more errors occurred. (The SSL connection could not be established, see inner exception.)

---> System.Net.Http.HttpRequestException: The SSL connection could not be established, see inner exception.

---> System.IO.IOException: Unable to write data to the transport connection: An existing connection was forcibly closed by the remote host..

---> System.Net.Sockets.SocketException (10054): An existing connection was forcibly closed by the remote host.

at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.CreateException(SocketError error, Boolean forAsyncThrow)

at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.SendAsyncForNetworkStream(Socket socket, CancellationToken cancellationToken)

at System.Net.Sockets.Socket.SendAsyncForNetworkStream(ReadOnlyMemory`1 buffer, SocketFlags socketFlags, CancellationToken cancellationToken)

at System.Net.Sockets.NetworkStream.WriteAsync(ReadOnlyMemory`1 buffer, CancellationToken cancellationToken)

at System.Net.Security.SslStream.ForceAuthenticationAsyncTIOAdapter

Developer technologies | .NET | Other
0 comments No comments

3 answers

Sort by: Most helpful
  1. AgaveJoe 31,186 Reputation points
    2026-04-08T18:26:17.21+00:00

    Hi Andrew,

    While I understand the frustration of HttpClient behaving differently than a browser, there are a few technical red flags in your implementation that likely explain why the server is dropping your connections.

    The .Result Anti-Pattern

    Using Client.GetStringAsync(Url).Result is a major anti-pattern in .NET. By forcing an asynchronous task to run synchronously, you risk:

    Thread Pool Starvation: Especially during "batches," you can exhaust the threads available to your app.

    Deadlocks: Depending on your synchronization context, this is a common source of hangs.

    Exception Masking: You mentioned the AggregateException making error handling cumbersome; that is a direct result of using .Result. If you use Await, the first inner exception is captured directly, making your code much cleaner.

    Rate Limiting & Fingerprinting (The "10054" Error)

    The fact that failures happen in batches on only one site is a textbook symptom of Rate Limiting or Anti-Bot protection (like Cloudflare).

    Socket Error 10054 (Connection forcibly closed) is exactly how a Web Application Firewall (WAF) behaves when it detects a "scraping" pattern. It doesn't send a polite 429 status; it kills the TCP socket to save resources.

    The Fix: I’ve dealt with this recently when Cloudflare updated their bot detection. Simply adding a User-Agent and Accept-Language header to your HttpClient often allows the request to pass the initial bot "sniff test."

    Apples to Oranges Comparison

    Comparing a browser to a looping HttpClient is a bit lopsided. A browser typically fetches a page and its assets, then stops. Your app is likely hitting the server in a tight loop for different tickers—something a human can't do. Even if you held down Ctrl+F5 in a browser, the browser manages TLS session reuse and connection pooling more gracefully than a low-level library.

    Suggested Path Forward:

    Refactor to Async/Await: Switch to Await Client.GetStringAsync(Url) to fix the threading and exception handling.

    Mimic a Browser: Add a standard User-Agent header to your HttpClient instance.

    Introduce Throttling: Add a small delay (e.g., 1 second) between calls. If the 10054 errors stop, you’ve confirmed it’s a rate-limiting issue.

    Check for APIs: It might be worth checking if the site offers a legitimate service endpoint, which is usually more stable than scraping HTML.

    0 comments No comments

  2. Surya Amrutha Vaishnavi Lanka (INFOSYS LIMITED) 1,960 Reputation points Microsoft External Staff
    2026-04-07T07:28:08.7033333+00:00

    Thanks for reaching out!
    HttpClient is a low-level transport client that surfaces TLS/network failures as exceptions. Browsers implement additional layers such as retry policies, connection pooling, TLS session reuse, and adaptive behaviour, which mask transient failures.

    To achieve this we can implement

    • configurable retry strategy with exponential backoff
    • connection pooling and reuse via SocketsHttpHandler.
    • Browser like request headers.
    • controlled TLS protocol configuration

    Unlike browsers, HttpClient does not implement advanced heuristics such as dynamic fingerprinting, adaptive throttling, or JavaScript-based challenge handling.


  3. Q&A Assist
    2026-04-03T22:52:30.3733333+00:00
    AI generated content. This question contains an answer created with AI from Q&A Assist. Learn more

    Browsers and HttpClient both use HTTPS/TLS, but they differ in how they manage connections, retries, and error handling. The behavior described (intermittent SocketException 10054 / “connection forcibly closed by remote host”) is consistent with the remote server or an intermediary (proxy, load balancer, firewall) closing TLS connections in a way that HttpClient surfaces as an exception, while browsers hide this behind automatic retries and connection management.

    Key points based on the documented behavior of HttpClient:

    1. HttpClient surfaces network and TLS problems as exceptions
      • When an HTTP request fails at the protocol or transport level, HttpClient throws HttpRequestException. Other exceptions such as TaskCanceledException, IOException, or SocketException can be involved as inner exceptions.
      • This is by design: applications are expected to catch these exceptions and decide how to handle them (retry, backoff, log, etc.).
      • Documentation explicitly calls out that multiple exception types may need to be handled, not just HttpRequestException.
      See: HTTP error handling with HttpClient, where HttpRequestException is thrown and inspected via StatusCode when available.
    2. Methods like GetStringAsync implicitly enforce success and throw
      • Methods that do not return HttpResponseMessage (such as GetStringAsync, GetByteArrayAsync, GetStreamAsync) implicitly call EnsureSuccessStatusCode.
      • Any non-success HTTP status (non-2xx) or protocol failure results in HttpRequestException instead of a response object.
      • This is different from a browser, which will show an error page or retry internally instead of surfacing an exception to application code.
    3. HTTP status vs. transport-level failures
      • For HTTP protocol errors, status codes are available either on HttpResponseMessage.StatusCode or HttpRequestException.StatusCode (for methods like GetStringAsync).
      • For transport-level failures (like TLS handshake issues or connection resets), there is no HTTP status code; instead, the exception chain (e.g., HttpRequestExceptionIOExceptionSocketException) is the only signal.
      • Browsers typically retry on such failures, possibly with different connection parameters, and hide the transient nature from the user.
    4. Connection reuse and pooling behavior
      • HttpClient is designed to be long-lived and reused. In .NET Core and later, it pools connections via the underlying handler.
      • DNS and connection lifetime behavior is controlled by the handler (for example, SocketsHttpHandler.PooledConnectionLifetime). If the server or intermediary behaves differently across connections or rejects reused TLS sessions, this can cause intermittent failures that a browser’s more complex connection logic might mask.
      • Documentation notes that connection-level behavior is tied to the handler configuration and that multiple HttpClient instances may be needed when different connection settings are required.
    5. UWP / Windows.Web.Http behavior is similar: exceptions, not silent recovery
      • In UWP, Windows.Web.Http.HttpClient also throws exceptions for network and TLS issues. The guidance is to always catch exceptions for async methods and inspect Exception.HResult and Exception.Message.
      • This mirrors the .NET HttpClient model: network stack errors are surfaced to app code rather than automatically hidden.
    6. Practical implications for the described scenario
      • The intermittent SocketException 10054 indicates the remote host (or something in the path) is closing the connection. HttpClient correctly surfaces this as an exception.
      • Browsers likely:
        • Maintain multiple parallel connections.
        • Retry failed requests automatically.
        • Adjust TLS parameters or session reuse behavior dynamically.
      • HttpClient does not implement browser-like retry and recovery logic by default. Application code is expected to:
        • Catch HttpRequestException and inner exceptions.
        • Optionally implement retry logic with backoff.
        • Adjust handler settings (timeouts, connection lifetimes) if needed.
    7. How to handle this with HttpClient according to documentation
      • Wrap calls in try/catch and handle HttpRequestException, TaskCanceledException, and inner exceptions.
      • For methods like GetStringAsync, rely on catching HttpRequestException and inspecting StatusCode when present; for transport errors, inspect the inner exception chain.
      • Consider using a long-lived HttpClient with a custom handler configuration if connection reuse is suspected to interact poorly with the server.

    In summary, the difference is not that HttpClient is incorrect; it is that HttpClient is a low-level HTTP client library that surfaces network and TLS failures as exceptions for application code to handle, whereas browsers implement additional layers of retry and recovery logic on top of the same underlying networking stack.


    References:


Your answer

Answers can be marked as 'Accepted' by the question author and 'Recommended' by moderators, which helps users know the answer solved the author's problem.