aspnet / BasicMiddleware

[Archived] Basic middleware components for ASP.NET Core. Project moved to https://github.com/aspnet/AspNetCore
Apache License 2.0
169 stars 84 forks source link

Can not access a closed Stream on middleware when await Response.WriteAsync #307

Closed ChaosEngine closed 6 years ago

ChaosEngine commented 6 years ago

Title

Unhandled Exception: System.InvalidOperationException: Can not access a closed Stream. happens when executing

await response.WriteAsync(some_html, Encoding.UTF8);

on net461 target framework, netcoreapp2.0 proves to work fine

Functional impact

IIS Express and kestrel crashes (process exists) when running simple middleware app on net461. Does not happen on netcoreapp2.0

Minimal repro steps

  1. Add Microsoft.AspNetCore.ResponseCompression in the pupeline of web app
  2. Create middlware with await Response.WriteAsync(...)
  3. enable net4xx target framweork
  4. execute middleware pipeline.
  5. Opening middlware page crashes the dotnet process or IIS Express.

Sample project exposing the bug: https://github.com/ChaosEngine/SomeMiddleware/releases/tag/bug1.0 line affecting the bug: https://github.com/ChaosEngine/SomeMiddleware/blob/49af9462989ff19794e0956fa15aac92e422da7d/WebApplication1/SomeMiddleware.cs#L58

Expected result

As with netcoreapp2.0 the page result should be returned from the middleware and gzip compression should be enabled.

Actual result

The IIS Express or dotnet process crashes with error message:

WebApplication1> Hosting environment: Development
WebApplication1> Content root path: C:\Users\apauli\Documents\Visual Studio 2015\Projects\SomeMiddleware\WebApplication1
WebApplication1> Now listening on: http://localhost:31067
WebApplication1> Application started. Press Ctrl+C to shut down.
WebApplication1> 
WebApplication1> Unhandled Exception: System.InvalidOperationException: Can not access a closed Stream.
WebApplication1>    at System.IO.Compression.GZipStream.EndWrite(IAsyncResult asyncResult)
WebApplication1>    at System.IO.Stream.<>c.<BeginEndWriteAsync>b__53_1(Stream stream, IAsyncResult asyncResult)
WebApplication1>    at System.Threading.Tasks.TaskFactory`1.FromAsyncTrimPromise`1.Complete(TInstance thisRef, Func`3 endMethod, IAsyncResult asyncResult, Boolean requiresSynchronization)
WebApplication1> --- End of stack trace from previous location where exception was thrown ---
WebApplication1>    at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
WebApplication1>    at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
WebApplication1>    at Microsoft.AspNetCore.ResponseCompression.BodyWrapperStream.<WriteAsync>d__32.MoveNext()
WebApplication1> --- End of stack trace from previous location where exception was thrown ---
WebApplication1>    at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
WebApplication1>    at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
WebApplication1>    at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
WebApplication1>    at WebApplication1.SomeMiddleware.<RespondWithIndexHtml>d__5.MoveNext()
WebApplication1> --- End of stack trace from previous location where exception was thrown ---
WebApplication1>    at System.Runtime.CompilerServices.AsyncMethodBuilderCore.<>c.<ThrowAsync>b__6_1(Object state)
WebApplication1>    at System.Threading.QueueUserWorkItemCallback.WaitCallback_Context(Object state)
WebApplication1>    at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
WebApplication1>    at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
WebApplication1>    at System.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem()
WebApplication1>    at System.Threading.ThreadPoolWorkQueue.Dispatch()
WebApplication1>    at System.Threading._ThreadPoolWaitCallback.PerformWaitCallback()

Further technical details

the bug could be worked around by using different use of following:

//Fixup
var bytes = Encoding.UTF8.GetBytes(some_html.ToString());
response.Body.Write(bytes, 0, bytes.Length);

Original bug was discovered in Swashbuckle.AspNetCore on newer version than 1.1.0 and reported as https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/600.

Tratcher commented 6 years ago

That workaround is suspect as it does exactly the same thing that response.WriteAsync does internally. https://github.com/aspnet/HttpAbstractions/blob/87cd79d6fc54bb4abf07c1e380cd7a9498a78612/src/Microsoft.AspNetCore.Http.Abstractions/Extensions/HttpResponseWritingExtensions.cs#L63-L64

ChaosEngine commented 6 years ago

@Tratcher O gosh, of course your right. It was meant to be:

var bytes = Encoding.UTF8.GetBytes(some_html.ToString());
response.Body.Write(bytes, 0, bytes.Length);

as a working fix-up (non-async version).

Tratcher commented 6 years ago

https://github.com/ChaosEngine/SomeMiddleware/blob/49af9462989ff19794e0956fa15aac92e422da7d/WebApplication1/SomeMiddleware.cs#L37 async void RespondWithIndexHtml must be async Task RespondWithIndexHtml and it must be await'd when called in Invoke. Otherwise it runs on a background thread after the response completes.

ChaosEngine commented 6 years ago

Ha, naturally :-) Thank you for pointing this out; hopefully will make a PR for https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/600. Closing this issue now.