Is there an existing issue for this?
Description
The ABP dynamic HTTP client uses HttpCompletionOption.ResponseHeadersRead.
With ResponseHeadersRead, HttpClient returns after receiving response headers. The response content stream remains open until it is consumed or the HttpResponseMessage is disposed.
For ABP-formatted error responses, the response body is consumed by the ABP error handling pipeline, so the connection is released normally.
For non-ABP error responses without the _AbpErrorFormat header, the response body may not be consumed before the client throws. In that case, the underlying connection can remain occupied until the server closes it or the client times out.
This is easy to observe on .NET Framework because the default per-host connection limit is 2, but the same response lifetime rule applies on modern .NET versions as well.
Reproduction Steps
- Create a .NET Framework 4.8 console application.
- Configure an ABP dynamic HTTP client proxy to call an endpoint.
- Make the endpoint, or an intermediary such as a gateway or WAF, return a non-success response without the
_AbpErrorFormat header while keeping the connection alive.
- Keep
ServicePointManager.DefaultConnectionLimit unchanged.
- Call the same ABP client method repeatedly.
Observed on .NET Framework:
- Request 1 receives the error response.
- Request 2 receives the error response.
- Request 3 waits because both per-host connections are still occupied.
- The pattern repeats after the connections are eventually released.
Expected behavior
When ResponseHeadersRead is used, the ABP HTTP client should release the connection on every error path.
For non-success responses without _AbpErrorFormat, the client should drain the response content or dispose the HttpResponseMessage before throwing.
Actual behavior
For non-ABP error responses, the response body may remain unconsumed and the response may not be disposed before the exception leaves the ABP HTTP client pipeline.
The underlying connection can remain occupied until the remote side closes it or a timeout occurs. On .NET Framework this can quickly exhaust the default two per-host connections and cause subsequent requests to block.
Regression?
Unknown.
Known Workarounds
Add a custom DelegatingHandler to dispose non-ABP error responses:
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
var response = await base.SendAsync(request, cancellationToken);
if (response.StatusCode >= HttpStatusCode.BadRequest &&
response.Content is { } content &&
!response.Headers.Contains(AbpHttpConsts.AbpErrorFormat))
{
response.Dispose();
}
return response;
}
Alternatively, drain the response content before throwing for non-ABP error responses.
Version
10.4.0
User Interface
Common (Default)
Database Provider
None/Others
Tiered or separate authentication server
None (Default)
Operation System
Windows (Default)
Other information
The issue was reproduced on .NET Framework 4.8 with the default per-host connection limit. The behavior is consistent with ResponseHeadersRead: the response must be consumed or disposed to release the connection deterministically. Modern .NET versions usually make this harder to reproduce because their connection limits are higher, but disposing or draining the response is still the preferred way to release the connection promptly and avoid holding resources longer than necessary.
Is there an existing issue for this?
Description
The ABP dynamic HTTP client uses
HttpCompletionOption.ResponseHeadersRead.With
ResponseHeadersRead,HttpClientreturns after receiving response headers. The response content stream remains open until it is consumed or theHttpResponseMessageis disposed.For ABP-formatted error responses, the response body is consumed by the ABP error handling pipeline, so the connection is released normally.
For non-ABP error responses without the
_AbpErrorFormatheader, the response body may not be consumed before the client throws. In that case, the underlying connection can remain occupied until the server closes it or the client times out.This is easy to observe on .NET Framework because the default per-host connection limit is 2, but the same response lifetime rule applies on modern .NET versions as well.
Reproduction Steps
_AbpErrorFormatheader while keeping the connection alive.ServicePointManager.DefaultConnectionLimitunchanged.Observed on .NET Framework:
Expected behavior
When
ResponseHeadersReadis used, the ABP HTTP client should release the connection on every error path.For non-success responses without
_AbpErrorFormat, the client should drain the response content or dispose theHttpResponseMessagebefore throwing.Actual behavior
For non-ABP error responses, the response body may remain unconsumed and the response may not be disposed before the exception leaves the ABP HTTP client pipeline.
The underlying connection can remain occupied until the remote side closes it or a timeout occurs. On .NET Framework this can quickly exhaust the default two per-host connections and cause subsequent requests to block.
Regression?
Unknown.
Known Workarounds
Add a custom
DelegatingHandlerto dispose non-ABP error responses:Alternatively, drain the response content before throwing for non-ABP error responses.
Version
10.4.0
User Interface
Common (Default)
Database Provider
None/Others
Tiered or separate authentication server
None (Default)
Operation System
Windows (Default)
Other information
The issue was reproduced on .NET Framework 4.8 with the default per-host connection limit. The behavior is consistent with
ResponseHeadersRead: the response must be consumed or disposed to release the connection deterministically. Modern .NET versions usually make this harder to reproduce because their connection limits are higher, but disposing or draining the response is still the preferred way to release the connection promptly and avoid holding resources longer than necessary.