refit icon indicating copy to clipboard operation
refit copied to clipboard

[Bug]: HttpClients not disposed

Open Timovzl opened this issue 1 year ago • 9 comments

Describe the bug 🐞

It appears that the source-generated clients receive ownership of an injected HttpClient, which they do not dispose. In fact, these clients do not implement IDisposable at all. HttpClient is IDisposable, however.

Should HttpClient always be disposed? No, not if it originated from the DI container, which is then responsible for its lifetime. In that case, it must not be disposed. But in the scenario where the code just created a new HttpClient and gave it to the source-generated client to own, then that client becomes responsible for the disposal.

Especially with the generated clients currently being registered with transient lifetime, a lot of undisposed HttpClient instances can be accumulated.

Step to reproduce

  • Call services.AddRefitClient<IWhatever>().
  • Use Go To Implementation (usually Ctrl+F12) to view the source-generated implementation.
  • Observe how the type is not IDisposable, and yet it injects a disposable resource, the HttpClient.
  • See also how the generated client is registered with transient lifetime and is fed a brand new HttpClient that is otherwise abandoned.

Reproduction repository

https://github.com/reactiveui/refit

Expected behavior

Preferably, an HttpClient not owned by the container should not be injected (see #1646).

Alternatively, an injected HttpClient not owned by anything else should become owned by the generated client, requiring it to implement IDisposable to dispose of the resource. This would solve the problem when parent services or the generated client have scoped lifetime, but not for transient parents! A transient parent that is IDisposable needs to be disposed by the caller. Not only is this often overlooked, but it is also hard to do if the interface being implemented itself does not implement IDisposable. This shows why the former solution is far, far preferable.

As such, solving #1646 appropriately may make the current issue obsolete. However, the issue seemed significant and complex enough to warrant a separate ticket.

Workaround

Edit: @derekm has found that if your client interface implements IDisposable, then the source-generated client will implement it and use it to dispose the HttpClient.

Screenshots 🖼️

No response

IDE

No response

Operating system

No response

Version

No response

Device

No response

Refit Version

No response

Additional information ℹ️

No response

Timovzl avatar Jan 30 '24 17:01 Timovzl

My understanding is that HttpClients created by IHttpClientFactory don't need to be disposed explicitly:

Disposal of the client isn't required. Disposal cancels outgoing requests and guarantees the given HttpClient instance can't be used after calling Dispose. IHttpClientFactory tracks and disposes resources used by HttpClient instances. The HttpClient instances can generally be treated as .NET objects not requiring disposal.

Source: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-2.1#httpclient-and-lifetime-management-3

Learn about using the IHttpClientFactory interface to manage logical HttpClient instances in ASP.NET Core.

velvolue avatar Nov 07 '24 12:11 velvolue

As @Timovzl wrote:

Should HttpClient always be disposed? No, not if it originated from the DI container, which is then responsible for its lifetime. In that case, it must not be disposed. But in the scenario where the code just created a new HttpClient and gave it to the source-generated client to own, then that client becomes responsible for the disposal.

Especially with the generated clients currently being registered with transient lifetime, a lot of undisposed HttpClient instances can be accumulated.

This is very true.

mikhail-barg avatar Nov 14 '24 10:11 mikhail-barg

Yes, if Refit did new HttpClient(), I totally agree it would need explicit disposal, but Refit uses IHttpClientFactory and as such is not required to dispose the client. Please correct me if I'm wrong, but I haven't seen any documentation that claims otherwise.

velvolue avatar Nov 14 '24 11:11 velvolue

@velvolue Based on the docs you linked, it turns out disposal is not strictly necessary in the current implementation.

Note that is still the most sensible thing to do: ensuring that something takes responsibility for a disposable resource.

More importantly, the client that gets the injected client cannot know how that client was obtained. That is an implementation detail of another class. The reasons based on which we might currently say “it’s okay” may not hold in the future, and then it would be very, very easy to overlook the lack of ownership being taken over the injected client.

Timovzl avatar Nov 14 '24 12:11 Timovzl

Yes, if Refit did new HttpClient(), I totally agree it would need explicit disposal, but Refit uses IHttpClientFactory and as such is not required to dispose the client. Please correct me if I'm wrong, but I haven't seen any documentation that claims otherwise.

But Refit does new HttpClient:() https://github.com/reactiveui/refit/blob/f8bf4bb72a19800f0968dce608d8c62935f02b6c/Refit/RestService.cs#L172

mikhail-barg avatar Nov 14 '24 12:11 mikhail-barg

@Timovzl Sure, I get your point - it wouldn't hurt. But still, it's not a bug per se in the current implementation.

@mikhail-barg That is something else. The Refit client registered as a transient service is fed an HttpClient from IHttpClientFactory and thus does not create a new instance itself. It's a different overload.

velvolue avatar Nov 14 '24 12:11 velvolue

@Timovzl -- If your client interface extends IDisposable, then InterfaceStubGenerator & Parser will detected the Dispose method interface (Parser.cs lines 290, 343 & 364) and Emitter will generate the Dispose() method to dispose the generated class's Client property (Emitter.cs lines 136 & 239).

The fix to close this bug: Docs should be updated to say that RestService users SHOULD have their client interfaces extend IDisposable.

derekm avatar Dec 02 '24 23:12 derekm

@derekm While updating the docs, it wouldn't hurt with a big fat warning in the "Using HttpClientFactory" section about injecting Refit clients (or any typed HttpClient for that matter) into singleton services, as that's an anti-pattern to be aware of.

velvolue avatar Dec 03 '24 06:12 velvolue

[...] it wouldn't hurt with a big fat warning in the "Using HttpClientFactory" section about injecting Refit clients (or any typed HttpClient for that matter) into singleton services, as that's an anti-pattern to be aware of.

The fix I've suggested here would resolve that.

Basically, unless Refit chooses to provide and recommend an IRefitClientFactory, injecting an IRefitClient into a singleton service shouldn't be an antipattern. (I've worked around the issue by feeding Refit my own long-lived HttpClient, around a SocketsHttpHandler with a relatively short PooledConnectionLifetime. This is one of the proper options from the recommended practices.)

Timovzl avatar May 04 '25 17:05 Timovzl