As mentioned in a couple other posts (see References below) I am attempting to create response filters in order to modify content being produced by another web application.
I have the basic string transformation logic working and encapsulated into Filters that derive from a common FilterBase. However, the logic must operate on the full content, not chunks of content. Therefore I need to cache the chunks as they are written and perform the filter when all the writes are completed.
As shown below I created a new ResponseFilter derived from MemoryStream. On Write, the content is cached to another MemoryStream. On Flush, the full content, now in the MemoryStream is converted to a string and the Filter logic kicks in. The modified content is then written back out to the originating stream.
However, on every second request (basically when a new Filter is instantiated over the previous one) the previous filter's Flush method is being executed. At this point the the application crashes on the _outputStream.Write() method as the _cachedStream is empty.
The order of event is as follows:
- First Request
- Write method is called
- Flush method is called
- Close method is called
- Close method is called
- At this point the app returns and the proper content is displayed.
- Second Request
- Flush method is called
- Application crashes on _outputStream.Write. ArgumentOutOfRangeException (offset).
- Continue through crash (w/ in Visual Studio)
- Close method is called
There are a couple of questions I have:
- Why is Close called twice?
- Why is Flush called after Closed was called?
- To Jay's point below, Flush may be called before the stream is completely read, where should the filter logic reside? In Close? In Flush but with "if closing"?
- What is the proper implementation for a Response Filter that works on the entire content at once?
Note: I experience the exact same behavior (minus Close events) if I do not override the Close method.
public class ResponseFilter : MemoryStream
{
private readonly Stream _outputStream;
private MemoryStream _cachedStream = new MemoryStream(1024);
private readonly FilterBase _filter;
public ResponseFilter (Stream outputStream, FilterBase filter)
{
_outputStream = outputStream;
_filter = filter;
}
// Flush is called on the second, fourth, and so on, page request (second request) with empty content.
public override void Flush()
{
Encoding encoding = HttpContext.Current.Response.ContentEncoding;
string cachedContent = encoding.GetString(_cachedStream.ToArray());
// Filter the cached content
cachedContent = _filter.Filter(cachedContent);
byte[] buffer = encoding.GetBytes(cachedContent);
_cachedStream = new MemoryStream();
_cachedStream.Write(buffer, 0, buffer.Length);
// Write new content to stream
_outputStream.Write(_cachedStream.ToArray(), 0, (int)_cachedStream.Length);
_cachedStream.SetLength(0);
_outputStream.Flush();
}
// Write is called on the first, third, and so on, page request.
public override void Write(byte[] buffer, int offset, int count)
{
// Cache the content.
_cachedStream.Write(buffer, 0, count);
}
public override void Close()
{
_outputStream.Close();
}
}
// Example usage in a custom HTTP Module on the BeginRequest event.
FilterBase transformFilter = new MapServiceJsonResponseFilter();
response.Filter = new ResponseFilter(response.Filter, transformFilter);
References:
- How do I deploy a managed HTTP Module Site Wide?
- Creating multiple (15+) HTTP Response filters, Inheritance vs. Composition w/ Injection