-
Notifications
You must be signed in to change notification settings - Fork 10.4k
Implement PipeBody Features and add to HttpContext #6394
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
Conversation
// If the Response.Body has been updated, recreate the pipeWriter | ||
(_pipeWriter is StreamPipeWriter writer && !object.ReferenceEquals(writer.InnerStream, HttpResponseFeature.Body))) | ||
{ | ||
_pipeWriter = new StreamPipeWriter(HttpResponseFeature.Body); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this case, do we lose the old reference to the pipe? Who is responsible for cleaning it up? Can this leak memory? Does it matter?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tough situation. Someone can keep a reference to the BodyPipe like:
context.Response.Body = new MemoryStream();
var canBeDisposed = context.Response.BodyPipe;
context.Response.Body = Stream.Null;
var nonDisposed = context.Response.BodyPipe;
Should we dispose canBeDisposed in this situation? I don't think we would dispose the stream in this situation, so I don't think we should call dispose here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Considering we're implicitly constructing the StreamPipeWriter, we should dispose it. I don't think we can expect developers to know they might have dispose the old response body StreamPipeWriter just because they replaced the response body Stream.
In this case it's not trivial to track who constructed the _pipeWriter, but we should try. It's also probably a good idea to fail any I/O being done on the old _pipeWriter instead of closing in gracefully.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this case it's not trivial to track who constructed the _pipeWriter, but we should try.
I think the tracking needs to exist inside of the feature. I'll take a stab at something.
It's also probably a good idea to fail any I/O being done on the old _pipeWriter instead of closing in gracefully.
You are effectively asking for us to call Abort on the underlying stream, correct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, what happens if someone wraps the BodyPipe with another PipeReader/Writer. For example, if we were to reimplement ResponseCompression with Pipes, we would do:
ctx.Response.BodyPipe = new ResponseCompressionPipeWriter(ctx.Response.BodyPipe);
which would fail because the BodyPipe is now disposed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We will now call HttpResponse.RegisterForDispose()
for all adapters that we set.
@jkotalik @halter73 @Tratcher servers have to re-implement this logic https://github.com/aspnet/AspNetCore/pull/6394/files#diff-f773336ba05e72446c3f058e83ca3bc4R30 yeah? If a server implements these features it has to handle the wrapping and unwrapping manually. These features will only be used by Http.Sys server? Is that correct? (Assuming we do native pipes in both IIS and Kestrel). |
@davidfowl Kestrel, IIS, and TestServer need to implement this feature but I expect they'd do so in reverse. Pipe would be the default and Stream the fallback. And yes, the implementation here will primarily be used for Http.Sys. |
} | ||
set | ||
{ | ||
_pipeReader = value ?? throw new ArgumentNullException(nameof(value)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why throw when setting a null request body pipe? We don't throw when setting a null request body stream do we?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've bounced back and forth between this a few times. There are a few things we need to think about:
When setting the BodyPipe
, we should update Body
accordingly. When someone sets BodyPipe to null, either we can throw or set both Body and BodyPipe to null. Today, when someone sets Body to null, it remains null.
So I think I agree with you.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Body should never be set to null. Stream.Null maybe, but not null. No we don't throw for that at the moment but in hindsight we should have.
I don't see a scenario for setting pipe to null either.
// If the Response.Body has been updated, recreate the pipeWriter | ||
(_pipeWriter is StreamPipeWriter writer && !object.ReferenceEquals(writer.InnerStream, HttpResponseFeature.Body))) | ||
{ | ||
_pipeWriter = new StreamPipeWriter(HttpResponseFeature.Body); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Considering we're implicitly constructing the StreamPipeWriter, we should dispose it. I don't think we can expect developers to know they might have dispose the old response body StreamPipeWriter just because they replaced the response body Stream.
In this case it's not trivial to track who constructed the _pipeWriter, but we should try. It's also probably a good idea to fail any I/O being done on the old _pipeWriter instead of closing in gracefully.
Offline discussion on Dispose: You can't. Consider the decorator example for something like response compression. It wraps the inner stream with its own that does the transformation and reassigns the response body field. Then it undoes that wrapping at the end of the response. The same style wrapping should be possible for pipes, so the original value must be kept alive. |
You can and you must if you don't want to leak pooled memory. When we construct a StreamPipeReader/Writer, we've decided that the body Stream is the source of truth. This only wraps StreamPipeReader/Writers, not arbitrary pipes, so there's no risk of this replacing a transformative pipe. |
@halter73 see above. Thoughts on adding it to RegisterForDispose. |
@halter73 we worked out a cleaner solution: register the pipe for dispose on creation, not on replacement. |
else if (!object.ReferenceEquals(_internalPipeWriter.InnerStream, _context.Response.Body)) | ||
{ | ||
_internalPipeWriter = new StreamPipeWriter(_context.Response.Body); | ||
_context.Response.RegisterForDispose(_internalPipeWriter); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have to call RegisterForDispose here because by default the DefaultHttpContext doesn't call Uninitialize on the Request and Response features. https://github.com/aspnet/AspNetCore/blob/master/src/Http/Http/src/DefaultHttpContext.cs#L197.
I'm still thinking about how to write a test for this.
fef445a
to
9f1969e
Compare
For #3972.
Add more Stream/Pipe interleaving test to verify stream and pipe can be read from/ written two sequentially.