Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
1f57ec1
Initial plan
Copilot Aug 13, 2026
5984a75
Add SseParser buffer options
Copilot Aug 13, 2026
681f797
Restore existing SseParser Create overloads
Copilot Aug 14, 2026
eb47059
Address review: use ArgumentNullException.ThrowIfNull and validate Ma…
Copilot Aug 14, 2026
fee9f0c
Address SSE parser review feedback
Copilot Aug 14, 2026
d60b319
Apply suggestions from code review
MihaZupan Aug 14, 2026
0fe7c83
Revert unrelated test wrapping and fix MaxBufferSize enforcement bug
Copilot Aug 14, 2026
6674fb1
Fix SSE MaxBufferSize read enforcement
Copilot Aug 14, 2026
9765a2d
Fix netstandard2.0/.NET Framework compat for SseParserOptions
Copilot Aug 16, 2026
532dba5
Silently raise MaxBufferSize values below 128 to that minimum
Copilot Aug 16, 2026
c76a3ad
Reuse DefaultArrayPoolRentSize instead of a separate MinMaxBufferSize…
Copilot Aug 17, 2026
f2fa2fd
Keep SSE parser initial buffer size
Copilot Aug 17, 2026
2fea097
Remove SSE buffer rent max cap
Copilot Aug 17, 2026
71d63bd
Use full rented capacity for SSE reads
Copilot Aug 17, 2026
65cb3d9
Adjust SSE max-buffer enforcement to grow-time check
Copilot Aug 18, 2026
158a15d
Manual corrections and logic movements.
mrek-msft Aug 20, 2026
cc3d2b5
Improve comments
mrek-msft Aug 21, 2026
c941e35
Resolving PR comments
mrek-msft Aug 25, 2026
9655642
Remove duplicate test
mrek-msft Aug 25, 2026
9866b9a
SImplify code, fix too late check for skipping if large enough
mrek-msft Aug 25, 2026
f33b397
Code cleanup
mrek-msft Aug 25, 2026
7d836a9
Simplify code, work in uint to prevent explicit overflow checks, add …
mrek-msft Aug 25, 2026
4ad284e
Do not "grow" if current sioze equals requested.
mrek-msft Aug 25, 2026
3cc0fa5
Typos in comments
mrek-msft Aug 25, 2026
d1962d4
Removing confusing comments
mrek-msft Aug 25, 2026
975536b
Refactor typos in code.
mrek-msft Aug 25, 2026
ff27e48
Update src/libraries/System.Net.ServerSentEvents/src/System/Net/Serve…
mrek-msft Aug 26, 2026
afa6232
remove Array.MaxLength check
mrek-msft Aug 26, 2026
b64eaea
Merge branch 'copilot/add-sseparser-options' of https://github.com/do…
mrek-msft Aug 26, 2026
0ed6ad8
Handle above Array.MaxLength edge case
mrek-msft Aug 26, 2026
27de8f5
Fix typo
mrek-msft Aug 26, 2026
4a10b2a
Fix bad comparison operator
mrek-msft Aug 26, 2026
93c49c2
Improve comment
mrek-msft Aug 26, 2026
16ebdf1
Fix 'allows' test
MihaZupan Aug 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ public static partial class SseParser
public const string EventTypeDefault = "message";
public static System.Net.ServerSentEvents.SseParser<string> Create(System.IO.Stream sseStream) { throw null; }
public static System.Net.ServerSentEvents.SseParser<T> Create<T>(System.IO.Stream sseStream, System.Net.ServerSentEvents.SseItemParser<T> itemParser) { throw null; }
public static System.Net.ServerSentEvents.SseParser<T> Create<T>(System.IO.Stream sseStream, System.Net.ServerSentEvents.SseParserOptions<T> options) { throw null; }
}
public sealed partial class SseParserOptions<T>
{
public SseParserOptions(System.Net.ServerSentEvents.SseItemParser<T> itemParser) { }
public System.Net.ServerSentEvents.SseItemParser<T> ItemParser { get { throw null; } }
public int MaxBufferSize { get { throw null; } set { } }
}
Comment thread
mrek-msft marked this conversation as resolved.
public sealed partial class SseParser<T>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ System.Net.ServerSentEvents.SseParser</PackageDescription>
<Compile Include="System\Net\ServerSentEvents\SseItem.cs" />
<Compile Include="System\Net\ServerSentEvents\SseItemParser.cs" />
<Compile Include="System\Net\ServerSentEvents\SseParser.cs" />
<Compile Include="System\Net\ServerSentEvents\SseParserOptions.cs" />
<Compile Include="System\Net\ServerSentEvents\ThrowHelper.cs" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

using System.IO;
using System.Text;

namespace System.Net.ServerSentEvents
{
/// <summary>Provides a parser for parsing server-sent events.</summary>
Expand Down Expand Up @@ -32,19 +31,21 @@ public static SseParser<string> Create(Stream sseStream) =>
/// <param name="itemParser">The parser to use to transform each payload of bytes into a data element.</param>
/// <returns>The enumerable, which can be enumerated synchronously or asynchronously.</returns>
/// <exception cref="ArgumentNullException"><paramref name="sseStream"/> or <paramref name="itemParser"/> is null.</exception>
public static SseParser<T> Create<T>(Stream sseStream, SseItemParser<T> itemParser)
{
if (sseStream is null)
{
ThrowHelper.ThrowArgumentNullException(nameof(sseStream));
}
public static SseParser<T> Create<T>(Stream sseStream, SseItemParser<T> itemParser) =>
Create(sseStream, new SseParserOptions<T>(itemParser));
Comment thread
mrek-msft marked this conversation as resolved.

if (itemParser is null)
{
ThrowHelper.ThrowArgumentNullException(nameof(itemParser));
}
/// <summary>Creates a parser for parsing a <paramref name="sseStream"/> of server-sent events into a sequence of <see cref="SseItem{T}"/> values.</summary>
/// <typeparam name="T">Specifies the type of data in each event.</typeparam>
/// <param name="sseStream">The stream containing the data to parse.</param>
/// <param name="options">The options to use when parsing the stream.</param>
/// <returns>The enumerable, which can be enumerated synchronously or asynchronously.</returns>
/// <exception cref="ArgumentNullException"><paramref name="sseStream"/> or <paramref name="options"/> is null.</exception>
public static SseParser<T> Create<T>(Stream sseStream, SseParserOptions<T> options)
Comment thread
MihaZupan marked this conversation as resolved.
{
ThrowHelper.ThrowIfNull(sseStream, nameof(sseStream));
ThrowHelper.ThrowIfNull(options, nameof(options));

return new SseParser<T>(sseStream, itemParser);
return new SseParser<T>(sseStream, options);
Comment thread
mrek-msft marked this conversation as resolved.
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace System.Net.ServerSentEvents
{
/// <summary>Provides options for parsing server-sent events.</summary>
/// <typeparam name="T">Specifies the type of data parsed from an event.</typeparam>
public sealed class SseParserOptions<T>
{
/// <summary>Initializes a new instance of the <see cref="SseParserOptions{T}"/> class.</summary>
/// <param name="itemParser">The parser to use to transform each payload of bytes into a data element.</param>
/// <exception cref="ArgumentNullException"><paramref name="itemParser"/> is null.</exception>
public SseParserOptions(SseItemParser<T> itemParser)
{
ThrowHelper.ThrowIfNull(itemParser, nameof(itemParser));

ItemParser = itemParser;
}

/// <summary>Gets the parser to use to transform each payload of bytes into a data element.</summary>
public SseItemParser<T> ItemParser { get; }

/// <summary>Gets or sets the maximum buffer size requested from the underlying allocator, or -1 to use the default limit.</summary>
/// <exception cref="ArgumentOutOfRangeException">The value set is less than -1.</exception>
/// <remarks>The limit is enforced on a best-effort basis. The permitted memory consumption may be several times higher than the configured limit. Values smaller than the internal minimum buffer size are treated as that minimum.</remarks>
public int MaxBufferSize
{
get => _maxBufferSize;
set
{
if (value < -1)
{
ThrowHelper.ThrowArgumentOutOfRangeException(nameof(value));
}

_maxBufferSize = value;
}
}

private int _maxBufferSize = -1;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,21 @@ public sealed class SseParser<T>
private readonly long TimeSpan_MaxValueMilliseconds = (long)TimeSpan.MaxValue.TotalMilliseconds;

/// <summary>The default size of an ArrayPool buffer to rent.</summary>
/// <remarks>Larger size used by default to minimize number of reads. Smaller size used in debug to stress growth/shifting logic.</remarks>
/// <remarks>
/// Larger size used by default to minimize number of reads. Smaller size used in debug to stress growth/shifting logic.
/// Also serves as the smallest configurable maximum buffer size; buffers smaller than this don't meaningfully reduce
/// memory usage but can cause excessive I/O and line-buffer churn.
/// </remarks>
private const int DefaultArrayPoolRentSize =
#if DEBUG
16;
#else
1024;
#endif

/// <summary>The maximum amount of data buffered by default.</summary>
Comment thread
MihaZupan marked this conversation as resolved.
Comment thread
MihaZupan marked this conversation as resolved.
private const int DefaultMaxBufferSize = 1024 * 1024 * 1024;

/// <summary>The stream to be parsed.</summary>
private readonly Stream _stream;
/// <summary>The parser delegate used to transform bytes into a <typeparamref name="T"/>.</summary>
Expand Down Expand Up @@ -74,7 +81,7 @@ public sealed class SseParser<T>
/// <remarks>This can be different than <see cref="_dataLength"/> != 0 if empty data was appended.</remarks>
private bool _dataAppended;

private int _maxBufferSize = 1024 * 1024 * 1024;
private readonly int _maxBufferSize;

/// <summary>The event type for the next event.</summary>
private string? _eventType;
Expand All @@ -87,11 +94,18 @@ public sealed class SseParser<T>

/// <summary>Initialize the enumerable.</summary>
/// <param name="stream">The stream to parse.</param>
/// <param name="itemParser">The function to use to parse payload bytes into a <typeparamref name="T"/>.</param>
internal SseParser(Stream stream, SseItemParser<T> itemParser)
/// <param name="options">The options to use to parse the stream.</param>
internal SseParser(Stream stream, SseParserOptions<T> options)
{
_stream = stream;
_itemParser = itemParser;
_itemParser = options.ItemParser;
_maxBufferSize = options.MaxBufferSize == -1 ? DefaultMaxBufferSize : Math.Max(options.MaxBufferSize, DefaultArrayPoolRentSize);

#if NET
_maxBufferSize = Math.Min(_maxBufferSize, Array.MaxLength);
#else
_maxBufferSize = Math.Min(_maxBufferSize, 0x7FFFFFC7);
#endif
}
Comment thread
mrek-msft marked this conversation as resolved.

/// <summary>Gets an enumerable of the server-sent events from this parser.</summary>
Expand Down Expand Up @@ -306,19 +320,12 @@ private void ShiftOrGrowLineBufferIfNecessary()
}
else if (_lineLength == _lineBuffer.Length)
{
int newLength;
try
{
newLength = checked(_lineBuffer.Length * 2);
}
catch (OverflowException)
{
throw new InvalidDataException(SR.InvalidDataException_SseExceededMaxLength);
}

GrowBuffer(ref _lineBuffer, newLength);
GrowBuffer(ref _lineBuffer, (uint)_lineBuffer.Length + 1);
}
Comment thread
mrek-msft marked this conversation as resolved.
}

// Storage available for at least one byte
Debug.Assert(_lineOffset + _lineLength < _lineBuffer.Length);
}

/// <summary>Processes a complete line from the SSE stream.</summary>
Expand Down Expand Up @@ -400,20 +407,7 @@ private bool ProcessLine(out SseItem<T> sseItem, out int advance)
}

// We need to copy the data from the line buffer to the data buffer. Make sure there's enough room.
int newLength;
try
{
newLength = checked(_dataLength + _lineLength + 1);
}
catch (OverflowException)
{
throw new InvalidDataException(SR.InvalidDataException_SseExceededMaxLength);
}

if (_dataBuffer is null || newLength > _dataBuffer.Length)
{
GrowBuffer(ref _dataBuffer, newLength);
}
GrowBuffer(ref _dataBuffer, (uint)_dataLength + (uint)_lineLength + 1);

Comment thread
mrek-msft marked this conversation as resolved.
// Append a newline if there's already content in the buffer.
// Then copy the field value to the data buffer
Expand Down Expand Up @@ -554,15 +548,30 @@ private void SkipBomIfPresent()
}

/// <summary>Grows the buffer, returning the existing one to the ArrayPool and renting an ArrayPool replacement.</summary>
private void GrowBuffer([NotNull] ref byte[]? buffer, int minimumLength)
private void GrowBuffer([NotNull] ref byte[]? buffer, uint minimumSize)
{
if (minimumLength > _maxBufferSize)
int currentSize = buffer?.Length ?? 0;

uint preferredSize = (uint)currentSize * 2;
preferredSize = Math.Min(preferredSize, (uint)_maxBufferSize);
preferredSize = Math.Max(preferredSize, DefaultArrayPoolRentSize);
Debug.Assert(preferredSize <= int.MaxValue);

if (minimumSize > _maxBufferSize)
{
throw new InvalidDataException(SR.InvalidDataException_SseExceededMaxLength);
}
Debug.Assert(minimumSize <= int.MaxValue);

if (buffer is not null && currentSize >= minimumSize)
Comment thread
MihaZupan marked this conversation as resolved.
{
return;
}

int rentedSize = Math.Max((int)preferredSize, (int)minimumSize);

byte[]? toReturn = buffer;
buffer = ArrayPool<byte>.Shared.Rent(Math.Max(minimumLength, DefaultArrayPoolRentSize));
buffer = ArrayPool<byte>.Shared.Rent(rentedSize);
if (toReturn is not null)
{
Array.Copy(toReturn, buffer, toReturn.Length);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,19 @@ public static void ThrowArgumentException_CannotBeNegative(string parameterName)
{
throw new ArgumentException(SR.ArgumentException_CannotBeNegative, parameterName);
}

public static void ThrowIfNull([NotNull] object? argument, string parameterName)
{
if (argument is null)
{
ThrowArgumentNullException(parameterName);
}
}

[DoesNotReturn]
public static void ThrowArgumentOutOfRangeException(string parameterName)
{
throw new ArgumentOutOfRangeException(parameterName);
}
}
}
Loading
Loading