Skip to content

Commit 85aabb7

Browse files
authored
Fix HttpContent.ReadAsStream length, don't buffer StreamContent unless necessary (#416)
1 parent 2ae0710 commit 85aabb7

File tree

6 files changed

+179
-113
lines changed

6 files changed

+179
-113
lines changed

Tests/HttpUnitTests/StreamContentTest.cs

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,65 @@ public void CopyTo_CallMultipleTimesWithStreamNotSupportingSeekingButBufferedStr
176176
Assert.Equal(10 - consumed, destination2.Length);
177177
}
178178

179+
[TestMethod]
180+
public void CopyTo_NoLoadIntoBuffer_NotBuffered()
181+
{
182+
var initialSourceContent = new byte[10] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
183+
184+
var sourceContent = new byte[10];
185+
Array.Copy(initialSourceContent, sourceContent, 10);
186+
187+
var sourceStream = new MockStream(sourceContent, true, true);
188+
189+
var content = new StreamContent(sourceStream);
190+
191+
var destination1 = new MemoryStream();
192+
content.CopyTo(destination1);
193+
194+
Assert.AreEqual(10, destination1.Length);
195+
CollectionAssert.AreEqual(initialSourceContent, destination1.ToArray());
196+
197+
// replace source content to ensure data was not buffered in the content
198+
var replacedSourceContent = new byte[10] { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 };
199+
Array.Copy(replacedSourceContent, sourceContent, 10);
200+
201+
var destination2 = new MemoryStream();
202+
content.CopyTo(destination2);
203+
Assert.AreEqual(10, destination2.Length);
204+
CollectionAssert.AreEqual(replacedSourceContent, destination2.ToArray());
205+
}
206+
207+
[TestMethod]
208+
public void CopyTo_LoadIntoBuffer_Buffered()
209+
{
210+
var initialSourceContent = new byte[10] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
211+
212+
var sourceContent = new byte[10];
213+
Array.Copy(initialSourceContent, sourceContent, 10);
214+
215+
var sourceStream = new MockStream(sourceContent, true, true);
216+
217+
var content = new StreamContent(sourceStream);
218+
219+
// buffer so changing the source stream doesn't change the Content
220+
content.LoadIntoBuffer();
221+
222+
var destination1 = new MemoryStream();
223+
content.CopyTo(destination1);
224+
225+
Assert.AreEqual(10, destination1.Length);
226+
CollectionAssert.AreEqual(initialSourceContent, destination1.ToArray());
227+
228+
// replace source content to ensure data was buffered in the content
229+
var replacedSourceContent = new byte[10] { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 };
230+
Array.Copy(replacedSourceContent, sourceContent, 10);
231+
232+
var destination2 = new MemoryStream();
233+
content.CopyTo(destination2);
234+
Assert.AreEqual(10, destination2.Length);
235+
CollectionAssert.AreEqual(initialSourceContent, destination2.ToArray());
236+
}
237+
179238
[TestMethod]
180239
public void ContentReadStream_GetProperty_ReturnOriginalStream()
181240
{
@@ -189,6 +248,20 @@ public void ContentReadStream_GetProperty_ReturnOriginalStream()
189248
Assert.NotSame(source, stream);
190249
}
191250

251+
[TestMethod]
252+
public void ContentReadStream_GetProperty_LoadIntoBuffer_ReturnOriginalStream()
253+
{
254+
var source = new MockStream(new byte[10]);
255+
var content = new StreamContent(source);
256+
content.LoadIntoBuffer();
257+
258+
Stream stream = content.ReadAsStream();
259+
Assert.IsFalse(stream.CanWrite);
260+
Assert.AreEqual(source.Length, stream.Length);
261+
Assert.AreEqual(0, source.ReadCount);
262+
Assert.AreNotSame(source, stream);
263+
}
264+
192265
[TestMethod]
193266
public void ContentReadStream_GetPropertyPartiallyConsumed_ReturnOriginalStream()
194267
{
@@ -220,7 +293,7 @@ public void ContentReadStream_CheckResultProperties_ValuesRepresentReadOnlyStrea
220293
var content = new StreamContent(source);
221294
Stream contentReadStream = content.ReadAsStream();
222295

223-
// The following checks verify that the stream returned passes all read-related properties to the
296+
// The following checks verify that the stream returned passes all read-related properties to the
224297
// underlying MockStream and throws when using write-related members.
225298

226299
Assert.False(contentReadStream.CanWrite);

nanoFramework.System.Net.Http/Http/HttpClient.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,9 @@ public byte[] GetByteArray(string requestUri)
309309
/// This operation will block.
310310
/// </para>
311311
/// <para>
312-
/// This method does not read nor buffer the response body. It will return as soon as the response headers are read.
312+
/// As of now, this method reads and buffers the entire response body (see <see cref="StreamContent.SerializeToStream(Stream)"/>).
313+
/// If the response needs to be streamed, one of the methods that return <see cref="Send(HttpRequestMessage, HttpCompletionOption)"/> should be used
314+
/// with <see cref="HttpCompletionOption.ResponseHeadersRead"/> and <see cref="HttpContent.CopyTo(Stream)"/> instead.
313315
/// </para>
314316
/// <para>
315317
/// This is the .NET nanoFramework equivalent of GetStreamAsync.
@@ -434,7 +436,7 @@ private HttpResponseMessage SendWorker(HttpRequestMessage request, HttpCompletio
434436
HttpResponseMessage response = base.Send(request);
435437

436438
// Read the content when default HttpCompletionOption.ResponseContentRead is set
437-
if (response.Content != null && (completionOption & HttpCompletionOption.ResponseHeadersRead) == 0)
439+
if (response.Content != null && completionOption == HttpCompletionOption.ResponseContentRead)
438440
{
439441
response.Content.LoadIntoBuffer();
440442
}

nanoFramework.System.Net.Http/Http/HttpClientHandler.cs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -224,22 +224,26 @@ protected internal override HttpResponseMessage Send(HttpRequestMessage request)
224224
headers.AddInternal(headerKey, content.Headers._headerStore[headerKey]);
225225
}
226226

227-
if (request.Headers.TransferEncodingChunked == true)
227+
if (request.Headers.TransferEncodingChunked)
228228
{
229229
webRequest.SendChunked = true;
230230
}
231231

232232
// set content length
233-
request.Content.TryComputeLength(out long lenght);
234-
webRequest.ContentLength = lenght;
233+
// if it can't be computed and Transfer-Encoding: chunked isn't set,
234+
// webRequest.GetRequestStream throws an exception (so we don't have to validate that here).
235+
if (request.Content.TryComputeLength(out long length))
236+
{
237+
webRequest.ContentLength = length;
238+
}
239+
235240

236241
// set request sent flag
237242
_sentRequest = true;
238243

239244
var stream = webRequest.GetRequestStream();
240245

241-
request.Content.ReadAsStream().CopyTo(stream);
242-
246+
request.Content.CopyTo(stream);
243247
}
244248
else if (MethodHasBody(request.Method))
245249
{

nanoFramework.System.Net.Http/Http/HttpContent.cs

Lines changed: 23 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,10 @@ namespace System.Net.Http
1616
public abstract class HttpContent : IDisposable
1717
{
1818
private HttpContentHeaders _headers;
19-
private FixedMemoryStream _buffer;
20-
private Stream _stream;
19+
private MemoryStream _buffer;
2120

2221
private bool _disposed;
2322

24-
internal const int MaxBufferSize = int.MaxValue;
2523
internal static readonly Encoding DefaultStringEncoding = Encoding.UTF8;
2624

2725
/// <summary>
@@ -41,11 +39,6 @@ public HttpContentHeaders Headers
4139
}
4240
}
4341

44-
/// <summary>
45-
/// Contains the actual bytes read into the buffer
46-
/// </summary>
47-
protected int TotalBytesRead { get; set; }
48-
4942
/// <summary>
5043
/// Initializes a new instance of the HttpContent class.
5144
/// </summary>
@@ -72,7 +65,9 @@ public void CopyTo(Stream stream)
7265

7366
if (_buffer != null)
7467
{
68+
_buffer.Position = 0;
7569
_buffer.CopyTo(stream);
70+
return;
7671
}
7772

7873
SerializeToStream(stream);
@@ -94,7 +89,7 @@ public void CopyTo(Stream stream)
9489
/// </para>
9590
/// </remarks>
9691
/// <exception cref="ObjectDisposedException">If the object has been disposed.</exception>
97-
public Stream LoadIntoBuffer()
92+
public void LoadIntoBuffer()
9893
{
9994
if (_disposed)
10095
{
@@ -103,21 +98,12 @@ public Stream LoadIntoBuffer()
10398

10499
if (_buffer != null)
105100
{
106-
return _buffer;
107-
}
108-
109-
_buffer = new FixedMemoryStream(int.MaxValue);
110-
111-
SerializeToStream(_buffer);
112-
113-
if (TotalBytesRead > 0)
114-
{
115-
_buffer.SetLength(TotalBytesRead);
101+
return;
116102
}
117103

118-
_buffer.Seek(0, SeekOrigin.Begin);
119-
120-
return _buffer;
104+
var buffer = new MemoryStream();
105+
SerializeToStream(buffer);
106+
_buffer = buffer;
121107
}
122108

123109
/// <summary>
@@ -127,24 +113,7 @@ public Stream LoadIntoBuffer()
127113
/// <exception cref="ObjectDisposedException">If the object has been disposed.</exception>
128114
public Stream ReadAsStream()
129115
{
130-
if (_disposed)
131-
{
132-
throw new ObjectDisposedException();
133-
}
134-
135-
if (_buffer != null)
136-
{
137-
138-
return new MemoryStream(_buffer.GetBuffer());
139-
}
140-
141-
if (_stream == null)
142-
{
143-
144-
_stream = LoadIntoBuffer();
145-
}
146-
147-
return _stream;
116+
return CreateContentReadStream();
148117
}
149118

150119
/// <summary>
@@ -237,35 +206,22 @@ protected virtual void Dispose(bool disposing)
237206
/// </remarks>
238207
protected abstract void SerializeToStream(Stream stream);
239208

240-
private sealed class FixedMemoryStream : MemoryStream
209+
/// <summary>
210+
/// If overridden, returns the HTTP content as a stream.
211+
/// Otherwise, <see cref="SerializeToStream(Stream)"/> is called.
212+
/// </summary>
213+
/// <returns></returns>
214+
/// <remarks>
215+
/// <para>
216+
/// This is the .NET nanoFramework equivalent of CreateContentReadStreamAsync().
217+
/// </para>
218+
/// </remarks>
219+
protected virtual Stream CreateContentReadStream()
241220
{
242-
readonly long _maxSize;
243-
244-
public FixedMemoryStream(long maxSize)
245-
: base()
246-
{
247-
_maxSize = maxSize;
248-
}
249-
250-
private void CheckOverflow(int count)
251-
{
252-
if (Length + count > _maxSize)
253-
{
254-
throw new HttpRequestException();
255-
}
256-
}
257-
258-
public override void WriteByte(byte value)
259-
{
260-
CheckOverflow(1);
261-
base.WriteByte(value);
262-
}
221+
LoadIntoBuffer();
263222

264-
public override void Write(byte[] buffer, int offset, int count)
265-
{
266-
CheckOverflow(count);
267-
base.Write(buffer, offset, count);
268-
}
223+
_buffer.Seek(0, SeekOrigin.Begin);
224+
return _buffer;
269225
}
270226
}
271227
}

0 commit comments

Comments
 (0)