Skip to content
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

Regression in HTTP/1.1 stress tests: ObjectDisposedException ('SslStream') #56159

Closed
antonfirsov opened this issue Jul 22, 2021 · 29 comments · Fixed by #57287
Closed

Regression in HTTP/1.1 stress tests: ObjectDisposedException ('SslStream') #56159

antonfirsov opened this issue Jul 22, 2021 · 29 comments · Fixed by #57287
Assignees
Milestone

Comments

@antonfirsov
Copy link
Member

antonfirsov commented Jul 22, 2021

Since last Sunday (July 18), every HTTP 1.1 Linux CI stress run has at least one occurrence of the following new failure type:

https://dev.azure.com/dnceng/public/_build/results?buildId=1248255&view=logs&j=0da5d1d9-276d-5173-c4c4-9d4d4ed14fdb&t=45b31e99-b18c-506f-46cf-7d3a4d5140a9

Stack Trace
System.Net.Http.HttpRequestException: An error occurred while sending the request.
client_1  |  ---> System.ObjectDisposedException: Cannot access a disposed object.
client_1  | Object name: 'SslStream'.
client_1  |    at System.Net.Security.SslStream.<ThrowIfExceptional>g__ThrowExceptional|138_0(ExceptionDispatchInfo e) in /_/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.cs:line 849
client_1  |    at System.Net.Security.SslStream.WriteAsync(ReadOnlyMemory`1 buffer, CancellationToken cancellationToken) in /_/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.cs:line 817
client_1  |    at System.Net.Http.HttpConnection.WriteToStreamAsync(ReadOnlyMemory`1 source, Boolean async) in /_/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs:line 1447
client_1  |    at System.Net.Http.HttpConnection.FlushAsync(Boolean async) in /_/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs:line 1422
client_1  |    at System.Net.Http.HttpConnection.SendAsyncCore(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken) in /_/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs:line 503
client_1  |    --- End of inner exception stack trace ---
client_1  |    at System.Net.Http.HttpConnection.SendAsyncCore(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken) in /_/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs:line 817
client_1  |    at System.Net.Http.HttpConnectionPool.SendUsingHttp11Async(HttpRequestMessage request, Boolean async, Boolean doRequestAuth, CancellationToken cancellationToken) in /_/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs:line 981
client_1  |    at System.Net.Http.HttpConnectionPool.DetermineVersionAndSendAsync(HttpRequestMessage request, Boolean async, Boolean doRequestAuth, CancellationToken cancellationToken) in /_/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs:line 1015
client_1  |    at System.Net.Http.HttpConnectionPool.SendAndProcessAltSvcAsync(HttpRequestMessage request, Boolean async, Boolean doRequestAuth, CancellationToken cancellationToken) in /_/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs:line 1028
client_1  |    at System.Net.Http.HttpConnectionPool.SendWithRetryAsync(HttpRequestMessage request, Boolean async, Boolean doRequestAuth, CancellationToken cancellationToken) in /_/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs:line 1038
client_1  |    at System.Net.Http.RedirectHandler.SendAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken) in /_/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/RedirectHandler.cs:line 30
client_1  |    at System.Net.Http.HttpClient.<SendAsync>g__Core|83_0(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationTokenSource cts, Boolean disposeCts, CancellationTokenSource pendingRequestsCts, CancellationToken originalCancellationToken) in /_/src/libraries/System.Net.Http/src/System/Net/Http/HttpClient.cs:line 553
client_1  |    at HttpStress.RequestContext.SendAsync(HttpRequestMessage request, HttpCompletionOption httpCompletion, Nullable`1 token) in /app/ClientOperations.cs:line 95
client_1  |    at HttpStress.ClientOperations.<>c.<<get_Operations>b__1_4>d.MoveNext() in /app/ClientOperations.cs:line 349
client_1  | --- End of stack trace from previous location ---
client_1  |    at HttpStress.StressClient.<>c__DisplayClass17_0.<<StartCore>g__RunWorker|0>d.MoveNext() in /app/StressClient.cs:line 204

After a quick naive look at recently merged System.Net.Http and System.Net.Security PR-s, #55772 seems to be the only one that matches the timing. Http PRs seem to be either test fixes, or unrelated to HTTP/ 1.1

/cc @wfurt @Tratcher @geoffkizer @alnikola

Edit 1: Less frequently, but it also seems to happen on Windows.

Edit 2: First occurrence seems to be on 7/15

@dotnet-issue-labeler dotnet-issue-labeler bot added area-System.Net.Http untriaged New issue has not been triaged by the area owner labels Jul 22, 2021
@ghost
Copy link

ghost commented Jul 22, 2021

Tagging subscribers to this area: @dotnet/ncl
See info in area-owners.md if you want to be subscribed.

Issue Details

Since last Sunday (July 18), every HTTP 1.1 Linux CI stress run has at least one occurrence of the following failure:

https://dev.azure.com/dnceng/public/_build/results?buildId=1248255&view=logs&j=0da5d1d9-276d-5173-c4c4-9d4d4ed14fdb&t=45b31e99-b18c-506f-46cf-7d3a4d5140a9

Stack Trace
System.Net.Http.HttpRequestException: An error occurred while sending the request.
client_1  |  ---> System.ObjectDisposedException: Cannot access a disposed object.
client_1  | Object name: 'SslStream'.
client_1  |    at System.Net.Security.SslStream.<ThrowIfExceptional>g__ThrowExceptional|138_0(ExceptionDispatchInfo e) in /_/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.cs:line 849
client_1  |    at System.Net.Security.SslStream.WriteAsync(ReadOnlyMemory`1 buffer, CancellationToken cancellationToken) in /_/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.cs:line 817
client_1  |    at System.Net.Http.HttpConnection.WriteToStreamAsync(ReadOnlyMemory`1 source, Boolean async) in /_/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs:line 1447
client_1  |    at System.Net.Http.HttpConnection.FlushAsync(Boolean async) in /_/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs:line 1422
client_1  |    at System.Net.Http.HttpConnection.SendAsyncCore(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken) in /_/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs:line 503
client_1  |    --- End of inner exception stack trace ---
client_1  |    at System.Net.Http.HttpConnection.SendAsyncCore(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken) in /_/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs:line 817
client_1  |    at System.Net.Http.HttpConnectionPool.SendUsingHttp11Async(HttpRequestMessage request, Boolean async, Boolean doRequestAuth, CancellationToken cancellationToken) in /_/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs:line 981
client_1  |    at System.Net.Http.HttpConnectionPool.DetermineVersionAndSendAsync(HttpRequestMessage request, Boolean async, Boolean doRequestAuth, CancellationToken cancellationToken) in /_/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs:line 1015
client_1  |    at System.Net.Http.HttpConnectionPool.SendAndProcessAltSvcAsync(HttpRequestMessage request, Boolean async, Boolean doRequestAuth, CancellationToken cancellationToken) in /_/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs:line 1028
client_1  |    at System.Net.Http.HttpConnectionPool.SendWithRetryAsync(HttpRequestMessage request, Boolean async, Boolean doRequestAuth, CancellationToken cancellationToken) in /_/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs:line 1038
client_1  |    at System.Net.Http.RedirectHandler.SendAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken) in /_/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/RedirectHandler.cs:line 30
client_1  |    at System.Net.Http.HttpClient.<SendAsync>g__Core|83_0(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationTokenSource cts, Boolean disposeCts, CancellationTokenSource pendingRequestsCts, CancellationToken originalCancellationToken) in /_/src/libraries/System.Net.Http/src/System/Net/Http/HttpClient.cs:line 553
client_1  |    at HttpStress.RequestContext.SendAsync(HttpRequestMessage request, HttpCompletionOption httpCompletion, Nullable`1 token) in /app/ClientOperations.cs:line 95
client_1  |    at HttpStress.ClientOperations.<>c.<<get_Operations>b__1_4>d.MoveNext() in /app/ClientOperations.cs:line 349
client_1  | --- End of stack trace from previous location ---
client_1  |    at HttpStress.StressClient.<>c__DisplayClass17_0.<<StartCore>g__RunWorker|0>d.MoveNext() in /app/StressClient.cs:line 204

After a quick naive look at recently merged System.Net.Http and System.Net.Security PR-s, #55772 seems to be the only one that matches the timing. Http PRs seem to be all test fixes, or unrelated to HTTP/ 1.1

/cc @wfurt @Tratcher @geoffkizer @alnikola

Author: antonfirsov
Assignees: -
Labels:

area-System.Net.Http, untriaged

Milestone: -

@karelz karelz added this to the 6.0.0 milestone Jul 22, 2021
@karelz karelz removed the untriaged New issue has not been triaged by the area owner label Jul 22, 2021
@wfurt
Copy link
Member

wfurt commented Jul 22, 2021

This was also reported from field and happens rarely. I don't think it is recent regression.

@antonfirsov
Copy link
Member Author

antonfirsov commented Jul 22, 2021

Maybe it's not entirely new, but we haven't seen it in the stress runs before, and since 7/18 it suddenly started to impact literally every stress run.

@antonfirsov
Copy link
Member Author

@MattGal are you aware of any recent changes around BuildPool.Ubuntu.1804.Amd64.Open?

@alnikola
Copy link
Contributor

alnikola commented Aug 3, 2021

The issue seems to be caused by a lack of HttpConnection._disposed check on the code path starting from assigning an HttpConnection to the next request in HttpConnectionPool.ReturnHttp11Connection, down to HttpConnection.SendAsyncCore writing data to SslStream. The lack of this check makes possible an attempt to write to a disposed stream. Specifically, this issue manifests itself in Get Aborted stress scenario where the server aborts the connection on each request. Thus, if there is more than 1 request in the HttpConnectionPool._http11RequestQueue (which is true for stress tests), HttpConnectionPool can try to send a request over an aborted connection.

Having investigated code further, I realized that the above theory is wrong because ReturnHttp11Connection is called for a new connection which hasn't been used yet.

@alnikola alnikola assigned alnikola and unassigned alnikola Aug 3, 2021
@geoffkizer
Copy link
Contributor

In general we shouldn't be calling ReturnHttp11Connection if the connection is already disposed. Do you know when this is happening?

@alnikola
Copy link
Contributor

alnikola commented Aug 3, 2021

@geoffkizer I realized that my theory is wrong.

@geoffkizer
Copy link
Contributor

Ok. Let me know if I can help.

@antonfirsov
Copy link
Member Author

No idea why did this error start surfacing only now, but looks like the problem is that we are not passing cancellationToken to FlushAsync. Two places from the CI stack traces, but there are more:

In rare cases when HttpConnection disposal is faster in it's race with SendAsyncCore code, we end up calling into an already disposed stream, throwing HttpRequestExcetption with inner ObjectDisposedException instead of OperationCanceledException.

@antonfirsov
Copy link
Member Author

Looks like SslStream will still throw ObjectDisposedException if the stream is disposed, even if we pass a pre-cancelled token.

@geoffkizer @ManickaP shouldn't we check our tokens manually in a very aggressive manner? I'm surprised we don't have more issues around cancellation.

@wfurt
Copy link
Member

wfurt commented Aug 6, 2021

From the trace, we are trying to WriteAsync. So the question is why do we do that on disposed Stream.
It feels like we should either swallow the exception if we intentionally despised the stream e.g. Disposed/Cancellation/error or we should delay disposal if we have pending operations.

@ManickaP
Copy link
Member

ManickaP commented Aug 9, 2021

That we don't pass in cancellationToken is normal, we register to it and tear down the connection if it fires:

CancellationTokenRegistration cancellationRegistration = RegisterCancellation(cancellationToken);

But we should be then able to recognize the exception coming from the cancellation and properly map it:
if (MapSendException(error, cancellationToken, out Exception mappedException))
{
throw mappedException;
}

I've seen something similar last year when investigating stress and fixing it: #41800 but it manifested only in 100-continue.

Also on 15.7. the big request queuing change was merged: #53851. It doesn't seem to touch the relevant code but it might have changed the timing and surface a race here.

Interestingly, the failed calls in HTTP stress are not cancelled:

client_1  | 	 4: GET Aborted               Fail: 1	Timestamps: 14:51:34.3754360, Duration: 00:00:00.0035326, Cancelled: False
client_1  | 	 5: POST                      Fail: 2	Timestamps: 14:54:22.4029921, Duration: 00:00:00.0012125, Cancelled: False, Timestamps: 15:03:44.4705660, Duration: 00:00:00.0015315, Cancelled: False
client_1  | 	    TOTAL                     Fail: 3

What lead you to believe this is caused by cancellation?

@antonfirsov
Copy link
Member Author

client_250.log

Line 1746 says:

15:11:23.4917562[HandlerMessage] poolId: 3429838, workerId: 49526251, requestId: 11394055, memberName: RegisterCancellation, message: Cancellation requested. Disposing of the connection.

Line 1839 is where the exception happens:

15:11:23.4920609[HandlerMessage] poolId: 3429838, workerId: 49526251, requestId: 11394055, memberName: SendAsyncCore, message: Error sending request: System.ObjectDisposedException: Cannot access a disposed object.

No idea why doesn't the HttpStress code recognize that the request has been cancelled.

@ManickaP
Copy link
Member

ManickaP commented Aug 9, 2021

Are you 100% sure those log messages belong to the errors reported by the stress test?
If the cancellation was triggered ('Cancellation requested. Disposing of the connection.') the token would be cancelled and MapSendException would map it to OCE.

If you're able to repro it locally, could you try to run it with cancellation rate 0?

@antonfirsov
Copy link
Member Author

@ManickaP you are right the lines in my #56159 (comment) have nothing to do with the error, SslStream must be disposed some other way, will keep investigating.

@antonfirsov
Copy link
Member Author

antonfirsov commented Aug 10, 2021

This is likely a regression from #53851. Looks like Dispose (triggered by cancelling the request) somehow ends up being in a race with ReturnHttp11Connection(isNewConnection=False).

client_092-partial.log

The relevant events:

14:46:55.7191483[HandlerMessage] poolId: 3429838, workerId: 51853754, requestId: 8327141, memberName: SendAsyncCore, message: Request is fully sent.
14:46:55.7191551[HandlerMessage] poolId: 3429838, workerId: 51853754, requestId: 8327141, memberName: SendAsyncCore, message: Received response: StatusCode: 200, ReasonPhrase: 'OK', Version: 1.1, Content: System.Net.Http.HttpConnectionResponseContent, Headers:
{
  Date: Mon, 09 Aug 2021 14:46:55 GMT
  Server: Kestrel
  Transfer-Encoding: chunked
}
14:46:55.7195325[HandlerMessage] poolId: 3429838, workerId: 51853754, requestId: 8327141, memberName: FillAsync, message: Received 2874 bytes.
14:46:55.7195965[HandlerMessage] poolId: 3429838, workerId: 51853754, requestId: 8327141, memberName: RegisterCancellation, message: Cancellation requested. Disposing of the connection.
14:46:55.7195989[HandlerMessage] poolId: 3429838, workerId: 51853754, requestId: 8327141, memberName: FillAsync, message: Received 11 bytes.
14:46:55.7196017[HandlerMessage] poolId: 3429838, workerId: 51853754, requestId: 8327141, memberName: Dispose, message: Connection closing.
14:46:55.7199270[HandlerMessage] poolId: 3429838, workerId: 51853754, requestId: 0, memberName: ReturnHttp11Connection, message: isNewConnection=False
14:46:55.7199322[HandlerMessage] poolId: 3429838, workerId: 51853754, requestId: 0, memberName: ReturnHttp11Connection, message: Dequeued waiting HTTP/1.1 request.
14:46:55.7199344[HandlerMessage] poolId: 3429838, workerId: 51853754, requestId: 0, memberName: DrainOnDisposeAsync, message: Connection drain succeeded
14:46:55.7199507[HandlerMessage] poolId: 3429838, workerId: 51853754, requestId: 54389309, memberName: SendAsyncCore, message: Sending request: Method: POST, RequestUri: 'https://localhost:5001/', Version: 1.1, Content: HttpStress.ClientOperations+StringDuplexContent, Headers:
{
  Content-Length: 35
}

The connection 51853754 is being disposed by the previous request (8327141), then passed to the next (failing) request 54389309 through _http11RequestQueue.

Note: the request 8327141 is a GET Slow where the server is pushing down the stream char-by char.

@geoffkizer my diagnosis would be that although CancellationTokenRegistration should not be dead long time before ReturnHttp11Connection is invoked, that doesn't prevent an already fired callback to finish and race with the request completion code (including ReturnHttp11Connection). But I may be terribly wrong, any other ideas?

Also: why do we see log lines from FillAsync, when a previous line reported that the response has been received for that request?

@geoffkizer
Copy link
Contributor

What cancellation token is firing specifically, and what action is it taking?

@MattGal
Copy link
Member

MattGal commented Aug 10, 2021

@MattGal are you aware of any recent changes around BuildPool.Ubuntu.1804.Amd64.Open?

Apologies, I didn't see the original tag until Geoff commented. Nothing interesting has changed on that image in some time, sounds like the right conversations are happening though.

@geoffkizer
Copy link
Contributor

@geoffkizer my diagnosis would be that although CancellationTokenRegistration should not be dead long time before ReturnHttp11Connection is invoked, that doesn't prevent an already fired callback to finish and race with the request completion code (including ReturnHttp11Connection). But I may be terribly wrong, any other ideas?

We are disposing the cancellation registration on line 706, before we create the response stream and start to process it.

Dispose on the cancellation registration will not return until the cancellation registration is either cancelled without firing, or until the cancellation callback we give it completes. In other words, by the time the Dispose returns, the cancellation callback can't be running anymore -- it's either already run and completed, or never will.

@geoffkizer
Copy link
Contributor

Also: why do we see log lines from FillAsync, when a previous line reported that the response has been received for that request?

FillAsync is also used while reading the response body.

RegisterCancellation is called from a bunch of places, specifically when reading the response body. So I'm assuming that one of the response body reads is getting cancelled, which is disposing the connection; yet somehow we are still returning the connection to the pool? Weird.

It certainly seems likely that my connection pooling PR introduced this somehow, but I'm not sure how...

@geoffkizer
Copy link
Contributor

It's very interesting that DrainOnDisposeAsync is getting called. This indicates that the response stream is getting disposed, and we think it's still valid so we are trying to drain it so we can continue to use the connection.

It seems like perhaps there's a race here between the cancellation token for a read operation firing and the stream's Dispose logic?

I'm not sure how my PR would have caused this, though...

@geoffkizer
Copy link
Contributor

Note also that the drain happens asynchronously... if there is still an outstanding read at this point, that would be really bad.

What does the test code here look like? Is it possible that the stream is getting Disposed while there is still an outstanding read operation?

@antonfirsov
Copy link
Member Author

What does the test code here look like?

("GET Slow",
async ctx =>
{
using var req = new HttpRequestMessage(HttpMethod.Get, "/slow");
int expectedLength = ctx.SetExpectedResponseContentLengthHeader(req.Headers);
using HttpResponseMessage m = await ctx.SendAsync(req);
ValidateStatusCode(m);
ValidateServerContent(await m.Content.ReadAsStringAsync(), expectedLength);
}),

Where SendAsync most likely triggers this path and httpCompletion = HttpCompletionOption.ResponseContentRead:

// trigger a random cancellation
using var cts = CancellationTokenSource.CreateLinkedTokenSource(_globalToken);
Task<HttpResponseMessage> task = _client.SendAsync(request, httpCompletion, cts.Token);
// either spinwait or delay before triggering cancellation
if (GetRandomBoolean(probability: 0.66))
{
// bound spinning to 100 us
double spinTimeMs = 0.1 * _random.NextDouble();
Stopwatch sw = Stopwatch.StartNew();
do { Thread.SpinWait(10); } while (!task.IsCompleted && sw.Elapsed.TotalMilliseconds < spinTimeMs);
}
else
{
// 60ms is the 99th percentile when
// running the stress suite locally under default load
await Task.WhenAny(task, Task.Delay(_random.Next(0, 60), cts.Token));
}
cts.Cancel();
IsCancellationRequested = true;
return WithVersionValidation(await task);

Will add some more logging to get a proof, and more details about the call stack.

@kedia990
Copy link

I wanted to quickly chime in here to note that we're facing this (or at least a very similar variant of this) issue with a production service in our organization. This became apparent during our efforts to migrate our applications which depend on said service to .NET Core / PowerShell Core (which have now stalled; with any luck, a fix for this will ship with .NET 6). Here are the characteristics of the issue (relevant stack traces provided at the end):

  • It's consistently reproducible against the service in question. API calls fail 100% of the time with ObjectDisposedException.
  • The service uses HTTP/1.1 and Kerberos authentication. Hitting an unauthenticated endpoint on the service works fine, but most endpoints it serves require authentication.
  • TLS doesn't appear to be a factor. Hitting the HTTPS endpoint results in an ODE from SslStream; hitting HTTP results in an ODE from NetworkStream. Both stack traces have been provided below.
  • This issue is confined to SocketsHttpHandler; switching to WinHttpHandler (in .NET Core 3.1, since the knob to toggle it has been removed in .NET 5) fixes the issue.
  • This issue affects .NET 5, .NET Core 3.1 and .NET Core 2.1 in our environment. It does not affect .NET Framework (obviously).

Having reviewed the discussion so far, I realize that there are a couple of differences - (a) authentication doesn't seem to feature in your discussions / stack trace, and (b) the timing of #53851, as a candidate for the cause, wouldn't explain this for us if we're able to repro the issue as far back as .NET Core 2.1.

That said, given that the manifestation of the bug appears to be identical and we have a reliable reproducer, I figured I'd provide the data point here. If there's anything I can try on our end / do to help debug this issue, I'm happy to help (and probably learn a little something too, perhaps). (OTOH, if you think this is completely unrelated and we should be tracking this elsewhere, I'll stop bothering you folks here :) .)

Expand for Stack Traces
PS C:\Users\redact> $httpClient.GetStringAsync('https://redact').GetAwaiter().GetResult()
MethodInvocationException: Exception calling "GetResult" with "0" argument(s): "An error occurred while sending the request."

PS C:\Users\redact> $Error[0].Exception.ToString()
System.Management.Automation.MethodInvocationException: Exception calling "GetResult" with "0" argument(s): "An error occurred while sending the request."
 ---> System.Net.Http.HttpRequestException: An error occurred while sending the request.
 ---> System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'SslStream'.
   at System.Net.Security.SslStream.<ThrowIfExceptional>g__ThrowExceptional|137_0(ExceptionDispatchInfo e)
   at System.Net.Security.SslStream.WriteAsync(ReadOnlyMemory`1 buffer, CancellationToken cancellationToken)
   at System.Net.Http.HttpConnection.WriteToStreamAsync(ReadOnlyMemory`1 source, Boolean async)
   at System.Net.Http.HttpConnection.FlushAsync(Boolean async)
   at System.Net.Http.HttpConnection.SendAsyncCore(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
   --- End of inner exception stack trace ---
   at System.Net.Http.HttpConnection.SendAsyncCore(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
   at System.Net.Http.AuthenticationHelper.SendWithNtAuthAsync(HttpRequestMessage request, Uri authUri, Boolean async, ICredentials credentials, Boolean isProxyAuth, HttpConnection connection, HttpConnectionPool connectionPool, CancellationToken cancellationToken)
   at System.Net.Http.HttpConnectionPool.SendWithRetryAsync(HttpRequestMessage request, Boolean async, Boolean doRequestAuth, CancellationToken cancellationToken)
   at System.Net.Http.AuthenticationHelper.SendWithAuthAsync(HttpRequestMessage request, Uri authUri, Boolean async, ICredentials credentials, Boolean preAuthenticate, Boolean isProxyAuth, Boolean doRequestAuth, HttpConnectionPool pool, CancellationToken cancellationToken)
   at System.Net.Http.RedirectHandler.SendAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
   at System.Net.Http.HttpClient.SendAsyncCore(HttpRequestMessage request, HttpCompletionOption completionOption, Boolean async, Boolean emitTelemetryStartStop, CancellationToken cancellationToken)
   at System.Net.Http.HttpClient.GetStringAsyncCore(HttpRequestMessage request, CancellationToken cancellationToken)
   at CallSite.Target(Closure , CallSite , Object )
   --- End of inner exception stack trace ---
   ...

PS C:\Users\redact> $httpClient.GetStringAsync('http://redact').GetAwaiter().GetResult()
MethodInvocationException: Exception calling "GetResult" with "0" argument(s): "An error occurred while sending the request."

PS C:\Users\redact> $Error[0].Exception.ToString()
System.Management.Automation.MethodInvocationException: Exception calling "GetResult" with "0" argument(s): "An error occurred while sending the request."
 ---> System.Net.Http.HttpRequestException: An error occurred while sending the request.
 ---> System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'System.Net.Sockets.NetworkStream'.
   at System.Net.Sockets.NetworkStream.<ThrowIfDisposed>g__ThrowObjectDisposedException|63_0()
   at System.Net.Sockets.NetworkStream.WriteAsync(ReadOnlyMemory`1 buffer, CancellationToken cancellationToken)
   at System.Net.Http.HttpConnection.WriteToStreamAsync(ReadOnlyMemory`1 source, Boolean async)
   at System.Net.Http.HttpConnection.FlushAsync(Boolean async)
   at System.Net.Http.HttpConnection.SendAsyncCore(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
   --- End of inner exception stack trace ---
   at System.Net.Http.HttpConnection.SendAsyncCore(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
   at System.Net.Http.AuthenticationHelper.SendWithNtAuthAsync(HttpRequestMessage request, Uri authUri, Boolean async, ICredentials credentials, Boolean isProxyAuth, HttpConnection connection, HttpConnectionPool connectionPool, CancellationToken cancellationToken)
   at System.Net.Http.HttpConnectionPool.SendWithRetryAsync(HttpRequestMessage request, Boolean async, Boolean doRequestAuth, CancellationToken cancellationToken)
   at System.Net.Http.AuthenticationHelper.SendWithAuthAsync(HttpRequestMessage request, Uri authUri, Boolean async, ICredentials credentials, Boolean preAuthenticate, Boolean isProxyAuth, Boolean doRequestAuth, HttpConnectionPool pool, CancellationToken cancellationToken)
   at System.Net.Http.RedirectHandler.SendAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
   at System.Net.Http.HttpClient.SendAsyncCore(HttpRequestMessage request, HttpCompletionOption completionOption, Boolean async, Boolean emitTelemetryStartStop, CancellationToken cancellationToken)
   at System.Net.Http.HttpClient.GetStringAsyncCore(HttpRequestMessage request, CancellationToken cancellationToken)
   at CallSite.Target(Closure , CallSite , Object )
   --- End of inner exception stack trace ---
   ...

@ManickaP
Copy link
Member

ManickaP commented Aug 11, 2021

Hi @kedia990, yeah I think the cause of your problem will be very different, what we track here is a recent regression in 6.0 preview versions. Could you migrate your comment to a new issue? We can continue discussion from there.
Also if you have a repro, it would be great if you shared it, so we can look at it a test it on our own.

@antonfirsov
Copy link
Member Author

@ManickaP @geoffkizer I managed to dig out more details. Cancellation isn't happenning during HttpConnection.SendAsyncCore() but later, during a call to response.Content.LoadIntoBufferAsync() in HttpClient.SendAsync

Here are the series of events:

@geoffkizer
Copy link
Contributor

In the meanwhile ReadChunkFromConnectionBuffer calls _connection.CompleteResponse() which returns the disposed connection to the pool

Yes, but right before we call CompleteResponse, we dispose the CT registration and check if the CT has been cancelled. So if cancellation has happened, this latter check should succeed and cause us to throw OCE here.

@geoffkizer
Copy link
Contributor

@stephentoub Any thoughts here? It looks like somehow the connection is getting disposed by the cancellation registration callback, but also getting returned to the pool via CompleteResponse...

@antonfirsov
Copy link
Member Author

antonfirsov commented Aug 12, 2021

This is caused by draining triggered on ChunkedEncodingReadStream.Dispose:

Note that it's very common that DrainOnDisposeAsync starts on a disposed connection. To me it looks like the only criteria is that cancellation should kick in outside of the ReadChunkFromConnectionBuffer method that nulls out _connection in case of cancellation. This seems to be already incorrect behavior, and it looks to me that it's been around for a while, and might possibly explain #56159 (comment) and #56159 (comment). It just rarely ends up returning the connection to the pool for some reason. #53851 or some other change probably made it more likely to occur.

@ghost ghost added the in-pr There is an active PR which will close this issue when it is merged label Aug 12, 2021
@karelz karelz added the bug label Aug 16, 2021
@ghost ghost removed the in-pr There is an active PR which will close this issue when it is merged label Aug 16, 2021
@ghost ghost locked as resolved and limited conversation to collaborators Sep 15, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants