Skip to content

[BUG] ObjectDisposedException when mocking HttpResponseMessage content via a System.Net.Http.DelegatingHandler on the underlying HTTP client. #38505

Open
@kalbert312

Description

@kalbert312

Library name and version

Azure.ResourceManager.Resources 1.6.0, Azure.ResourceManager 1.7.0, Azure.Core 1.34.0

Describe the bug

I'm encountering an ObjectDisposedException in our unit test environment that uses a DelegatingHandler to mock out response content. See the repro steps for relevant code snippets.

After debugging, I discovered the following:

  • In Azure.Core.Pipeline.HttpClientTransport::ProcessAsync(HttpMessage message, bool async), the StringContent gets read as a MemoryStream and is passed to the SDK's response abstraction PipelineResponse which is an IDisposable that when disposed, will dispose the underlying HttpResponseMessage and its content.
  • In Azure.Core.Pipeline.ResponseBodyPolicy, if the content stream is a non-seekable stream and message.BufferResponse is true, a setter message.Response.ContentStream = bufferedStream is called. This invokes the overridden setter in Azure.Core.Pipeline.PipelineResponse which nulls the Content on HttpResponseMessage
    • This step does not occur in the unit test setup because responseContentStream.CanSeek is true for a MemoryStream
  • In Azure.ResourceManager.Resources.ArmDeploymentResource::UpdateAsync(...) the HttpMessage is disposed after CreateOrUpdateAtScopeAsync is done, which will dispose the StringContent on the response since it was not nulled by the previous point.
  • The SDK then wraps the response with a ResourcesArmOperation which leads to the disposed exception.

Expected behavior

Be able to mock a long running operation's intermediate response at the HTTP layer via DelegatingHandler.

Actual behavior

System.ObjectDisposedException: Cannot access a closed Stream.
   at System.IO.__Error.StreamIsClosed()
   at System.IO.MemoryStream.get_Length()
   at Azure.Core.NextLinkOperationImplementation.IsFinalState(Response response, HeaderSource headerSource, Nullable`1& failureState, String& resourceLocation)
   at Azure.Core.NextLinkOperationImplementation.Create(HttpPipeline pipeline, RequestMethod requestMethod, Uri startRequestUri, Response response, OperationFinalStateVia finalStateVia, Boolean skipApiVersionOverride, String apiVersionOverrideValue)
   at Azure.Core.NextLinkOperationImplementation.Create[T](IOperationSource`1 operationSource, HttpPipeline pipeline, RequestMethod requestMethod, Uri startRequestUri, Response response, OperationFinalStateVia finalStateVia, Boolean skipApiVersionOverride, String apiVersionOverrideValue)
   at Azure.ResourceManager.Resources.ResourcesArmOperation`1..ctor(IOperationSource`1 source, ClientDiagnostics clientDiagnostics, HttpPipeline pipeline, Request request, Response response, OperationFinalStateVia finalStateVia, Boolean skipApiVersionOverride, String apiVersionOverrideValue)
   at Azure.ResourceManager.Resources.ArmDeploymentResource.<UpdateAsync>d__20.MoveNext()

Reproduction Steps

The mock delegating handler:

public class MockRequestHandler : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        // ...

        var response = new HttpResponseMessage();
        response.Content = new StringContent(/* mock json */);

        // it does NOT call base.SendAsync()

        // ...

        return Task.FromResult(response);
    }

The http client:

var webRequestHandler = new WebRequestHandler { AllowAutoRedirect = false };

var httpClient = new HttpClient(
    handler: HttpClientFactory.CreatePipeline(innerHandler: webRequestHandler, handlers: delegatingHandlers), // mock handler goes here
    disposeHandler: true);

The ArmClient is initialized as follows:

var armClientOptions = new ArmClientOptions
{
  // ...
  Transport = new HttpClientTransport(httpClient) // http client with the delegating handler
};

var armClient = new ArmClient(tokenCredential, default, armClientOptions);

The ArmClient call:

var armDeploymentSdkResource = armClient.GetArmDeploymentResource(/* ResourceIdentifier */);
var deploymentOperation = await armDeploymentSdkResource
    .UpdateAsync(WaitUntil.Started, deploymentRequestInput, this.CancellationToken);

Environment

Windows 11
System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription = ".NET Framework 4.8.9167.0"
JetBrains Rider 2023.2.1

Metadata

Metadata

Assignees

Labels

ARM - Corecustomer-reportedIssues that are reported by GitHub users external to the Azure organization.needs-team-attentionWorkflow: This issue needs attention from Azure service team or SDK teamquestionThe issue doesn't require a change to the product in order to be resolved. Most issues start as that

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions