Skip to content

Commit

Permalink
Merge pull request #42293 from CarnaViire/hcf-typed-client-problems
Browse files Browse the repository at this point in the history
Add HttpClientFactory troubleshooting docs
  • Loading branch information
CarnaViire authored Sep 2, 2024
2 parents a471f7d + af1fe39 commit af3134d
Show file tree
Hide file tree
Showing 4 changed files with 203 additions and 12 deletions.
186 changes: 186 additions & 0 deletions docs/core/extensions/httpclient-factory-troubleshooting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
---
title: Troubleshoot IHttpClientFactory issues
description: Learn how to troubleshoot common HttpClient and IHttpClientFactory problems.
author: CarnaViire
ms.author: knatalia
ms.date: 08/21/2024
---

# Common `IHttpClientFactory` usage issues

In this article, you'll learn some of the most common problems you can run into when using `IHttpClientFactory` to create `HttpClient` instances.

`IHttpClientFactory` is a convenient way to set up multiple `HttpClient` configurations in the DI container, configure logging, set up resilience strategies, and more. `IHttpClientFactory` also incapsulates the lifetime management of `HttpClient` and `HttpMessageHandler` instances, to prevent problems like socket exhaustion and losing DNS changes. For an overview on how to use `IHttpClientFactory` in your .NET application, see [IHttpClientFactory with .NET](httpclient-factory.md).

Due to a complex nature of `IHttpClientFactory` integration with DI, you can hit some issues that might be hard to catch and troubleshoot. The scenarios listed in this article also contain recommendations, which you can apply proactively to avoid potential problems.

## `HttpClient` doesn't respect `Scoped` lifetime

You can hit a problem if you need to access any scoped service, for example, `HttpContext`, or some scoped cache, from within the `HttpMessageHandler`. The data saved there can either "disappear", or, the other way around, "persist" when it shouldn't. This is caused by the Dependency Injection (DI) **scope mismatch** between the application context and the handler instance, and it's a known limitation in `IHttpClientFactory`.

`IHttpClientFactory` creates a separate DI scope per each `HttpMessageHandler` instance. These handler scopes are distinct from application context scopes (for example, ASP.NET Core incoming request scope, or a user-created manual DI scope), so they will not share scoped service instances.

As a result of this limitation:

- Any data cached "externally" in a scoped service will **not** be available within the `HttpMessageHandler`.
- Any data cached "internally" within the `HttpMessageHandler` or its scoped dependencies **can** be observed from multiple application DI scopes (for example, from different incoming requests) as they can share the same handler.

Consider the following recommendations to help alleviate this known limitation:

❌ DO NOT cache any scope-related information (such as data from `HttpContext`) inside `HttpMessageHandler` instances or its dependencies to avoid leaking sensitive information.

❌ DO NOT use cookies, as the `CookieContainer` will be shared together with the handler.

✔️ CONSIDER not storing the information, or only pass it within the `HttpRequestMessage` instance.

To pass arbitrary information alongside the `HttpRequestMessage`, you can use the <xref:System.Net.Http.HttpRequestMessage.Options?displayProperty=nameWithType> property.

✔️ CONSIDER encapsulating all the scope-related (for example, authentication) logic in a separate `DelegatingHandler` that's **not** created by the `IHttpClientFactory`, and use it to wrap the `IHttpClientFactory`-created handler.

To create just an `HttpMessageHandler` without `HttpClient`, call <xref:System.Net.Http.IHttpMessageHandlerFactory.CreateHandler%2A?displayProperty=nameWithType> for any registered _named client_. In that case, you will need to create an `HttpClient` instance yourself using the combined handler. You can find a fully runnable example for this workaround on [GitHub](https://github.com/dotnet/docs/tree/main/docs/core/extensions/snippets/http/scopeworkaround).

For more information, see the [Message Handler Scopes in IHttpClientFactory](httpclient-factory.md#message-handler-scopes-in-ihttpclientfactory) section in the `IHttpClientFactory` guidelines.

## `HttpClient` doesn't respect DNS changes

Even if `IHttpClientFactory` is used, it's still possible to hit the stale DNS problem. This can usually happen if an `HttpClient` instance gets captured in a `Singleton` service, or, in general, stored somewhere for a period of time that's longer than the specified `HandlerLifetime`. `HttpClient` will also get captured if the respective _typed client_ is captured by a singleton.

❌ DO NOT cache `HttpClient` instances created by `IHttpClientFactory` for prolonged periods of time.

❌ DO NOT inject _typed client_ instances into `Singleton` services.

✔️ CONSIDER requesting a client from `IHttpClientFactory` in a timely manner or each time you need one. Factory-created clients are safe to dispose.

`HttpClient` instances created by `IHttpClientFactory` are intended to be **short-lived**.

- Recycling and recreating `HttpMessageHandler`'s when their lifetime expires is essential for `IHttpClientFactory` to ensure the handlers react to DNS changes. `HttpClient` is tied to a specific handler instance upon its creation, so new `HttpClient` instances should be requested in a timely manner to ensure the client will get the updated handler.

- Disposing of such `HttpClient` instances **created by the factory** will not lead to socket exhaustion, as its disposal **does not** trigger disposal of the `HttpMessageHandler`. `IHttpClientFactory` tracks and disposes of resources used to create `HttpClient` instances, specifically the `HttpMessageHandler` instances, as soon their lifetime expires and there's no `HttpClient` using them anymore.

_Typed clients_ are intended to be **short-lived** as well, as an `HttpClient` instance is injected into the constructor, so it will share the _typed client_ lifetime.

For more information, see the [`HttpClient` lifetime management](httpclient-factory.md#httpclient-lifetime-management) and [Avoid _typed clients_ in singleton services](httpclient-factory.md#avoid-typed-clients-in-singleton-services) sections in the `IHttpClientFactory` guidelines.

## `HttpClient` uses too many sockets

Even if `IHttpClientFactory` is used, it's still possible to hit the socket exhaustion issue with a specific usage scenario. By default, `HttpClient` doesn't limit the number of concurrent requests. If a large number of HTTP/1.1 requests are started concurrently at the same time, each of them will end up triggering a new HTTP connection attempt, because there is no free connection in the pool and no limit is set.

❌ DO NOT start a large number of HTTP/1.1 requests concurrently at the same time without specifying the limits.

✔️ CONSIDER setting <xref:System.Net.Http.HttpClientHandler.MaxConnectionsPerServer?displayProperty=nameWithType> (or <xref:System.Net.Http.SocketsHttpHandler.MaxConnectionsPerServer?displayProperty=nameWithType>, if you use it as a primary handler) to a reasonable value. Note that these limits only apply to the specific handler instance.

✔️ CONSIDER using HTTP/2, which allows multiplexing requests over a single TCP connection.

## _Typed client_ has the wrong `HttpClient` injected

There can be various situations in which it is possible to get an unexpected `HttpClient` injected into a _typed client_. Most of the time, the root cause will be in an erroneous configuration, as, by DI design, any subsequent registration of a service overrides the previous one.

_Typed clients_ use _named clients_ "under the hood": adding a _typed client_ implicitly registers and links it to a _named client_. The client name, unless explicitly provided, will be set to the type name of `TClient`. This would be the **first** one from the `TClient,TImplementation` pair if `AddHttpClient<TClient,TImplementation>` overloads are used.

Therefore, registering a _typed client_ does two separate things:

1. Registers a _named client_ (in a simple default case, the name is `typeof(TClient).Name`).
1. Registers a `Transient` service using the `TClient` or `TClient,TImplementation` provided.

The following two statements are technically the same:

```csharp
services.AddHttpClient<ExampleClient>(c => c.BaseAddress = new Uri("http://example.com"));

// -OR-
services.AddHttpClient(nameof(ExampleClient), c => c.BaseAddress = new Uri("http://example.com")) // register named client
.AddTypedClient<ExampleClient>(); // link the named client to a typed client
```

In a simple case, it will also be similar to the following:

```csharp
services.AddHttpClient(nameof(ExampleClient), c => c.BaseAddress = new Uri("http://example.com")); // register named client
// register plain Transient service and link it to the named client
services.AddTransient<ExampleClient>(s =>
new ExampleClient(
s.GetRequiredService<IHttpClientFactory>().CreateClient(nameof(ExampleClient))));
```

Consider the following examples of how the link between _typed_ and _named_ clients can get broken.

### _Typed client_ is registered a second time

❌ DO NOT register the _typed client_ separately — it is already registered automatically by the `AddHttpClient<T>` call.

If a _typed client_ is erroneously registered a second time as a plain Transient service, this will overwrite the registration added by the `HttpClientFactory`, breaking the link to the _named client_. It will manifest as if the `HttpClient`'s configuration is lost, as an unconfigured `HttpClient` will get injected into the _typed client_ instead.

It might be confusing that, instead of throwing an exception, a "wrong" `HttpClient` is used. This happens because the "default" unconfigured `HttpClient` — the client with the <xref:Microsoft.Extensions.Options.Options.DefaultName?displayProperty=nameWithType> name (`string.Empty`) — is registered as a plain Transient service, to enable the most basic `HttpClientFactory` usage scenario. That's why after the link gets broken and the _typed client_ becomes just an ordinary service, this "default" `HttpClient` will naturally get injected into the respective constructor parameter.

### Different _typed clients_ are registered on a common interface

In case two different _typed clients_ are registered on a common interface, they both would reuse the same _named client_. This can seem like the first _typed client_ getting the second _named client_ "wrongly" injected.

❌ DO NOT register multiple _typed clients_ on a single interface without explicitly specifying the name.

✔️ CONSIDER registering and configuring a _named client_ separately, and then linking it to one or multiple _typed clients_, either by specifying the name in `AddHttpClient<T>` call or by calling `AddTypedClient` during the _named client_ setup.

By design, registering and configuring a _named client_ with the same name several times just appends the configuration actions to the list of existing ones. This behavior of `HttpClientFactory` might not be obvious, but it is the same approach that is used by the [Options pattern](options.md) and configuration APIs like <xref:Microsoft.Extensions.Options.OptionsBuilder%601.Configure%2A>.

This is mostly useful for advanced handler configurations, for example, adding a custom handler to a _named client_ defined externally, or mocking a primary handler for tests, but it works for `HttpClient` instance configuration as well. For example, the three following examples will result in an `HttpClient` configured in the **same** way (both `BaseAddress` and `DefaultRequestHeaders` are set):

```csharp
// one configuration callback
services.AddHttpClient("example", c =>
{
c.BaseAddress = new Uri("http://example.com");
c.DefaultRequestHeaders.UserAgent.ParseAdd("HttpClient/8.0");
});

// -OR-
// two configuration callbacks
services.AddHttpClient("example", c => c.BaseAddress = new Uri("http://example.com"))
.ConfigureHttpClient(c => c.DefaultRequestHeaders.UserAgent.ParseAdd("HttpClient/8.0"));

// -OR-
// two configuration callbacks in separate AddHttpClient calls
services.AddHttpClient("example", c => c.BaseAddress = new Uri("http://example.com"));
services.AddHttpClient("example")
.ConfigureHttpClient(c => c.DefaultRequestHeaders.UserAgent.ParseAdd("HttpClient/8.0"));
```

This enables linking a _typed client_ to an already defined _named client_, and also linking several _typed clients_ to a single _named client_. It is more obvious when overloads with a `name` parameter are used:

```csharp
services.AddHttpClient("LogClient", c => c.BaseAddress = new Uri(LogServerAddress));

services.AddHttpClient<FooLogger>("LogClient");
services.AddHttpClient<BarLogger>("LogClient");
```

The same thing can also be achieved by calling <xref:Microsoft.Extensions.DependencyInjection.HttpClientBuilderExtensions.AddTypedClient%2A> during the _named client_ configuration:

```csharp
services.AddHttpClient("LogClient", c => c.BaseAddress = new Uri(LogServerAddress))
.AddTypedClient<FooLogger>()
.AddTypedClient<BarLogger>();
```

However, if you _don't_ want to reuse the same _named client_, you but you still wish to register the clients on the same interface, you can do so by explicitly specifying different names for them:

```csharp
services.AddHttpClient<ITypedClient, ExampleClient>(nameof(ExampleClient),
c => c.BaseAddress = new Uri("http://example.com"));
services.AddHttpClient<ITypedClient, GithubClient>(nameof(GithubClient),
c => c.BaseAddress = new Uri("https://github.com"));
```

## See also

- [IHttpClientFactory with .NET][hcf]
- <xref:System.Net.Http.IHttpClientFactory>
- <xref:System.Net.Http.IHttpMessageHandlerFactory>
- <xref:System.Net.Http.HttpClient>
- [Make HTTP requests with the HttpClient][httpclient]

[hcf]: httpclient-factory.md
[httpclient]: ../../fundamentals/networking/http/httpclient.md
22 changes: 11 additions & 11 deletions docs/core/extensions/httpclient-factory.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ The typed client is registered as transient with DI. In the preceding code, `Add
> Using typed clients in singleton services can be dangerous. For more information, see the [Avoid Typed clients in singleton services](#avoid-typed-clients-in-singleton-services) section.
> [!NOTE]
> When registering a typed client with the `AddHttpClient<TClient>` method, the `TClient` type must have a constructor that accepts an `HttpClient` parameter. Additionally, the `TClient` type shouldn't be registered with the DI container separately.
> When registering a typed client with the `AddHttpClient<TClient>` method, the `TClient` type must have a constructor that accepts an `HttpClient` as a parameter. Additionally, the `TClient` type shouldn't be registered with the DI container separately, as this will lead to the later registration overwriting the former.
### Generated clients

Expand Down Expand Up @@ -231,10 +231,10 @@ There are several additional configuration options for controlling the `IHttpCli
| <xref:Microsoft.Extensions.DependencyInjection.HttpClientBuilderExtensions.AddHttpMessageHandler%2A> | Adds an additional message handler for a named `HttpClient`. |
| <xref:Microsoft.Extensions.DependencyInjection.HttpClientBuilderExtensions.AddTypedClient%2A> | Configures the binding between the `TClient` and the named `HttpClient` associated with the `IHttpClientBuilder`. |
| <xref:Microsoft.Extensions.DependencyInjection.HttpClientBuilderExtensions.ConfigureHttpClient%2A> | Adds a delegate that will be used to configure a named `HttpClient`. |
| <xref:Microsoft.Extensions.DependencyInjection.HttpClientBuilderExtensions.ConfigureHttpMessageHandlerBuilder%2A> | Adds a delegate that will be used to configure message handlers using <xref:Microsoft.Extensions.Http.HttpMessageHandlerBuilder> for a named `HttpClient`. |
| <xref:Microsoft.Extensions.DependencyInjection.HttpClientBuilderExtensions.ConfigurePrimaryHttpMessageHandler%2A> | Configures the primary `HttpMessageHandler` from the dependency injection container for a named `HttpClient`. |
| <xref:Microsoft.Extensions.DependencyInjection.HttpClientBuilderExtensions.RedactLoggedHeaders%2A> | Sets the collection of HTTP header names for which values should be redacted before logging. |
| <xref:Microsoft.Extensions.DependencyInjection.HttpClientBuilderExtensions.SetHandlerLifetime%2A> | Sets the length of time that a `HttpMessageHandler` instance can be reused. Each named client can have its own configured handler lifetime value. |
| <xref:Microsoft.Extensions.DependencyInjection.HttpClientBuilderExtensions.UseSocketsHttpHandler%2A> | Configures a new or a previously added `SocketsHttpHandler` instance from the dependency injection container to be used as a primary handler for a named `HttpClient`. (.NET 5+ only) |

## Using IHttpClientFactory together with SocketsHttpHandler

Expand All @@ -244,21 +244,19 @@ However, `SocketsHttpHandler` and `IHttpClientFactory` can be used together impr

To use both APIs:

1. Specify `SocketsHttpHandler` as `PrimaryHandler` and set up its `PooledConnectionLifetime` (for example, to a value that was previously in `HandlerLifetime`).
1. As `SocketsHttpHandler` will handle connection pooling and recycling, then handler recycling at the `IHttpClientFactory` level is not needed anymore. You can disable it by setting `HandlerLifetime` to `Timeout.InfiniteTimeSpan`.
1. Specify `SocketsHttpHandler` as `PrimaryHandler` via <xref:Microsoft.Extensions.DependencyInjection.HttpClientBuilderExtensions.ConfigurePrimaryHttpMessageHandler%2A>, or <xref:Microsoft.Extensions.DependencyInjection.HttpClientBuilderExtensions.UseSocketsHttpHandler%2A> (.NET 5+ only).
1. Set up <xref:System.Net.Http.SocketsHttpHandler.PooledConnectionLifetime?displayProperty=nameWithType> based on the interval you expect DNS to be updated; for example, to a value that was previously in `HandlerLifetime`.
1. (Optional) Since `SocketsHttpHandler` will handle connection pooling and recycling, handler recycling at the `IHttpClientFactory` level is no longer needed. You can disable it by setting `HandlerLifetime` to `Timeout.InfiniteTimeSpan`.

```csharp
services.AddHttpClient(name)
.ConfigurePrimaryHttpMessageHandler(() =>
{
return new SocketsHttpHandler()
{
PooledConnectionLifetime = TimeSpan.FromMinutes(2)
};
})
.UseSocketsHttpHandler((handler, _) =>
handler.PooledConnectionLifetime = TimeSpan.FromMinutes(2)) // Recreate connection every 2 minutes
.SetHandlerLifetime(Timeout.InfiniteTimeSpan); // Disable rotation, as it is handled by PooledConnectionLifetime
```

In the example above, 2 minutes were chosen arbitrarily for illustration purposes, aligning to a default `HandlerLifetime` value. You should choose the value based on the expected frequency of DNS or other network changes. For more information, see the [DNS behavior](../../fundamentals/networking/http/httpclient-guidelines.md#dns-behavior) section in the `HttpClient` guidelines, and the Remarks section in the <xref:System.Net.Http.SocketsHttpHandler.PooledConnectionLifetime> API documentation.

## Avoid typed clients in singleton services

When using the _named client_ approach, `IHttpClientFactory` is injected into services, and `HttpClient` instances are created by calling <xref:System.Net.Http.IHttpClientFactory.CreateClient%2A> every time an `HttpClient` is needed.
Expand Down Expand Up @@ -297,6 +295,7 @@ For more information, see the [full example](https://github.com/dotnet/docs/tree

## See also

- [Common `IHttpClientFactory` usage issues][hcf-issues]
- [Dependency injection in .NET][di]
- [Logging in .NET][logging]
- [Configuration in .NET][config]
Expand All @@ -306,6 +305,7 @@ For more information, see the [full example](https://github.com/dotnet/docs/tree
- [Make HTTP requests with the HttpClient][httpclient]
- [Implement HTTP retry with exponential backoff][http-retry]

[hcf-issues]: httpclient-factory-troubleshooting.md
[di]: dependency-injection.md
[logging]: logging.md
[config]: configuration.md
Expand Down
Loading

0 comments on commit af3134d

Please sign in to comment.