From 1f57ec12be8eaf0087794396feda67ba3b540132 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 13 Aug 2026 13:11:59 +0000 Subject: [PATCH 1/8] Initial plan From 5984a757431d3cba5c8d6cb792c7c9e39a1f7ce4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 13 Aug 2026 13:46:48 +0000 Subject: [PATCH 2/8] Add SseParser buffer options Co-authored-by: mrek-msft <188900745+mrek-msft@users.noreply.github.com> --- .../ref/System.Net.ServerSentEvents.cs | 9 ++- .../src/System.Net.ServerSentEvents.csproj | 1 + .../System/Net/ServerSentEvents/SseParser.cs | 28 ++------ .../Net/ServerSentEvents/SseParserOptions.cs | 29 ++++++++ .../Net/ServerSentEvents/SseParser_1.cs | 13 ++-- .../tests/SseFormatterTests.cs | 4 +- .../tests/SseParserTests.cs | 69 ++++++++++--------- 7 files changed, 91 insertions(+), 62 deletions(-) create mode 100644 src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParserOptions.cs diff --git a/src/libraries/System.Net.ServerSentEvents/ref/System.Net.ServerSentEvents.cs b/src/libraries/System.Net.ServerSentEvents/ref/System.Net.ServerSentEvents.cs index 1fb04dfab99c27..eddaa21ecb9d8d 100644 --- a/src/libraries/System.Net.ServerSentEvents/ref/System.Net.ServerSentEvents.cs +++ b/src/libraries/System.Net.ServerSentEvents/ref/System.Net.ServerSentEvents.cs @@ -26,8 +26,13 @@ public readonly partial struct SseItem public static partial class SseParser { public const string EventTypeDefault = "message"; - public static System.Net.ServerSentEvents.SseParser Create(System.IO.Stream sseStream) { throw null; } - public static System.Net.ServerSentEvents.SseParser Create(System.IO.Stream sseStream, System.Net.ServerSentEvents.SseItemParser itemParser) { throw null; } + public static System.Net.ServerSentEvents.SseParser Create(System.IO.Stream sseStream, System.Net.ServerSentEvents.SseParserOptions options) { throw null; } + } + public sealed partial class SseParserOptions + { + public SseParserOptions(System.Net.ServerSentEvents.SseItemParser itemParser) { } + public System.Net.ServerSentEvents.SseItemParser ItemParser { get { throw null; } } + public int MaxBufferSize { get { throw null; } set { } } } public sealed partial class SseParser { diff --git a/src/libraries/System.Net.ServerSentEvents/src/System.Net.ServerSentEvents.csproj b/src/libraries/System.Net.ServerSentEvents/src/System.Net.ServerSentEvents.csproj index 223c7982164693..5679e432168200 100644 --- a/src/libraries/System.Net.ServerSentEvents/src/System.Net.ServerSentEvents.csproj +++ b/src/libraries/System.Net.ServerSentEvents/src/System.Net.ServerSentEvents.csproj @@ -20,6 +20,7 @@ System.Net.ServerSentEvents.SseParser + diff --git a/src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParser.cs b/src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParser.cs index 1ea0378296c4c9..cd78d54b9007e2 100644 --- a/src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParser.cs +++ b/src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParser.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.IO; -using System.Text; - namespace System.Net.ServerSentEvents { /// Provides a parser for parsing server-sent events. @@ -12,39 +10,25 @@ public static class SseParser /// The default ("message") for an event that did not explicitly specify a type. public const string EventTypeDefault = "message"; - /// Creates a parser for parsing a of server-sent events into a sequence of values. - /// The stream containing the data to parse. - /// - /// The enumerable of strings, which can be enumerated synchronously or asynchronously. The strings - /// are decoded from the UTF8-encoded bytes of the payload of each event. - /// - /// is null. - /// - /// This overload has behavior equivalent to calling with a delegate - /// that decodes the data of each event using 's GetString method. - /// - public static SseParser Create(Stream sseStream) => - Create(sseStream, static (_, bytes) => Encoding.UTF8.GetString(bytes)); - /// Creates a parser for parsing a of server-sent events into a sequence of values. /// Specifies the type of data in each event. /// The stream containing the data to parse. - /// The parser to use to transform each payload of bytes into a data element. + /// The options to use when parsing the stream. /// The enumerable, which can be enumerated synchronously or asynchronously. - /// or is null. - public static SseParser Create(Stream sseStream, SseItemParser itemParser) + /// or is null. + public static SseParser Create(Stream sseStream, SseParserOptions options) { if (sseStream is null) { ThrowHelper.ThrowArgumentNullException(nameof(sseStream)); } - if (itemParser is null) + if (options is null) { - ThrowHelper.ThrowArgumentNullException(nameof(itemParser)); + ThrowHelper.ThrowArgumentNullException(nameof(options)); } - return new SseParser(sseStream, itemParser); + return new SseParser(sseStream, options); } } } diff --git a/src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParserOptions.cs b/src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParserOptions.cs new file mode 100644 index 00000000000000..caf81f65f52f24 --- /dev/null +++ b/src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParserOptions.cs @@ -0,0 +1,29 @@ +// 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 +{ + /// Provides options for parsing server-sent events. + /// Specifies the type of data parsed from an event. + public sealed class SseParserOptions + { + /// Initializes a new instance of the class. + /// The parser to use to transform each payload of bytes into a data element. + /// is null. + public SseParserOptions(SseItemParser itemParser) + { + if (itemParser is null) + { + ThrowHelper.ThrowArgumentNullException(nameof(itemParser)); + } + + ItemParser = itemParser; + } + + /// Gets the parser to use to transform each payload of bytes into a data element. + public SseItemParser ItemParser { get; } + + /// Gets or sets the maximum buffer size, or -1 to use the default limit. + public int MaxBufferSize { get; set; } = -1; + } +} diff --git a/src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParser_1.cs b/src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParser_1.cs index 0e80d774d21bec..909534ff6451aa 100644 --- a/src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParser_1.cs +++ b/src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParser_1.cs @@ -40,6 +40,8 @@ public sealed class SseParser #else 1024; #endif + /// The maximum amount of data buffered by default. + private const int DefaultMaxBufferSize = 1024 * 1024 * 1024; /// The stream to be parsed. private readonly Stream _stream; @@ -74,7 +76,7 @@ public sealed class SseParser /// This can be different than != 0 if empty data was appended. private bool _dataAppended; - private int _maxBufferSize = 1024 * 1024 * 1024; + private readonly int _maxBufferSize; /// The event type for the next event. private string? _eventType; @@ -87,11 +89,12 @@ public sealed class SseParser /// Initialize the enumerable. /// The stream to parse. - /// The function to use to parse payload bytes into a . - internal SseParser(Stream stream, SseItemParser itemParser) + /// The options to use to parse the stream. + internal SseParser(Stream stream, SseParserOptions options) { _stream = stream; - _itemParser = itemParser; + _itemParser = options.ItemParser; + _maxBufferSize = options.MaxBufferSize == -1 ? DefaultMaxBufferSize : options.MaxBufferSize; } /// Gets an enumerable of the server-sent events from this parser. @@ -556,7 +559,7 @@ private void SkipBomIfPresent() /// Grows the buffer, returning the existing one to the ArrayPool and renting an ArrayPool replacement. private void GrowBuffer([NotNull] ref byte[]? buffer, int minimumLength) { - if (minimumLength > _maxBufferSize) + if (_maxBufferSize >= 0 && minimumLength > _maxBufferSize) { throw new InvalidDataException(SR.InvalidDataException_SseExceededMaxLength); } diff --git a/src/libraries/System.Net.ServerSentEvents/tests/SseFormatterTests.cs b/src/libraries/System.Net.ServerSentEvents/tests/SseFormatterTests.cs index 9454d7db4972bb..58d71143f7ca5b 100644 --- a/src/libraries/System.Net.ServerSentEvents/tests/SseFormatterTests.cs +++ b/src/libraries/System.Net.ServerSentEvents/tests/SseFormatterTests.cs @@ -176,7 +176,7 @@ public static async Task WriteLargeItems_DataWrittenSuccessfully() memoryStream.Position = 0; int count = 0; - foreach (SseItem item in SseParser.Create(memoryStream, (eventType, data) => data.ToArray()).Enumerate()) + foreach (SseItem item in SseParser.Create(memoryStream, new SseParserOptions((eventType, data) => data.ToArray())).Enumerate()) { Assert.Equal(expected, item.Data); count++; @@ -201,7 +201,7 @@ public static async Task WriteAsync_ParserCanRoundtripJsonEvents() await SseFormatter.WriteAsync(GetItemsAsync(), stream, FormatJson); stream.Position = 0; - SseParser parser = SseParser.Create(stream, ParseJson); + SseParser parser = SseParser.Create(stream, new SseParserOptions(ParseJson)); await ValidateParseResults(parser.EnumerateAsync()); async IAsyncEnumerable> GetItemsAsync() diff --git a/src/libraries/System.Net.ServerSentEvents/tests/SseParserTests.cs b/src/libraries/System.Net.ServerSentEvents/tests/SseParserTests.cs index 9e64a473cd61eb..dd9711db1d2a1d 100644 --- a/src/libraries/System.Net.ServerSentEvents/tests/SseParserTests.cs +++ b/src/libraries/System.Net.ServerSentEvents/tests/SseParserTests.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Reflection; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; @@ -21,15 +20,23 @@ public partial class SseParserTests [Fact] public void Parse_InvalidArguments_Throws() { - AssertExtensions.Throws("sseStream", () => SseParser.Create(null)); - AssertExtensions.Throws("sseStream", () => SseParser.Create(null, delegate { return ""; })); - AssertExtensions.Throws("itemParser", () => SseParser.Create(Stream.Null, null)); + AssertExtensions.Throws("itemParser", () => new SseParserOptions(null)); + AssertExtensions.Throws("sseStream", () => SseParser.Create(null, new SseParserOptions(delegate { return ""; }))); + AssertExtensions.Throws("options", () => SseParser.Create(Stream.Null, null)); + } + + [Fact] + public void Options_DefaultMaxBufferSize() + { + var options = new SseParserOptions(delegate { return ""; }); + + Assert.Equal(-1, options.MaxBufferSize); } [Fact] public async Task Parse_Sync_SupportsOnlyOneEnumeration_Throws() { - SseParser parser = SseParser.Create(Stream.Null); + SseParser parser = CreateParser(Stream.Null); parser.Enumerate().GetEnumerator().MoveNext(); var e = parser.Enumerate().GetEnumerator(); var ea = parser.EnumerateAsync().GetAsyncEnumerator(); @@ -40,7 +47,7 @@ public async Task Parse_Sync_SupportsOnlyOneEnumeration_Throws() [Fact] public async Task Parse_Async_SupportsOnlyOneEnumeration_Throws() { - SseParser parser = SseParser.Create(Stream.Null); + SseParser parser = CreateParser(Stream.Null); await parser.EnumerateAsync().GetAsyncEnumerator().MoveNextAsync(); var ea = parser.EnumerateAsync().GetAsyncEnumerator(); var e = parser.Enumerate().GetEnumerator(); @@ -210,7 +217,7 @@ public async Task Parse_HtmlSpec_Example4(string newline, bool trickle, bool use $"{newline}", trickle); - SseParser parser = SseParser.Create(stream); + SseParser parser = CreateParser(stream); if (useAsync) { Assert.Equal(string.Empty, parser.LastEventId); @@ -266,7 +273,7 @@ public async Task Parse_HtmlSpec_Example4_InheritedIDs(string newline, bool tric $"{newline}", trickle); - SseParser parser = SseParser.Create(stream); + SseParser parser = CreateParser(stream); if (useAsync) { Assert.Equal(string.Empty, parser.LastEventId); @@ -416,7 +423,7 @@ public async Task Retry_SetsReconnectionInterval(string newline, bool trickle, b $"{newline}", trickle); - SseParser parser = SseParser.Create(stream); + SseParser parser = CreateParser(stream); Assert.Equal(Timeout.InfiniteTimeSpan, parser.ReconnectionInterval); if (useAsync) @@ -699,7 +706,7 @@ public async Task Delegate_ThrowsException_Propagates(string newline, bool trick { using Stream stream = GetStream($"data: hello{newline}{newline}data:world{newline}{newline}", trickle); - SseParser parser = SseParser.Create(stream, (eventType, bytes) => throw new FormatException(Encoding.UTF8.GetString(bytes.ToArray()))); + SseParser parser = CreateParser(stream, (eventType, bytes) => throw new FormatException(Encoding.UTF8.GetString(bytes.ToArray()))); FormatException fe; if (useAsync) @@ -723,7 +730,7 @@ public async Task Cancellation_Propagates(bool cancelEnumerator) { using Stream stream = GetStream($"data: hello\n\ndata:world\n\n", trickle: true); - SseParser parser = SseParser.Create(stream); + SseParser parser = CreateParser(stream); var cts = new CancellationTokenSource(); cts.Cancel(); @@ -740,7 +747,7 @@ public void NonGenericEnumerator_ProducesExpectedItems() { using Stream stream = GetStream($"data: hello\n\ndata:world\n\n", trickle: false); - IEnumerable sse = SseParser.Create(stream).Enumerate(); + IEnumerable sse = CreateParser(stream).Enumerate(); IEnumerator e = sse.GetEnumerator(); Assert.True(e.MoveNext()); @@ -807,7 +814,7 @@ public async Task ArrayPoolRental_PerItem(string newline, bool trickle, bool use int count = 0; if (useAsync) { - foreach (var e in SseParser.Create(stream, itemParser).Enumerate()) + foreach (var e in CreateParser(stream, itemParser).Enumerate()) { try { @@ -826,7 +833,7 @@ public async Task ArrayPoolRental_PerItem(string newline, bool trickle, bool use } else { - await foreach (var e in SseParser.Create(stream, itemParser).EnumerateAsync()) + await foreach (var e in CreateParser(stream, itemParser).EnumerateAsync()) { try { @@ -875,7 +882,7 @@ public async Task ArrayPoolRental_Closure(string newline, bool trickle, bool use int count = 0; if (useAsync) { - foreach (var e in SseParser.Create(stream, itemParser).Enumerate()) + foreach (var e in CreateParser(stream, itemParser).Enumerate()) { if ("[DONE]"u8.SequenceEqual(e.Data.Span)) { @@ -886,7 +893,7 @@ public async Task ArrayPoolRental_Closure(string newline, bool trickle, bool use } else { - await foreach (var e in SseParser.Create(stream, itemParser).EnumerateAsync()) + await foreach (var e in CreateParser(stream, itemParser).EnumerateAsync()) { if ("[DONE]"u8.SequenceEqual(e.Data.Span)) { @@ -905,18 +912,12 @@ public async Task ArrayPoolRental_Closure(string newline, bool trickle, bool use [MemberData(nameof(NewlineAsyncData))] public async Task Parse_LongLineCap_Throws(string newline, bool useAsync) { - // Temporary workaround until we expose limit in public API - void ReduceLineLengthLimit(SseParser parser) - { - Type type = typeof(SseParser); - var field = type.GetField("_maxBufferSize", BindingFlags.Instance | BindingFlags.NonPublic); - Assert.NotNull(field); - field.SetValue(parser, 10 * 1024); - } - using Stream stream = new InfiniteLineStream($"data: shortline{newline}{newline}data: "); - var parser = SseParser.Create(stream); - ReduceLineLengthLimit(parser); + var options = new SseParserOptions(static (_, bytes) => Encoding.UTF8.GetString(bytes.ToArray())) + { + MaxBufferSize = 10 * 1024 + }; + var parser = SseParser.Create(stream, options); if (useAsync) { @@ -961,18 +962,18 @@ private static Stream GetStream(byte[] bytes, bool trickle) => private static List> ReadAllEvents(Stream stream) { - return new List>(SseParser.Create(stream).Enumerate()); + return new List>(CreateParser(stream).Enumerate()); } private static List> ReadAllEvents(Stream stream, SseItemParser parser) { - return new List>(SseParser.Create(stream, parser).Enumerate()); + return new List>(CreateParser(stream, parser).Enumerate()); } private static async Task>> ReadAllEventsAsync(Stream stream, SseItemParser parser) { var list = new List>(); - await foreach (SseItem item in SseParser.Create(stream, parser).EnumerateAsync()) + await foreach (SseItem item in CreateParser(stream, parser).EnumerateAsync()) { list.Add(item); } @@ -983,7 +984,7 @@ private static async Task>> ReadAllEventsAsync(Stream stream, private static async Task>> ReadAllEventsAsync(Stream stream) { var list = new List>(); - await foreach (SseItem item in SseParser.Create(stream).EnumerateAsync()) + await foreach (SseItem item in CreateParser(stream).EnumerateAsync()) { list.Add(item); } @@ -991,6 +992,12 @@ private static async Task>> ReadAllEventsAsync(Stream strea return list; } + private static SseParser CreateParser(Stream stream) => + CreateParser(stream, static (_, bytes) => Encoding.UTF8.GetString(bytes.ToArray())); + + private static SseParser CreateParser(Stream stream, SseItemParser itemParser) => + SseParser.Create(stream, new SseParserOptions(itemParser)); + /// Stream where each read reads at most one byte and where every asynchronous operation yields. private sealed class TrickleStream : MemoryStream { From 681f7970d294500b7ec8544f135a48da284252f0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 14 Aug 2026 11:21:19 +0000 Subject: [PATCH 3/8] Restore existing SseParser Create overloads Co-authored-by: MihaZupan <25307628+MihaZupan@users.noreply.github.com> --- .../ref/System.Net.ServerSentEvents.cs | 2 ++ .../System/Net/ServerSentEvents/SseParser.cs | 24 +++++++++++++++++++ .../tests/SseParserTests.cs | 2 +- 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Net.ServerSentEvents/ref/System.Net.ServerSentEvents.cs b/src/libraries/System.Net.ServerSentEvents/ref/System.Net.ServerSentEvents.cs index eddaa21ecb9d8d..c585900df3f8cb 100644 --- a/src/libraries/System.Net.ServerSentEvents/ref/System.Net.ServerSentEvents.cs +++ b/src/libraries/System.Net.ServerSentEvents/ref/System.Net.ServerSentEvents.cs @@ -26,6 +26,8 @@ public readonly partial struct SseItem public static partial class SseParser { public const string EventTypeDefault = "message"; + public static System.Net.ServerSentEvents.SseParser Create(System.IO.Stream sseStream) { throw null; } + public static System.Net.ServerSentEvents.SseParser Create(System.IO.Stream sseStream, System.Net.ServerSentEvents.SseItemParser itemParser) { throw null; } public static System.Net.ServerSentEvents.SseParser Create(System.IO.Stream sseStream, System.Net.ServerSentEvents.SseParserOptions options) { throw null; } } public sealed partial class SseParserOptions diff --git a/src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParser.cs b/src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParser.cs index cd78d54b9007e2..c883d1f811b607 100644 --- a/src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParser.cs +++ b/src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParser.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.IO; +using System.Text; namespace System.Net.ServerSentEvents { /// Provides a parser for parsing server-sent events. @@ -10,6 +11,29 @@ public static class SseParser /// The default ("message") for an event that did not explicitly specify a type. public const string EventTypeDefault = "message"; + /// Creates a parser for parsing a of server-sent events into a sequence of values. + /// The stream containing the data to parse. + /// + /// The enumerable of strings, which can be enumerated synchronously or asynchronously. The strings + /// are decoded from the UTF8-encoded bytes of the payload of each event. + /// + /// is null. + /// + /// This overload has behavior equivalent to calling with a delegate + /// that decodes the data of each event using 's GetString method. + /// + public static SseParser Create(Stream sseStream) => + Create(sseStream, static (_, bytes) => Encoding.UTF8.GetString(bytes)); + + /// Creates a parser for parsing a of server-sent events into a sequence of values. + /// Specifies the type of data in each event. + /// The stream containing the data to parse. + /// The parser to use to transform each payload of bytes into a data element. + /// The enumerable, which can be enumerated synchronously or asynchronously. + /// or is null. + public static SseParser Create(Stream sseStream, SseItemParser itemParser) => + Create(sseStream, new SseParserOptions(itemParser)); + /// Creates a parser for parsing a of server-sent events into a sequence of values. /// Specifies the type of data in each event. /// The stream containing the data to parse. diff --git a/src/libraries/System.Net.ServerSentEvents/tests/SseParserTests.cs b/src/libraries/System.Net.ServerSentEvents/tests/SseParserTests.cs index dd9711db1d2a1d..39dc993c3cadba 100644 --- a/src/libraries/System.Net.ServerSentEvents/tests/SseParserTests.cs +++ b/src/libraries/System.Net.ServerSentEvents/tests/SseParserTests.cs @@ -22,7 +22,7 @@ public void Parse_InvalidArguments_Throws() { AssertExtensions.Throws("itemParser", () => new SseParserOptions(null)); AssertExtensions.Throws("sseStream", () => SseParser.Create(null, new SseParserOptions(delegate { return ""; }))); - AssertExtensions.Throws("options", () => SseParser.Create(Stream.Null, null)); + AssertExtensions.Throws("options", () => SseParser.Create(Stream.Null, (SseParserOptions)null)); } [Fact] From eb4705907f8fe244b83306ff3396e856c6b988e6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 14 Aug 2026 11:39:40 +0000 Subject: [PATCH 4/8] Address review: use ArgumentNullException.ThrowIfNull and validate MaxBufferSize setter Co-authored-by: MihaZupan <25307628+MihaZupan@users.noreply.github.com> --- .../Net/ServerSentEvents/SseParserOptions.cs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParserOptions.cs b/src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParserOptions.cs index caf81f65f52f24..2a10c10e1b5653 100644 --- a/src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParserOptions.cs +++ b/src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParserOptions.cs @@ -12,10 +12,7 @@ public sealed class SseParserOptions /// is null. public SseParserOptions(SseItemParser itemParser) { - if (itemParser is null) - { - ThrowHelper.ThrowArgumentNullException(nameof(itemParser)); - } + ArgumentNullException.ThrowIfNull(itemParser); ItemParser = itemParser; } @@ -24,6 +21,17 @@ public SseParserOptions(SseItemParser itemParser) public SseItemParser ItemParser { get; } /// Gets or sets the maximum buffer size, or -1 to use the default limit. - public int MaxBufferSize { get; set; } = -1; + /// The value set is less than -1. + public int MaxBufferSize + { + get => _maxBufferSize; + set + { + ArgumentOutOfRangeException.ThrowIfLessThan(value, -1); + _maxBufferSize = value; + } + } + + private int _maxBufferSize = -1; } } From fee9f0ccbbc0a5d0eda60c3ac82dd5acac02ddac Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 14 Aug 2026 12:08:15 +0000 Subject: [PATCH 5/8] Address SSE parser review feedback Co-authored-by: MihaZupan <25307628+MihaZupan@users.noreply.github.com> --- .../src/System/Net/ServerSentEvents/SseParser.cs | 11 ++--------- .../src/System/Net/ServerSentEvents/SseParser_1.cs | 2 +- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParser.cs b/src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParser.cs index c883d1f811b607..7f34ec9c5f15f9 100644 --- a/src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParser.cs +++ b/src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParser.cs @@ -42,15 +42,8 @@ public static SseParser Create(Stream sseStream, SseItemParser itemPars /// or is null. public static SseParser Create(Stream sseStream, SseParserOptions options) { - if (sseStream is null) - { - ThrowHelper.ThrowArgumentNullException(nameof(sseStream)); - } - - if (options is null) - { - ThrowHelper.ThrowArgumentNullException(nameof(options)); - } + ArgumentNullException.ThrowIfNull(sseStream); + ArgumentNullException.ThrowIfNull(options); return new SseParser(sseStream, options); } diff --git a/src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParser_1.cs b/src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParser_1.cs index 909534ff6451aa..463cae877e7a63 100644 --- a/src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParser_1.cs +++ b/src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParser_1.cs @@ -559,7 +559,7 @@ private void SkipBomIfPresent() /// Grows the buffer, returning the existing one to the ArrayPool and renting an ArrayPool replacement. private void GrowBuffer([NotNull] ref byte[]? buffer, int minimumLength) { - if (_maxBufferSize >= 0 && minimumLength > _maxBufferSize) + if (minimumLength > _maxBufferSize) { throw new InvalidDataException(SR.InvalidDataException_SseExceededMaxLength); } From d60b3198c2698e38d79ede8ea959c165d676c9c8 Mon Sep 17 00:00:00 2001 From: Miha Zupan Date: Fri, 14 Aug 2026 14:12:45 +0200 Subject: [PATCH 6/8] Apply suggestions from code review Co-authored-by: Miha Zupan --- .../src/System/Net/ServerSentEvents/SseParser.cs | 2 +- .../src/System/Net/ServerSentEvents/SseParser_1.cs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParser.cs b/src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParser.cs index 7f34ec9c5f15f9..c6eee56897ce42 100644 --- a/src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParser.cs +++ b/src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParser.cs @@ -11,7 +11,7 @@ public static class SseParser /// The default ("message") for an event that did not explicitly specify a type. public const string EventTypeDefault = "message"; - /// Creates a parser for parsing a of server-sent events into a sequence of values. + /// Creates a parser for parsing a of server-sent events into a sequence of values. /// The stream containing the data to parse. /// /// The enumerable of strings, which can be enumerated synchronously or asynchronously. The strings diff --git a/src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParser_1.cs b/src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParser_1.cs index 463cae877e7a63..a9674081c3e2dd 100644 --- a/src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParser_1.cs +++ b/src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParser_1.cs @@ -40,6 +40,7 @@ public sealed class SseParser #else 1024; #endif + /// The maximum amount of data buffered by default. private const int DefaultMaxBufferSize = 1024 * 1024 * 1024; From 0fe7c8360302474dd7ebd5adfdc6318a7f5683ef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 14 Aug 2026 13:09:41 +0000 Subject: [PATCH 7/8] Revert unrelated test wrapping and fix MaxBufferSize enforcement bug Co-authored-by: mrek-msft <188900745+mrek-msft@users.noreply.github.com> --- .../Net/ServerSentEvents/SseParser_1.cs | 24 +++++++++-- .../tests/SseFormatterTests.cs | 4 +- .../tests/SseParserTests.cs | 41 +++++++++---------- 3 files changed, 42 insertions(+), 27 deletions(-) diff --git a/src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParser_1.cs b/src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParser_1.cs index a9674081c3e2dd..a2b11dade6217c 100644 --- a/src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParser_1.cs +++ b/src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParser_1.cs @@ -108,7 +108,7 @@ public IEnumerable> Enumerate() // Rent a line buffer. This will grow as needed. The line buffer is what's passed to the stream, // so we want it to be large enough to reduce the number of reads we need to do when data is // arriving quickly. (In debug, we use a smaller buffer to stress the growth and shifting logic.) - _lineBuffer = ArrayPool.Shared.Rent(DefaultArrayPoolRentSize); + _lineBuffer = ArrayPool.Shared.Rent(Math.Min(DefaultArrayPoolRentSize, _maxBufferSize)); try { // Spec: "Event streams in this format must always be encoded as UTF-8". @@ -188,7 +188,7 @@ public async IAsyncEnumerable> EnumerateAsync([EnumeratorCancellation // Rent a line buffer. This will grow as needed. The line buffer is what's passed to the stream, // so we want it to be large enough to reduce the number of reads we need to do when data is // arriving quickly. (In debug, we use a smaller buffer to stress the growth and shifting logic.) - _lineBuffer = ArrayPool.Shared.Rent(DefaultArrayPoolRentSize); + _lineBuffer = ArrayPool.Shared.Rent(Math.Min(DefaultArrayPoolRentSize, _maxBufferSize)); try { // Spec: "Event streams in this format must always be encoded as UTF-8". @@ -310,6 +310,11 @@ private void ShiftOrGrowLineBufferIfNecessary() } else if (_lineLength == _lineBuffer.Length) { + if (_lineLength >= _maxBufferSize) + { + throw new InvalidDataException(SR.InvalidDataException_SseExceededMaxLength); + } + int newLength; try { @@ -317,7 +322,12 @@ private void ShiftOrGrowLineBufferIfNecessary() } catch (OverflowException) { - throw new InvalidDataException(SR.InvalidDataException_SseExceededMaxLength); + newLength = int.MaxValue; + } + + if (newLength > _maxBufferSize) + { + newLength = _maxBufferSize; } GrowBuffer(ref _lineBuffer, newLength); @@ -511,6 +521,10 @@ private int FillLineBuffer() if (bytesRead > 0) { _lineLength += bytesRead; + if (_lineLength > _maxBufferSize) + { + throw new InvalidDataException(SR.InvalidDataException_SseExceededMaxLength); + } } else { @@ -532,6 +546,10 @@ private async ValueTask FillLineBufferAsync(CancellationToken cancellationT if (bytesRead > 0) { _lineLength += bytesRead; + if (_lineLength > _maxBufferSize) + { + throw new InvalidDataException(SR.InvalidDataException_SseExceededMaxLength); + } } else { diff --git a/src/libraries/System.Net.ServerSentEvents/tests/SseFormatterTests.cs b/src/libraries/System.Net.ServerSentEvents/tests/SseFormatterTests.cs index 58d71143f7ca5b..9454d7db4972bb 100644 --- a/src/libraries/System.Net.ServerSentEvents/tests/SseFormatterTests.cs +++ b/src/libraries/System.Net.ServerSentEvents/tests/SseFormatterTests.cs @@ -176,7 +176,7 @@ public static async Task WriteLargeItems_DataWrittenSuccessfully() memoryStream.Position = 0; int count = 0; - foreach (SseItem item in SseParser.Create(memoryStream, new SseParserOptions((eventType, data) => data.ToArray())).Enumerate()) + foreach (SseItem item in SseParser.Create(memoryStream, (eventType, data) => data.ToArray()).Enumerate()) { Assert.Equal(expected, item.Data); count++; @@ -201,7 +201,7 @@ public static async Task WriteAsync_ParserCanRoundtripJsonEvents() await SseFormatter.WriteAsync(GetItemsAsync(), stream, FormatJson); stream.Position = 0; - SseParser parser = SseParser.Create(stream, new SseParserOptions(ParseJson)); + SseParser parser = SseParser.Create(stream, ParseJson); await ValidateParseResults(parser.EnumerateAsync()); async IAsyncEnumerable> GetItemsAsync() diff --git a/src/libraries/System.Net.ServerSentEvents/tests/SseParserTests.cs b/src/libraries/System.Net.ServerSentEvents/tests/SseParserTests.cs index 39dc993c3cadba..d1afd5ee6e4e56 100644 --- a/src/libraries/System.Net.ServerSentEvents/tests/SseParserTests.cs +++ b/src/libraries/System.Net.ServerSentEvents/tests/SseParserTests.cs @@ -20,6 +20,9 @@ public partial class SseParserTests [Fact] public void Parse_InvalidArguments_Throws() { + AssertExtensions.Throws("sseStream", () => SseParser.Create(null)); + AssertExtensions.Throws("sseStream", () => SseParser.Create(null, delegate { return ""; })); + AssertExtensions.Throws("itemParser", () => SseParser.Create(Stream.Null, (SseItemParser)null)); AssertExtensions.Throws("itemParser", () => new SseParserOptions(null)); AssertExtensions.Throws("sseStream", () => SseParser.Create(null, new SseParserOptions(delegate { return ""; }))); AssertExtensions.Throws("options", () => SseParser.Create(Stream.Null, (SseParserOptions)null)); @@ -36,7 +39,7 @@ public void Options_DefaultMaxBufferSize() [Fact] public async Task Parse_Sync_SupportsOnlyOneEnumeration_Throws() { - SseParser parser = CreateParser(Stream.Null); + SseParser parser = SseParser.Create(Stream.Null); parser.Enumerate().GetEnumerator().MoveNext(); var e = parser.Enumerate().GetEnumerator(); var ea = parser.EnumerateAsync().GetAsyncEnumerator(); @@ -47,7 +50,7 @@ public async Task Parse_Sync_SupportsOnlyOneEnumeration_Throws() [Fact] public async Task Parse_Async_SupportsOnlyOneEnumeration_Throws() { - SseParser parser = CreateParser(Stream.Null); + SseParser parser = SseParser.Create(Stream.Null); await parser.EnumerateAsync().GetAsyncEnumerator().MoveNextAsync(); var ea = parser.EnumerateAsync().GetAsyncEnumerator(); var e = parser.Enumerate().GetEnumerator(); @@ -217,7 +220,7 @@ public async Task Parse_HtmlSpec_Example4(string newline, bool trickle, bool use $"{newline}", trickle); - SseParser parser = CreateParser(stream); + SseParser parser = SseParser.Create(stream); if (useAsync) { Assert.Equal(string.Empty, parser.LastEventId); @@ -273,7 +276,7 @@ public async Task Parse_HtmlSpec_Example4_InheritedIDs(string newline, bool tric $"{newline}", trickle); - SseParser parser = CreateParser(stream); + SseParser parser = SseParser.Create(stream); if (useAsync) { Assert.Equal(string.Empty, parser.LastEventId); @@ -423,7 +426,7 @@ public async Task Retry_SetsReconnectionInterval(string newline, bool trickle, b $"{newline}", trickle); - SseParser parser = CreateParser(stream); + SseParser parser = SseParser.Create(stream); Assert.Equal(Timeout.InfiniteTimeSpan, parser.ReconnectionInterval); if (useAsync) @@ -706,7 +709,7 @@ public async Task Delegate_ThrowsException_Propagates(string newline, bool trick { using Stream stream = GetStream($"data: hello{newline}{newline}data:world{newline}{newline}", trickle); - SseParser parser = CreateParser(stream, (eventType, bytes) => throw new FormatException(Encoding.UTF8.GetString(bytes.ToArray()))); + SseParser parser = SseParser.Create(stream, (eventType, bytes) => throw new FormatException(Encoding.UTF8.GetString(bytes.ToArray()))); FormatException fe; if (useAsync) @@ -730,7 +733,7 @@ public async Task Cancellation_Propagates(bool cancelEnumerator) { using Stream stream = GetStream($"data: hello\n\ndata:world\n\n", trickle: true); - SseParser parser = CreateParser(stream); + SseParser parser = SseParser.Create(stream); var cts = new CancellationTokenSource(); cts.Cancel(); @@ -747,7 +750,7 @@ public void NonGenericEnumerator_ProducesExpectedItems() { using Stream stream = GetStream($"data: hello\n\ndata:world\n\n", trickle: false); - IEnumerable sse = CreateParser(stream).Enumerate(); + IEnumerable sse = SseParser.Create(stream).Enumerate(); IEnumerator e = sse.GetEnumerator(); Assert.True(e.MoveNext()); @@ -814,7 +817,7 @@ public async Task ArrayPoolRental_PerItem(string newline, bool trickle, bool use int count = 0; if (useAsync) { - foreach (var e in CreateParser(stream, itemParser).Enumerate()) + foreach (var e in SseParser.Create(stream, itemParser).Enumerate()) { try { @@ -833,7 +836,7 @@ public async Task ArrayPoolRental_PerItem(string newline, bool trickle, bool use } else { - await foreach (var e in CreateParser(stream, itemParser).EnumerateAsync()) + await foreach (var e in SseParser.Create(stream, itemParser).EnumerateAsync()) { try { @@ -882,7 +885,7 @@ public async Task ArrayPoolRental_Closure(string newline, bool trickle, bool use int count = 0; if (useAsync) { - foreach (var e in CreateParser(stream, itemParser).Enumerate()) + foreach (var e in SseParser.Create(stream, itemParser).Enumerate()) { if ("[DONE]"u8.SequenceEqual(e.Data.Span)) { @@ -893,7 +896,7 @@ public async Task ArrayPoolRental_Closure(string newline, bool trickle, bool use } else { - await foreach (var e in CreateParser(stream, itemParser).EnumerateAsync()) + await foreach (var e in SseParser.Create(stream, itemParser).EnumerateAsync()) { if ("[DONE]"u8.SequenceEqual(e.Data.Span)) { @@ -962,18 +965,18 @@ private static Stream GetStream(byte[] bytes, bool trickle) => private static List> ReadAllEvents(Stream stream) { - return new List>(CreateParser(stream).Enumerate()); + return new List>(SseParser.Create(stream).Enumerate()); } private static List> ReadAllEvents(Stream stream, SseItemParser parser) { - return new List>(CreateParser(stream, parser).Enumerate()); + return new List>(SseParser.Create(stream, parser).Enumerate()); } private static async Task>> ReadAllEventsAsync(Stream stream, SseItemParser parser) { var list = new List>(); - await foreach (SseItem item in CreateParser(stream, parser).EnumerateAsync()) + await foreach (SseItem item in SseParser.Create(stream, parser).EnumerateAsync()) { list.Add(item); } @@ -984,7 +987,7 @@ private static async Task>> ReadAllEventsAsync(Stream stream, private static async Task>> ReadAllEventsAsync(Stream stream) { var list = new List>(); - await foreach (SseItem item in CreateParser(stream).EnumerateAsync()) + await foreach (SseItem item in SseParser.Create(stream).EnumerateAsync()) { list.Add(item); } @@ -992,12 +995,6 @@ private static async Task>> ReadAllEventsAsync(Stream strea return list; } - private static SseParser CreateParser(Stream stream) => - CreateParser(stream, static (_, bytes) => Encoding.UTF8.GetString(bytes.ToArray())); - - private static SseParser CreateParser(Stream stream, SseItemParser itemParser) => - SseParser.Create(stream, new SseParserOptions(itemParser)); - /// Stream where each read reads at most one byte and where every asynchronous operation yields. private sealed class TrickleStream : MemoryStream { From 6674fb137368643fa52224439dd29b738a9f9430 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 14 Aug 2026 22:26:49 +0000 Subject: [PATCH 8/8] Fix SSE MaxBufferSize read enforcement Co-authored-by: mrek-msft <188900745+mrek-msft@users.noreply.github.com> --- .../Net/ServerSentEvents/SseParser_1.cs | 15 +++-- .../tests/SseParserTests.cs | 55 +++++++++++++++++++ 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParser_1.cs b/src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParser_1.cs index a2b11dade6217c..13a5546ace02e8 100644 --- a/src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParser_1.cs +++ b/src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParser_1.cs @@ -511,11 +511,12 @@ private int FillLineBuffer() ShiftOrGrowLineBufferIfNecessary(); int offset = _lineOffset + _lineLength; + int count = GetLineBufferReadCount(_lineBuffer.Length - offset); int bytesRead = _stream.Read( #if NET - _lineBuffer.AsSpan(offset)); + _lineBuffer.AsSpan(offset, count)); #else - _lineBuffer, offset, _lineBuffer.Length - offset); + _lineBuffer, offset, count); #endif if (bytesRead > 0) @@ -541,7 +542,7 @@ private async ValueTask FillLineBufferAsync(CancellationToken cancellationT ShiftOrGrowLineBufferIfNecessary(); int offset = _lineOffset + _lineLength; - int bytesRead = await _stream.ReadAsync(_lineBuffer.AsMemory(offset), cancellationToken).ConfigureAwait(false); + int bytesRead = await _stream.ReadAsync(_lineBuffer.AsMemory(offset, GetLineBufferReadCount(_lineBuffer.Length - offset)), cancellationToken).ConfigureAwait(false); if (bytesRead > 0) { @@ -560,6 +561,12 @@ private async ValueTask FillLineBufferAsync(CancellationToken cancellationT return bytesRead; } + private int GetLineBufferReadCount(int available) + { + int remainingAllowed = _maxBufferSize - _lineLength; + return remainingAllowed < available ? remainingAllowed + 1 : available; + } + /// Gets the UTF8 BOM. private static ReadOnlySpan Utf8Bom => [0xEF, 0xBB, 0xBF]; @@ -584,7 +591,7 @@ private void GrowBuffer([NotNull] ref byte[]? buffer, int minimumLength) } byte[]? toReturn = buffer; - buffer = ArrayPool.Shared.Rent(Math.Max(minimumLength, DefaultArrayPoolRentSize)); + buffer = ArrayPool.Shared.Rent(Math.Max(minimumLength, Math.Min(DefaultArrayPoolRentSize, _maxBufferSize))); if (toReturn is not null) { Array.Copy(toReturn, buffer, toReturn.Length); diff --git a/src/libraries/System.Net.ServerSentEvents/tests/SseParserTests.cs b/src/libraries/System.Net.ServerSentEvents/tests/SseParserTests.cs index d1afd5ee6e4e56..74e48b4d277e8d 100644 --- a/src/libraries/System.Net.ServerSentEvents/tests/SseParserTests.cs +++ b/src/libraries/System.Net.ServerSentEvents/tests/SseParserTests.cs @@ -938,6 +938,61 @@ public async Task Parse_LongLineCap_Throws(string newline, bool useAsync) } } + [Theory] + [InlineData(false, 127)] + [InlineData(true, 127)] + public async Task Parse_MaxBufferSize_AllowsConfiguredLimit(bool useAsync, int maxBufferSize) + { + using Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(new string('a', maxBufferSize))); + var options = new SseParserOptions(static (_, bytes) => Encoding.UTF8.GetString(bytes.ToArray())) + { + MaxBufferSize = maxBufferSize + }; + var parser = SseParser.Create(stream, options); + + if (useAsync) + { + int count = 0; + await foreach (SseItem _ in parser.EnumerateAsync()) + { + count++; + } + + Assert.Equal(0, count); + } + else + { + Assert.Empty(parser.Enumerate()); + } + } + + [Theory] + [InlineData(false, 127)] + [InlineData(true, 127)] + public async Task Parse_MaxBufferSize_ThrowsAfterConfiguredLimit(bool useAsync, int maxBufferSize) + { + using Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(new string('a', maxBufferSize + 1))); + var options = new SseParserOptions(static (_, bytes) => Encoding.UTF8.GetString(bytes.ToArray())) + { + MaxBufferSize = maxBufferSize + }; + var parser = SseParser.Create(stream, options); + + if (useAsync) + { + await Assert.ThrowsAsync(async () => + { + await foreach (SseItem _ in parser.EnumerateAsync()) + { + } + }); + } + else + { + Assert.Throws(() => parser.Enumerate().ToArray()); + } + } + private static void AssertSseItemEqual(SseItem left, SseItem right) { Assert.Equal(left.EventType, right.EventType);