Skip to content

Add configurable SSE parser buffer limit - #132275

Draft
mrek-msft with Copilot wants to merge 7 commits into
mainfrom
copilot/add-sseparser-options
Draft

Add configurable SSE parser buffer limit#132275
mrek-msft with Copilot wants to merge 7 commits into
mainfrom
copilot/add-sseparser-options

Conversation

Copilot AI commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

SSE parsing now supports a caller-configured buffer limit through SseParserOptions<T>.

API

  • Adds SseParserOptions<T> with ItemParser and MaxBufferSize.
  • Sets MaxBufferSize to -1 by default, preserving the internal default limit.
  • Uses the options-based SseParser.Create overload as the parser creation path.
var parser = SseParser.Create(stream, new SseParserOptions<string>(
    static (_, bytes) => Encoding.UTF8.GetString(bytes))
{
    MaxBufferSize = 1024 * 1024
});

Coverage

  • Replaces reflection-based buffer-limit test setup with the public API.
  • Updates parser and formatter tests for the options-based construction path.

Note

This description was generated by GitHub Copilot.

Copilot AI lite review requested due to automatic review settings August 13, 2026 13:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review any files in this pull request.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
16 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

Co-authored-by: mrek-msft <188900745+mrek-msft@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 13, 2026 13:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Suppressed comments (2)

src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParserOptions.cs:28

  • SseParserOptions<T>.MaxBufferSize is documented as "-1 to use the default limit", but values < -1 currently flow through and (because of the _maxBufferSize >= 0 guard) effectively disable the limit. It would be safer to validate that MaxBufferSize is either -1 or >= 0 and throw ArgumentOutOfRangeException otherwise.
        /// <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, or -1 to use the default limit.</summary>
        public int MaxBufferSize { get; set; } = -1;

src/libraries/System.Net.ServerSentEvents/tests/SseParserTests.cs:34

  • There’s no test coverage for invalid MaxBufferSize values (e.g., < -1). Adding a focused test would ensure the new option can’t be used to silently disable buffer limiting via negative values.
        [Fact]
        public void Options_DefaultMaxBufferSize()
        {
            var options = new SseParserOptions<string>(delegate { return ""; });

            Assert.Equal(-1, options.MaxBufferSize);
        }

Copilot AI changed the title [WIP] Add option to limit SseParser's internal buffer size Add configurable SSE parser buffer limit Aug 13, 2026
Copilot AI requested a review from mrek-msft August 13, 2026 14:00
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @karelz, @dotnet/ncl
See info in area-owners.md if you want to be subscribed.

Co-authored-by: MihaZupan <25307628+MihaZupan@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 14, 2026 11:21

@MihaZupan MihaZupan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Address comments

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Suppressed comments (4)

src/libraries/System.Net.ServerSentEvents/tests/SseParserTests.cs:996

  • This helper decodes payload bytes via bytes.ToArray(), which adds an extra allocation for every parsed event. Encoding.UTF8.GetString(ReadOnlySpan) can be used directly (as in SseParser.Create(Stream)).
        private static SseParser<string> CreateParser(Stream stream) =>
            CreateParser(stream, static (_, bytes) => Encoding.UTF8.GetString(bytes.ToArray()));

src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParser.cs:14

  • The XML doc summary for the non-generic Create(Stream) overload references SseItem{T}, but this method always returns SseParser. This produces incorrect public docs/intellisense and broken cross-references.
        /// <summary>Creates a parser for parsing a <paramref name="sseStream"/> of server-sent events into a sequence of <see cref="SseItem{T}"/> values.</summary>

src/libraries/System.Net.ServerSentEvents/tests/SseParserTests.cs:916

  • These tests convert the ReadOnlySpan payload to an array before decoding. The product code path already uses Encoding.UTF8.GetString(ReadOnlySpan) directly; using ToArray() here adds per-event allocations and increases test runtime/GC pressure unnecessarily.

This issue also appears on line 995 of the same file.

            var options = new SseParserOptions<string>(static (_, bytes) => Encoding.UTF8.GetString(bytes.ToArray()))

src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParser_1.cs:565

  • MaxBufferSize enforcement currently happens only via GrowBuffer(minimumLength). That means the parser can still buffer more than the configured limit without calling GrowBuffer (e.g., initial Rent(1024) when MaxBufferSize < 1024, or when ArrayPool returns a bucket size larger than the requested minimumLength). To honor the contract, the code needs to validate the actual buffered byte counts (e.g., after incrementing _lineLength in FillLineBuffer/FillLineBufferAsync and after appending to _dataLength) against _maxBufferSize, not just the requested growth size.
        /// <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)
        {
            if (_maxBufferSize >= 0 && minimumLength > _maxBufferSize)
            {
                throw new InvalidDataException(SR.InvalidDataException_SseExceededMaxLength);
            }

Copilot AI review requested due to automatic review settings August 14, 2026 11:39
Copilot AI requested a review from MihaZupan August 14, 2026 11:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

Suppressed comments (6)

src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParser.cs:14

  • The XML doc for the non-generic Create(Stream) overload now references SseItem{T}, but this method returns SseParser and the rest of the doc/comment refers to strings. This makes the public docs misleading.
        /// <summary>Creates a parser for parsing a <paramref name="sseStream"/> of server-sent events into a sequence of <see cref="SseItem{T}"/> values.</summary>

src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParser.cs:43

  • Adding Create(Stream, SseParserOptions) introduces an overload-resolution ambiguity for callers that previously wrote SseParser.Create(stream, null) to validate argument checking; that call will now be ambiguous between the itemParser and options overloads unless the null is explicitly cast. This is a (small but real) source-compat risk that should be explicitly acknowledged/validated as acceptable.
        public static SseParser<T> Create<T>(Stream sseStream, SseParserOptions<T> options)

src/libraries/System.Net.ServerSentEvents/tests/SseParserTests.cs:25

  • Parse_InvalidArguments_Throws no longer covers the existing Create(Stream) null check, and it also no longer validates that passing a null itemParser to the itemParser overload throws with the expected parameter name (which now requires an explicit cast because of the new options overload). Keeping these checks helps ensure the public surface continues to throw consistent ArgumentNullException parameter names.
        [Fact]
        public void Parse_InvalidArguments_Throws()
        {
            AssertExtensions.Throws<ArgumentNullException>("itemParser", () => new SseParserOptions<string>(null));
            AssertExtensions.Throws<ArgumentNullException>("sseStream", () => SseParser.Create<string>(null, new SseParserOptions<string>(delegate { return ""; })));
            AssertExtensions.Throws<ArgumentNullException>("options", () => SseParser.Create<string>(Stream.Null, (SseParserOptions<string>)null));

src/libraries/System.Net.ServerSentEvents/tests/SseParserTests.cs:996

  • CreateParser's default string parser allocates via bytes.ToArray() just to decode UTF-8. Using Encoding.UTF8.GetString(ReadOnlySpan) avoids the allocation and aligns with the default parser behavior in SseParser.Create(Stream).
        private static SseParser<string> CreateParser(Stream stream) =>
            CreateParser(stream, static (_, bytes) => Encoding.UTF8.GetString(bytes.ToArray()));

src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParser_1.cs:562

  • _maxBufferSize is always non-negative (it's either DefaultMaxBufferSize or a validated MaxBufferSize value), so the "_maxBufferSize >= 0" part of this condition is redundant and can be removed to simplify the check.
            if (_maxBufferSize >= 0 && minimumLength > _maxBufferSize)

src/libraries/System.Net.ServerSentEvents/tests/SseParserTests.cs:916

  • The item parser in this test allocates via bytes.ToArray() just to decode UTF-8. Encoding.UTF8.GetString has a ReadOnlySpan overload (and the production code uses it), so this can avoid an allocation and better match real usage.
            var options = new SseParserOptions<string>(static (_, bytes) => Encoding.UTF8.GetString(bytes.ToArray()))

Co-authored-by: MihaZupan <25307628+MihaZupan@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 14, 2026 12:08
Copilot AI requested a review from MihaZupan August 14, 2026 12:09
Co-authored-by: Miha Zupan <mihazupan.zupan1@gmail.com>

@MihaZupan MihaZupan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assuming CI is happy, LGTM

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

Suppressed comments (5)

src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParser.cs:14

  • The XML doc for the non-generic Create(Stream) overload references SseItem{T}, but this overload returns SseParser and produces SseItem. This makes the public docs misleading.

This issue also appears on line 34 of the same file.

        /// <summary>Creates a parser for parsing a <paramref name="sseStream"/> of server-sent events into a sequence of <see cref="SseItem{String}"/> values.</summary>

src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParser.cs:35

  • Create(Stream, SseItemParser) now constructs SseParserOptions before validating sseStream. This changes ArgumentNullException behavior when both arguments are null (now throws for itemParser instead of sseStream), which is a potentially observable breaking change for invalid inputs. Preserve the prior validation order by checking sseStream first.
        public static SseParser<T> Create<T>(Stream sseStream, SseItemParser<T> itemParser) =>
            Create(sseStream, new SseParserOptions<T>(itemParser));

src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParserOptions.cs:24

  • MaxBufferSize's documentation doesn't specify the unit. Since this is a public API and the value is used as a byte count internally, the summary should explicitly say "bytes" to avoid ambiguity.
        /// <summary>Gets or sets the maximum buffer size, or -1 to use the default limit.</summary>
        /// <exception cref="ArgumentOutOfRangeException">The value set is less than -1.</exception>

src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParser_1.cs:98

  • With MaxBufferSize now configurable, the current line-buffer growth strategy (doubling) and initial ArrayPool rent size can make the limit imprecise or ineffective. For example, if MaxBufferSize is between two growth steps (e.g., 1500), the parser will throw early when trying to grow from 1024→2048, and if MaxBufferSize is smaller than the initial rent size (1024), the parser can buffer more than MaxBufferSize without a growth check. Consider enforcing the limit based on the actual buffered length (_lineLength/_dataLength) after reads/appends, and sizing growth based on required length rather than the next doubling step.
            _stream = stream;
            _itemParser = options.ItemParser;
            _maxBufferSize = options.MaxBufferSize == -1 ? DefaultMaxBufferSize : options.MaxBufferSize;

src/libraries/System.Net.ServerSentEvents/tests/SseParserTests.cs:25

  • Parse_InvalidArguments_Throws no longer covers argument validation for existing public overloads (e.g., Create(null) and Create(stream, null itemParser)). This reduces coverage for parameter validation behavior that should remain stable alongside the new options overload.
        public void Parse_InvalidArguments_Throws()
        {
            AssertExtensions.Throws<ArgumentNullException>("itemParser", () => new SseParserOptions<string>(null));
            AssertExtensions.Throws<ArgumentNullException>("sseStream", () => SseParser.Create<string>(null, new SseParserOptions<string>(delegate { return ""; })));
            AssertExtensions.Throws<ArgumentNullException>("options", () => SseParser.Create<string>(Stream.Null, (SseParserOptions<string>)null));

Copilot AI review requested due to automatic review settings August 14, 2026 12:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Suppressed comments (2)

src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParser.cs:35

  • Create<T>(Stream, SseItemParser<T>) now constructs SseParserOptions<T> before validating sseStream, which changes the exception behavior when both sseStream and itemParser are null (it will throw for itemParser first). For consistency with the other overload and to preserve prior argument-validation order, validate sseStream before creating the options object.
        public static SseParser<T> Create<T>(Stream sseStream, SseItemParser<T> itemParser) =>
            Create(sseStream, new SseParserOptions<T>(itemParser));

src/libraries/System.Net.ServerSentEvents/tests/SseParserTests.cs:26

  • Parse_InvalidArguments_Throws no longer covers the SseParser.Create(Stream) null-check or the Create<T>(Stream, SseItemParser<T>) null itemParser case. Keeping these assertions helps ensure all public overloads continue to validate arguments and preserve parameter names.
        [Fact]
        public void Parse_InvalidArguments_Throws()
        {
            AssertExtensions.Throws<ArgumentNullException>("itemParser", () => new SseParserOptions<string>(null));
            AssertExtensions.Throws<ArgumentNullException>("sseStream", () => SseParser.Create<string>(null, new SseParserOptions<string>(delegate { return ""; })));
            AssertExtensions.Throws<ArgumentNullException>("options", () => SseParser.Create<string>(Stream.Null, (SseParserOptions<string>)null));
        }

Comment on lines +94 to 99
internal SseParser(Stream stream, SseParserOptions<T> options)
{
_stream = stream;
_itemParser = itemParser;
_itemParser = options.ItemParser;
_maxBufferSize = options.MaxBufferSize == -1 ? DefaultMaxBufferSize : options.MaxBufferSize;
}
Comment on lines -29 to -30
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; }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot we can't remove preexisting Create methods. Bring them back.


memoryStream.Position = 0;
int count = 0;
foreach (SseItem<byte[]> item in SseParser.Create(memoryStream, (eventType, data) => data.ToArray()).Enumerate())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to change it? All existing create methods remained unchanged and there is nothing in the test exercising new limiting capability.

@copilot consier undoing changes in this test file.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverted in 0fe7c83 — this test file is now identical to its pre-PR state.

Note

This reply was generated by GitHub Copilot.

[Fact]
public async Task Parse_Sync_SupportsOnlyOneEnumeration_Throws()
{
SseParser<string> parser = SseParser.Create(Stream.Null);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar in this file. No need to wrap SseParser.Create. Test should pass unmodified. Just add new teste testing new Create method and limiting capability. Do not touch preexisting tests.

@copilot please rework.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reworked in 0fe7c83 — removed the CreateParser wrapper, all preexisting tests now call SseParser.Create unchanged, and only additive tests/assertions were added for the new options-based API (plus one required disambiguation of a null argument since the new overload makes it ambiguous).

Note

This reply was generated by GitHub Copilot.

Co-authored-by: mrek-msft <188900745+mrek-msft@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 14, 2026 13:09

Copilot AI commented Aug 14, 2026

Copy link
Copy Markdown
Contributor Author

@copilot Address comments

Addressed in 0fe7c83: reverted unnecessary test wrapping per @mrek-msft's comments, and fixed the MaxBufferSize enforcement bug (limit is now checked against actual buffered bytes after each read, not just the doubled next-capacity estimate).

Note

This reply was generated by GitHub Copilot.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (4)

src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParser_1.cs:526

  • MaxBufferSize is enforced only after reading into the line buffer. Because Stream.Read is given the full remaining buffer, a single read can pull in more than the configured limit (especially if ArrayPool returns an array larger than requested), and only then throws. Cap the read size to the remaining allowed bytes (and throw when already at the cap) so the limit is enforced before reading past it.
                _lineLength += bytesRead;
                if (_lineLength > _maxBufferSize)
                {
                    throw new InvalidDataException(SR.InvalidDataException_SseExceededMaxLength);

src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParser_1.cs:551

  • MaxBufferSize is enforced only after the async read completes. A single ReadAsync can read more than the configured limit (especially if the rented array is larger than requested), and only then throws. Cap the async read size to the remaining allowed bytes (and throw when already at the cap) so the limit is enforced before reading past it.
                _lineLength += bytesRead;
                if (_lineLength > _maxBufferSize)
                {
                    throw new InvalidDataException(SR.InvalidDataException_SseExceededMaxLength);

src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParserOptions.cs:33

  • MaxBufferSize currently allows 0, which can result in renting a 0-length buffer and the parser treating a non-empty stream as EOF without reading (since Stream.Read on an empty span returns 0). Consider rejecting 0 and requiring either -1 (default) or a positive size in bytes.
        /// <summary>Gets or sets the maximum buffer size, or -1 to use the default limit.</summary>
        /// <exception cref="ArgumentOutOfRangeException">The value set is less than -1.</exception>
        public int MaxBufferSize

src/libraries/System.Net.ServerSentEvents/tests/SseParserTests.cs:919

  • This test uses bytes.ToArray() just to decode UTF-8, which adds an avoidable allocation. Encoding.UTF8.GetString supports decoding directly from a ReadOnlySpan<byte> in this codebase, so the copy isn't needed.
            var options = new SseParserOptions<string>(static (_, bytes) => Encoding.UTF8.GetString(bytes.ToArray()))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

[API Proposal]: Option to limit SseParser's internal buffer size

5 participants