Custom stream writer which avoids writing the BOM and does not flush or close the stream.

This commit is contained in:
Kiran Challa 2015-05-06 23:29:54 -07:00
parent d6012d4297
commit 40794fcc33
7 changed files with 642 additions and 128 deletions

View File

@ -64,98 +64,10 @@ namespace Microsoft.AspNet.Mvc
response.ContentType = contentTypeHeader.ToString();
var wrappedStream = new StreamWrapper(response.Body);
using (var writer = new StreamWriter(wrappedStream, encoding, BufferSize, leaveOpen: true))
using (var writer = new HttpResponseStreamWriter(response.Body, encoding))
{
try
{
var viewContext = new ViewContext(actionContext, view, viewData, tempData, writer);
await view.RenderAsync(viewContext);
}
catch
{
// Need to prevent writes/flushes on dispose because the StreamWriter will flush even if
// nothing got written. This leads to a response going out on the wire prematurely in case an
// exception is being thrown inside the try catch block.
wrappedStream.BlockWrites = true;
throw;
}
}
}
private class StreamWrapper : Stream
{
private readonly Stream _wrappedStream;
public StreamWrapper(Stream stream)
{
_wrappedStream = stream;
}
public bool BlockWrites { get; set; }
public override bool CanRead
{
get { return _wrappedStream.CanRead; }
}
public override bool CanSeek
{
get { return _wrappedStream.CanSeek; }
}
public override bool CanWrite
{
get { return _wrappedStream.CanWrite; }
}
public override void Flush()
{
if (!BlockWrites)
{
_wrappedStream.Flush();
}
}
public override long Length
{
get { return _wrappedStream.Length; }
}
public override long Position
{
get
{
return _wrappedStream.Position;
}
set
{
_wrappedStream.Position = value;
}
}
public override int Read(byte[] buffer, int offset, int count)
{
return _wrappedStream.Read(buffer, offset, count);
}
public override long Seek(long offset, SeekOrigin origin)
{
return Seek(offset, origin);
}
public override void SetLength(long value)
{
SetLength(value);
}
public override void Write(byte[] buffer, int offset, int count)
{
if (!BlockWrites)
{
_wrappedStream.Write(buffer, offset, count);
}
var viewContext = new ViewContext(actionContext, view, viewData, tempData, writer);
await view.RenderAsync(viewContext);
}
}
}

View File

@ -70,8 +70,7 @@ namespace Microsoft.AspNet.Mvc
var response = context.ActionContext.HttpContext.Response;
var selectedEncoding = context.SelectedEncoding;
using (var nonDisposableStream = new NonDisposableStream(response.Body))
using (var writer = new StreamWriter(nonDisposableStream, selectedEncoding, 1024, leaveOpen: true))
using (var writer = new HttpResponseStreamWriter(response.Body, selectedEncoding))
{
WriteObject(writer, context.Object);
}

View File

@ -0,0 +1,235 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Framework.Internal;
namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// Writes to the <see cref="Stream"/> using the supplied <see cref="Encoding"/>.
/// It does not write the BOM and also does not close the stream.
/// </summary>
public class HttpResponseStreamWriter : TextWriter
{
private const int DefaultBufferSize = 1024;
private readonly Stream _stream;
private Encoder _encoder;
private byte[] _byteBuffer;
private char[] _charBuffer;
private int _charBufferSize;
private int _charBufferCount;
public HttpResponseStreamWriter(Stream stream, Encoding encoding)
: this(stream, encoding, DefaultBufferSize)
{
}
public HttpResponseStreamWriter([NotNull] Stream stream, [NotNull] Encoding encoding, int bufferSize)
{
_stream = stream;
Encoding = encoding;
_encoder = encoding.GetEncoder();
_charBufferSize = bufferSize;
_charBuffer = new char[bufferSize];
_byteBuffer = new byte[encoding.GetMaxByteCount(bufferSize)];
}
public override Encoding Encoding { get; }
public override void Write(char value)
{
if (_charBufferCount == _charBufferSize)
{
FlushInternal();
}
_charBuffer[_charBufferCount] = value;
_charBufferCount++;
}
public override void Write(char[] values, int index, int count)
{
if (values == null)
{
return;
}
while (count > 0)
{
if (_charBufferCount == _charBufferSize)
{
FlushInternal();
}
CopyToCharBuffer(values, ref index, ref count);
}
}
public override void Write(string value)
{
if (value == null)
{
return;
}
var count = value.Length;
var index = 0;
while (count > 0)
{
if (_charBufferCount == _charBufferSize)
{
FlushInternal();
}
CopyToCharBuffer(value, ref index, ref count);
}
}
public override async Task WriteAsync(char value)
{
if (_charBufferCount == _charBufferSize)
{
await FlushInternalAsync();
}
_charBuffer[_charBufferCount] = value;
_charBufferCount++;
}
public override async Task WriteAsync(char[] values, int index, int count)
{
if (values == null)
{
return;
}
while (count > 0)
{
if (_charBufferCount == _charBufferSize)
{
await FlushInternalAsync();
}
CopyToCharBuffer(values, ref index, ref count);
}
}
public override async Task WriteAsync(string value)
{
if (value == null)
{
return;
}
var count = value.Length;
var index = 0;
while (count > 0)
{
if (_charBufferCount == _charBufferSize)
{
await FlushInternalAsync();
}
CopyToCharBuffer(value, ref index, ref count);
}
}
// We want to flush the stream when Flush/FlushAsync is explicitly
// called by the user (example: from a Razor view).
public override void Flush()
{
FlushInternal(true, true);
}
public override async Task FlushAsync()
{
await FlushInternalAsync(true, true);
}
// Do not flush the stream on Dispose, as this will cause response to be
// sent in chunked encoding in case of Helios.
protected override void Dispose(bool disposing)
{
FlushInternal(flushStream: false, flushEncoder: true);
}
private void FlushInternal(bool flushStream = false, bool flushEncoder = false)
{
if (_charBufferCount == 0)
{
return;
}
var count = _encoder.GetBytes(_charBuffer, 0, _charBufferCount, _byteBuffer, 0, flushEncoder);
if (count > 0)
{
_stream.Write(_byteBuffer, 0, count);
}
_charBufferCount = 0;
if (flushStream)
{
_stream.Flush();
}
}
private async Task FlushInternalAsync(bool flushStream = false, bool flushEncoder = false)
{
if (_charBufferCount == 0)
{
return;
}
var count = _encoder.GetBytes(_charBuffer, 0, _charBufferCount, _byteBuffer, 0, flushEncoder);
if (count > 0)
{
await _stream.WriteAsync(_byteBuffer, 0, count);
}
_charBufferCount = 0;
if (flushStream)
{
await _stream.FlushAsync();
}
}
private void CopyToCharBuffer(string value, ref int index, ref int count)
{
var remaining = Math.Min(_charBufferSize - _charBufferCount, count);
value.CopyTo(
sourceIndex: index,
destination: _charBuffer,
destinationIndex: _charBufferCount,
count: remaining);
_charBufferCount += remaining;
index += remaining;
count -= remaining;
}
private void CopyToCharBuffer(char[] values, ref int index, ref int count)
{
var remaining = Math.Min(_charBufferSize - _charBufferCount, count);
Buffer.BlockCopy(
src: values,
srcOffset: index * sizeof(char),
dst: _charBuffer,
dstOffset: _charBufferCount * sizeof(char),
count: remaining * sizeof(char));
_charBufferCount += remaining;
index += remaining;
count -= remaining;
}
}
}

View File

@ -18,9 +18,6 @@ namespace Microsoft.AspNet.Mvc
{
public class ViewExecutorTest
{
// The buffer size of the StreamWriter used in ViewResult.
private const int ViewResultStreamWriterBufferSize = 1024;
public static TheoryData<MediaTypeHeaderValue, string, byte[]> ViewExecutorSetsContentTypeAndEncodingData
{
get
@ -85,39 +82,16 @@ namespace Microsoft.AspNet.Mvc
Assert.Equal(expectedContentData, memoryStream.ToArray());
}
public static IEnumerable<object[]> ExecuteAsync_DoesNotWriteToResponse_OnceExceptionIsThrownData
{
get
{
yield return new object[] { 30, 0 };
if (TestPlatformHelper.IsMono)
{
// The StreamWriter in Mono buffers 2x the buffer size before flushing.
yield return new object[] { ViewResultStreamWriterBufferSize * 2 + 30, ViewResultStreamWriterBufferSize };
}
else
{
yield return new object[] { ViewResultStreamWriterBufferSize + 30, ViewResultStreamWriterBufferSize };
}
}
}
// The StreamWriter used by ViewResult an internal buffer and consequently anything written to this buffer
// prior to it filling up will not be written to the underlying stream once an exception is thrown.
[Theory]
[MemberData(nameof(ExecuteAsync_DoesNotWriteToResponse_OnceExceptionIsThrownData))]
public async Task ExecuteAsync_DoesNotWriteToResponse_OnceExceptionIsThrown(int writtenLength, int expectedLength)
[Fact]
public async Task ExecuteAsync_DoesNotWriteToResponse_OnceExceptionIsThrown()
{
// Arrange
var longString = new string('a', writtenLength);
var expectedLength = 0;
var view = new Mock<IView>();
view.Setup(v => v.RenderAsync(It.IsAny<ViewContext>()))
.Callback((ViewContext v) =>
{
view.ToString();
v.Writer.Write(longString);
throw new Exception();
});

View File

@ -142,11 +142,7 @@ namespace Microsoft.AspNet.Mvc.Core.Test.Formatters
var formattedContent = "\"" + content + "\"";
var mediaType = string.Format("application/json; charset={0}", encodingAsString);
var encoding = CreateOrGetSupportedEncoding(formatter, encodingAsString, isDefaultEncoding);
var preamble = encoding.GetPreamble();
var data = encoding.GetBytes(formattedContent);
var expectedData = new byte[preamble.Length + data.Length];
Buffer.BlockCopy(preamble, 0, expectedData, 0, preamble.Length);
Buffer.BlockCopy(data, 0, expectedData, preamble.Length, data.Length);
var expectedData = encoding.GetBytes(formattedContent);
var memStream = new MemoryStream();
var outputFormatterContext = new OutputFormatterContext

View File

@ -0,0 +1,398 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
namespace Microsoft.AspNet.Mvc
{
public class HttpResponseStreamWriterTest
{
[Fact]
public async Task DoesNotWriteBOM()
{
// Arrange
var memoryStream = new MemoryStream();
var encodingWithBOM = new UTF8Encoding(encoderShouldEmitUTF8Identifier: true);
var writer = new HttpResponseStreamWriter(memoryStream, encodingWithBOM);
var expectedData = new byte[] { 97, 98, 99, 100 }; // without BOM
// Act
using (writer)
{
await writer.WriteAsync("abcd");
}
// Assert
Assert.Equal(expectedData, memoryStream.ToArray());
}
[Fact]
public async Task DoesNotFlush_UnderlyingStream_OnClosingWriter()
{
// Arrange
var stream = new TestMemoryStream();
var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8);
// Act
await writer.WriteAsync("Hello");
writer.Close();
// Assert
Assert.Equal(0, stream.FlushCallCount);
Assert.Equal(0, stream.FlushAsyncCallCount);
}
[Fact]
public async Task DoesNotFlush_UnderlyingStream_OnDisposingWriter()
{
// Arrange
var stream = new TestMemoryStream();
var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8);
// Act
await writer.WriteAsync("Hello");
writer.Dispose();
// Assert
Assert.Equal(0, stream.FlushCallCount);
Assert.Equal(0, stream.FlushAsyncCallCount);
}
[Fact]
public async Task DoesNotClose_UnderlyingStream_OnDisposingWriter()
{
// Arrange
var stream = new TestMemoryStream();
var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8);
// Act
await writer.WriteAsync("Hello");
writer.Close();
// Assert
Assert.Equal(0, stream.CloseCallCount);
}
[Fact]
public async Task DoesNotDispose_UnderlyingStream_OnDisposingWriter()
{
// Arrange
var stream = new TestMemoryStream();
var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8);
// Act
await writer.WriteAsync("Hello world");
writer.Dispose();
// Assert
Assert.Equal(0, stream.DisposeCallCount);
}
[Theory]
[InlineData(1023)]
[InlineData(1024)]
[InlineData(1050)]
[InlineData(2048)]
public async Task FlushesBuffer_OnClose(int byteLength)
{
// Arrange
var stream = new TestMemoryStream();
var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8);
await writer.WriteAsync(new string('a', byteLength));
// Act
writer.Close();
// Assert
Assert.Equal(0, stream.FlushCallCount);
Assert.Equal(0, stream.FlushAsyncCallCount);
Assert.Equal(byteLength, stream.Length);
}
[Theory]
[InlineData(1023)]
[InlineData(1024)]
[InlineData(1050)]
[InlineData(2048)]
public async Task FlushesBuffer_OnDispose(int byteLength)
{
// Arrange
var stream = new TestMemoryStream();
var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8);
await writer.WriteAsync(new string('a', byteLength));
// Act
writer.Dispose();
// Assert
Assert.Equal(0, stream.FlushCallCount);
Assert.Equal(0, stream.FlushAsyncCallCount);
Assert.Equal(byteLength, stream.Length);
}
[Fact]
public void NoDataWritten_Flush_DoesNotFlushUnderlyingStream()
{
// Arrange
var stream = new TestMemoryStream();
var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8);
// Act
writer.Flush();
// Assert
Assert.Equal(0, stream.FlushCallCount);
Assert.Equal(0, stream.Length);
}
[Theory]
[InlineData(1023)]
[InlineData(1024)]
[InlineData(1050)]
[InlineData(2048)]
public void FlushesBuffer_OnFlush(int byteLength)
{
// Arrange
var stream = new TestMemoryStream();
var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8);
writer.Write(new string('a', byteLength));
// Act
writer.Flush();
// Assert
Assert.Equal(1, stream.FlushCallCount);
Assert.Equal(byteLength, stream.Length);
}
[Fact]
public async Task NoDataWritten_FlushAsync_DoesNotFlushUnderlyingStream()
{
// Arrange
var stream = new TestMemoryStream();
var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8);
// Act
await writer.FlushAsync();
// Assert
Assert.Equal(0, stream.FlushAsyncCallCount);
Assert.Equal(0, stream.Length);
}
[Theory]
[InlineData(1023)]
[InlineData(1024)]
[InlineData(1050)]
[InlineData(2048)]
public async Task FlushesBuffer_OnFlushAsync(int byteLength)
{
// Arrange
var stream = new TestMemoryStream();
var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8);
await writer.WriteAsync(new string('a', byteLength));
// Act
await writer.FlushAsync();
// Assert
Assert.Equal(1, stream.FlushAsyncCallCount);
Assert.Equal(byteLength, stream.Length);
}
[Theory]
[InlineData(1023)]
[InlineData(1024)]
[InlineData(1050)]
[InlineData(2048)]
public void WriteChar_WritesToStream(int byteLength)
{
// Arrange
var stream = new TestMemoryStream();
var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8);
// Act
using (writer)
{
for (var i = 0; i < byteLength; i++)
{
writer.Write('a');
}
}
// Assert
Assert.Equal(byteLength, stream.Length);
}
[Theory]
[InlineData(1023)]
[InlineData(1024)]
[InlineData(1050)]
[InlineData(2048)]
public void WriteCharArray_WritesToStream(int byteLength)
{
// Arrange
var stream = new TestMemoryStream();
var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8);
// Act
using (writer)
{
writer.Write((new string('a', byteLength)).ToCharArray());
}
// Assert
Assert.Equal(byteLength, stream.Length);
}
[Theory]
[InlineData(1023)]
[InlineData(1024)]
[InlineData(1050)]
[InlineData(2048)]
public async Task WriteCharAsync_WritesToStream(int byteLength)
{
// Arrange
var stream = new TestMemoryStream();
var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8);
// Act
using (writer)
{
for (var i = 0; i < byteLength; i++)
{
await writer.WriteAsync('a');
}
}
// Assert
Assert.Equal(byteLength, stream.Length);
}
[Theory]
[InlineData(1023)]
[InlineData(1024)]
[InlineData(1050)]
[InlineData(2048)]
public async Task WriteCharArrayAsync_WritesToStream(int byteLength)
{
// Arrange
var stream = new TestMemoryStream();
var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8);
// Act
using (writer)
{
await writer.WriteAsync((new string('a', byteLength)).ToCharArray());
}
// Assert
Assert.Equal(byteLength, stream.Length);
}
[Theory]
[InlineData("你好世界", "utf-16")]
[InlineData("こんにちは世界", "shift_jis")]
[InlineData("హలో ప్రపంచ", "iso-8859-1")]
[InlineData("வணக்கம் உலக", "utf-32")]
public async Task WritesData_InExpectedEncoding(string data, string encodingName)
{
// Arrange
var encoding = Encoding.GetEncoding(encodingName);
var expectedBytes = encoding.GetBytes(data);
var stream = new MemoryStream();
var writer = new HttpResponseStreamWriter(stream, encoding);
// Act
using (writer)
{
await writer.WriteAsync(data);
}
// Assert
Assert.Equal(expectedBytes, stream.ToArray());
}
[Theory]
[InlineData('ん', 1023, "utf-8")]
[InlineData('ん', 1024, "utf-8")]
[InlineData('ん', 1050, "utf-8")]
[InlineData('你', 1023, "utf-16")]
[InlineData('你', 1024, "utf-16")]
[InlineData('你', 1050, "utf-16")]
[InlineData('こ', 1023, "shift_jis")]
[InlineData('こ', 1024, "shift_jis")]
[InlineData('こ', 1050, "shift_jis")]
[InlineData('హ', 1023, "iso-8859-1")]
[InlineData('హ', 1024, "iso-8859-1")]
[InlineData('హ', 1050, "iso-8859-1")]
[InlineData('வ', 1023, "utf-32")]
[InlineData('வ', 1024, "utf-32")]
[InlineData('வ', 1050, "utf-32")]
public async Task WritesData_OfDifferentLength_InExpectedEncoding(
char character,
int charCount,
string encodingName)
{
// Arrange
var encoding = Encoding.GetEncoding(encodingName);
string data = new string(character, charCount);
var expectedBytes = encoding.GetBytes(data);
var stream = new MemoryStream();
var writer = new HttpResponseStreamWriter(stream, encoding);
// Act
using (writer)
{
await writer.WriteAsync(data);
}
// Assert
Assert.Equal(expectedBytes, stream.ToArray());
}
private class TestMemoryStream : MemoryStream
{
private int _flushCallCount;
private int _flushAsyncCallCount;
private int _closeCallCount;
private int _disposeCallCount;
public int FlushCallCount { get { return _flushCallCount; } }
public int FlushAsyncCallCount { get { return _flushAsyncCallCount; } }
public int CloseCallCount { get { return _closeCallCount; } }
public int DisposeCallCount { get { return _disposeCallCount; } }
public override void Flush()
{
_flushCallCount++;
base.Flush();
}
public override Task FlushAsync(CancellationToken cancellationToken)
{
_flushAsyncCallCount++;
return base.FlushAsync(cancellationToken);
}
public override void Close()
{
_closeCallCount++;
base.Close();
}
protected override void Dispose(bool disposing)
{
_disposeCallCount++;
base.Dispose(disposing);
}
}
}
}

View File

@ -112,7 +112,7 @@ namespace Microsoft.AspNet.Mvc
public async Task ExecuteResultAsync_UsesPassedInFormatter()
{
// Arrange
var expected = Enumerable.Concat(Encoding.UTF8.GetPreamble(), _abcdUTF8Bytes);
var expected = _abcdUTF8Bytes;
var context = GetHttpContext();
var actionContext = new ActionContext(context, new RouteData(), new ActionDescriptor());