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 01/33] 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 02/33] 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 03/33] 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 04/33] 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 05/33] 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 06/33] 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 07/33] 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 08/33] 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); From 9765a2d4c997b12a8114a9bd06715761dba60595 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 16 Aug 2026 21:33:17 +0000 Subject: [PATCH 09/33] Fix netstandard2.0/.NET Framework compat for SseParserOptions Co-authored-by: mrek-msft <188900745+mrek-msft@users.noreply.github.com> --- .../src/System/Net/ServerSentEvents/SseParser.cs | 4 ++-- .../Net/ServerSentEvents/SseParserOptions.cs | 8 ++++++-- .../src/System/Net/ServerSentEvents/ThrowHelper.cs | 14 ++++++++++++++ 3 files changed, 22 insertions(+), 4 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 c6eee56897ce42..c968b29999e55a 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,8 +42,8 @@ public static SseParser Create(Stream sseStream, SseItemParser itemPars /// or is null. public static SseParser Create(Stream sseStream, SseParserOptions options) { - ArgumentNullException.ThrowIfNull(sseStream); - ArgumentNullException.ThrowIfNull(options); + ThrowHelper.ThrowIfNull(sseStream, nameof(sseStream)); + ThrowHelper.ThrowIfNull(options, nameof(options)); 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 index 2a10c10e1b5653..2708a1560dde27 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,7 +12,7 @@ public sealed class SseParserOptions /// is null. public SseParserOptions(SseItemParser itemParser) { - ArgumentNullException.ThrowIfNull(itemParser); + ThrowHelper.ThrowIfNull(itemParser, nameof(itemParser)); ItemParser = itemParser; } @@ -27,7 +27,11 @@ public int MaxBufferSize get => _maxBufferSize; set { - ArgumentOutOfRangeException.ThrowIfLessThan(value, -1); + if (value < -1) + { + ThrowHelper.ThrowArgumentOutOfRangeException(nameof(value)); + } + _maxBufferSize = value; } } diff --git a/src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/ThrowHelper.cs b/src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/ThrowHelper.cs index cb6a4c58132bc0..a7c73e918baa7e 100644 --- a/src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/ThrowHelper.cs +++ b/src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/ThrowHelper.cs @@ -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); + } } } From 532dba580b9ec88b0872d7504ab7e2607505a932 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 16 Aug 2026 21:53:03 +0000 Subject: [PATCH 10/33] Silently raise MaxBufferSize values below 128 to that minimum Co-authored-by: mrek-msft <188900745+mrek-msft@users.noreply.github.com> --- .../Net/ServerSentEvents/SseParserOptions.cs | 1 + .../Net/ServerSentEvents/SseParser_1.cs | 6 +- .../tests/SseParserTests.cs | 67 +++++++++++++++++-- 3 files changed, 69 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 2708a1560dde27..77c726ff9dcdff 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 @@ -22,6 +22,7 @@ public SseParserOptions(SseItemParser itemParser) /// Gets or sets the maximum buffer size, or -1 to use the default limit. /// The value set is less than -1. + /// Values below an internal minimum are treated as that minimum, as buffers smaller than that don't meaningfully reduce memory usage. public int MaxBufferSize { get => _maxBufferSize; 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 13a5546ace02e8..b61d6dfa38f5b4 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 @@ -44,6 +44,10 @@ public sealed class SseParser /// The maximum amount of data buffered by default. private const int DefaultMaxBufferSize = 1024 * 1024 * 1024; + /// 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. + private const int MinMaxBufferSize = 128; + /// The stream to be parsed. private readonly Stream _stream; /// The parser delegate used to transform bytes into a . @@ -95,7 +99,7 @@ internal SseParser(Stream stream, SseParserOptions options) { _stream = stream; _itemParser = options.ItemParser; - _maxBufferSize = options.MaxBufferSize == -1 ? DefaultMaxBufferSize : options.MaxBufferSize; + _maxBufferSize = options.MaxBufferSize == -1 ? DefaultMaxBufferSize : Math.Max(options.MaxBufferSize, MinMaxBufferSize); } /// Gets an enumerable of the server-sent events from this parser. diff --git a/src/libraries/System.Net.ServerSentEvents/tests/SseParserTests.cs b/src/libraries/System.Net.ServerSentEvents/tests/SseParserTests.cs index 74e48b4d277e8d..af4ebc7beed87d 100644 --- a/src/libraries/System.Net.ServerSentEvents/tests/SseParserTests.cs +++ b/src/libraries/System.Net.ServerSentEvents/tests/SseParserTests.cs @@ -939,8 +939,8 @@ public async Task Parse_LongLineCap_Throws(string newline, bool useAsync) } [Theory] - [InlineData(false, 127)] - [InlineData(true, 127)] + [InlineData(false, 129)] + [InlineData(true, 129)] public async Task Parse_MaxBufferSize_AllowsConfiguredLimit(bool useAsync, int maxBufferSize) { using Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(new string('a', maxBufferSize))); @@ -967,8 +967,8 @@ public async Task Parse_MaxBufferSize_AllowsConfiguredLimit(bool useAsync, int m } [Theory] - [InlineData(false, 127)] - [InlineData(true, 127)] + [InlineData(false, 129)] + [InlineData(true, 129)] public async Task Parse_MaxBufferSize_ThrowsAfterConfiguredLimit(bool useAsync, int maxBufferSize) { using Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(new string('a', maxBufferSize + 1))); @@ -993,6 +993,65 @@ await Assert.ThrowsAsync(async () => } } + [Theory] + [InlineData(false, 0)] + [InlineData(true, 0)] + [InlineData(false, 1)] + [InlineData(true, 1)] + [InlineData(false, 10)] + [InlineData(true, 10)] + public async Task Parse_MaxBufferSize_BelowMinimum_IsRaisedToMinimum(bool useAsync, int maxBufferSize) + { + // The enforced minimum (128) is well above any of the configured values above, so data shorter + // than it should be accepted, while data long enough to exceed it should still throw. + using (Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(new string('a', 100)))) + { + 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()); + } + } + + using (Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(new string('a', 1000)))) + { + 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); From c76a3ad4e3c003b707687207a2ad2d76e8137a46 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 17 Aug 2026 09:38:23 +0000 Subject: [PATCH 11/33] Reuse DefaultArrayPoolRentSize instead of a separate MinMaxBufferSize const Co-authored-by: mrek-msft <188900745+mrek-msft@users.noreply.github.com> --- .../Net/ServerSentEvents/SseParser_1.cs | 12 +++++----- .../tests/SseParserTests.cs | 23 ++++++++++++------- 2 files changed, 21 insertions(+), 14 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 b61d6dfa38f5b4..533d7f3722399e 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 @@ -33,7 +33,11 @@ public sealed class SseParser private readonly long TimeSpan_MaxValueMilliseconds = (long)TimeSpan.MaxValue.TotalMilliseconds; /// The default size of an ArrayPool buffer to rent. - /// Larger size used by default to minimize number of reads. Smaller size used in debug to stress growth/shifting logic. + /// + /// 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. + /// private const int DefaultArrayPoolRentSize = #if DEBUG 16; @@ -44,10 +48,6 @@ public sealed class SseParser /// The maximum amount of data buffered by default. private const int DefaultMaxBufferSize = 1024 * 1024 * 1024; - /// 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. - private const int MinMaxBufferSize = 128; - /// The stream to be parsed. private readonly Stream _stream; /// The parser delegate used to transform bytes into a . @@ -99,7 +99,7 @@ internal SseParser(Stream stream, SseParserOptions options) { _stream = stream; _itemParser = options.ItemParser; - _maxBufferSize = options.MaxBufferSize == -1 ? DefaultMaxBufferSize : Math.Max(options.MaxBufferSize, MinMaxBufferSize); + _maxBufferSize = options.MaxBufferSize == -1 ? DefaultMaxBufferSize : Math.Max(options.MaxBufferSize, DefaultArrayPoolRentSize); } /// Gets an enumerable of the server-sent events from this parser. diff --git a/src/libraries/System.Net.ServerSentEvents/tests/SseParserTests.cs b/src/libraries/System.Net.ServerSentEvents/tests/SseParserTests.cs index af4ebc7beed87d..e4a1feafed1972 100644 --- a/src/libraries/System.Net.ServerSentEvents/tests/SseParserTests.cs +++ b/src/libraries/System.Net.ServerSentEvents/tests/SseParserTests.cs @@ -17,6 +17,13 @@ namespace System.Net.ServerSentEvents.Tests { public partial class SseParserTests { + // Mirrors SseParser.DefaultArrayPoolRentSize, which also serves as the smallest configurable maximum buffer size. +#if DEBUG + private const int MinConfigurableMaxBufferSize = 16; +#else + private const int MinConfigurableMaxBufferSize = 1024; +#endif + [Fact] public void Parse_InvalidArguments_Throws() { @@ -939,8 +946,8 @@ public async Task Parse_LongLineCap_Throws(string newline, bool useAsync) } [Theory] - [InlineData(false, 129)] - [InlineData(true, 129)] + [InlineData(false, MinConfigurableMaxBufferSize + 1)] + [InlineData(true, MinConfigurableMaxBufferSize + 1)] public async Task Parse_MaxBufferSize_AllowsConfiguredLimit(bool useAsync, int maxBufferSize) { using Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(new string('a', maxBufferSize))); @@ -967,8 +974,8 @@ public async Task Parse_MaxBufferSize_AllowsConfiguredLimit(bool useAsync, int m } [Theory] - [InlineData(false, 129)] - [InlineData(true, 129)] + [InlineData(false, MinConfigurableMaxBufferSize + 1)] + [InlineData(true, MinConfigurableMaxBufferSize + 1)] public async Task Parse_MaxBufferSize_ThrowsAfterConfiguredLimit(bool useAsync, int maxBufferSize) { using Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(new string('a', maxBufferSize + 1))); @@ -1002,9 +1009,9 @@ await Assert.ThrowsAsync(async () => [InlineData(true, 10)] public async Task Parse_MaxBufferSize_BelowMinimum_IsRaisedToMinimum(bool useAsync, int maxBufferSize) { - // The enforced minimum (128) is well above any of the configured values above, so data shorter - // than it should be accepted, while data long enough to exceed it should still throw. - using (Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(new string('a', 100)))) + // The enforced minimum (MinConfigurableMaxBufferSize) is well above any of the configured values above, so data + // shorter than it should be accepted, while data long enough to exceed it should still throw. + using (Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(new string('a', MinConfigurableMaxBufferSize / 2)))) { var options = new SseParserOptions(static (_, bytes) => Encoding.UTF8.GetString(bytes.ToArray())) { @@ -1028,7 +1035,7 @@ public async Task Parse_MaxBufferSize_BelowMinimum_IsRaisedToMinimum(bool useAsy } } - using (Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(new string('a', 1000)))) + using (Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(new string('a', MinConfigurableMaxBufferSize * 2)))) { var options = new SseParserOptions(static (_, bytes) => Encoding.UTF8.GetString(bytes.ToArray())) { From f2fa2fdb47058ac4d0b25985dc4368c3050a7688 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 17 Aug 2026 12:35:23 +0000 Subject: [PATCH 12/33] Keep SSE parser initial buffer size Co-authored-by: mrek-msft <188900745+mrek-msft@users.noreply.github.com> --- .../src/System/Net/ServerSentEvents/SseParser_1.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 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 533d7f3722399e..6fb20947d088f1 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 @@ -112,7 +112,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(Math.Min(DefaultArrayPoolRentSize, _maxBufferSize)); + _lineBuffer = ArrayPool.Shared.Rent(DefaultArrayPoolRentSize); try { // Spec: "Event streams in this format must always be encoded as UTF-8". @@ -192,7 +192,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(Math.Min(DefaultArrayPoolRentSize, _maxBufferSize)); + _lineBuffer = ArrayPool.Shared.Rent(DefaultArrayPoolRentSize); try { // Spec: "Event streams in this format must always be encoded as UTF-8". From 2fea097ccb0a823acb429400bad2cd2351b62159 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 17 Aug 2026 13:24:28 +0000 Subject: [PATCH 13/33] Remove SSE buffer rent max cap Co-authored-by: mrek-msft <188900745+mrek-msft@users.noreply.github.com> --- .../src/System/Net/ServerSentEvents/SseParser_1.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 6fb20947d088f1..b555c72e47c4a4 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 @@ -595,7 +595,7 @@ private void GrowBuffer([NotNull] ref byte[]? buffer, int minimumLength) } byte[]? toReturn = buffer; - buffer = ArrayPool.Shared.Rent(Math.Max(minimumLength, Math.Min(DefaultArrayPoolRentSize, _maxBufferSize))); + buffer = ArrayPool.Shared.Rent(Math.Max(minimumLength, DefaultArrayPoolRentSize)); if (toReturn is not null) { Array.Copy(toReturn, buffer, toReturn.Length); From 71d63bd0c13baa3eb8b74be678d3b2d6c38ce847 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 17 Aug 2026 14:40:50 +0000 Subject: [PATCH 14/33] Use full rented capacity for SSE reads Co-authored-by: mrek-msft <188900745+mrek-msft@users.noreply.github.com> --- .../Net/ServerSentEvents/SseParser_1.cs | 27 +++---------------- .../tests/SseParserTests.cs | 2 ++ 2 files changed, 5 insertions(+), 24 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 b555c72e47c4a4..a8a41a2bcb6815 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 @@ -314,11 +314,6 @@ private void ShiftOrGrowLineBufferIfNecessary() } else if (_lineLength == _lineBuffer.Length) { - if (_lineLength >= _maxBufferSize) - { - throw new InvalidDataException(SR.InvalidDataException_SseExceededMaxLength); - } - int newLength; try { @@ -329,11 +324,6 @@ private void ShiftOrGrowLineBufferIfNecessary() newLength = int.MaxValue; } - if (newLength > _maxBufferSize) - { - newLength = _maxBufferSize; - } - GrowBuffer(ref _lineBuffer, newLength); } } @@ -515,7 +505,7 @@ private int FillLineBuffer() ShiftOrGrowLineBufferIfNecessary(); int offset = _lineOffset + _lineLength; - int count = GetLineBufferReadCount(_lineBuffer.Length - offset); + int count = _lineBuffer.Length - offset; int bytesRead = _stream.Read( #if NET _lineBuffer.AsSpan(offset, count)); @@ -546,7 +536,7 @@ private async ValueTask FillLineBufferAsync(CancellationToken cancellationT ShiftOrGrowLineBufferIfNecessary(); int offset = _lineOffset + _lineLength; - int bytesRead = await _stream.ReadAsync(_lineBuffer.AsMemory(offset, GetLineBufferReadCount(_lineBuffer.Length - offset)), cancellationToken).ConfigureAwait(false); + int bytesRead = await _stream.ReadAsync(_lineBuffer.AsMemory(offset), cancellationToken).ConfigureAwait(false); if (bytesRead > 0) { @@ -565,12 +555,6 @@ 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]; @@ -587,13 +571,8 @@ 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) + private static void GrowBuffer([NotNull] ref byte[]? buffer, int minimumLength) { - if (minimumLength > _maxBufferSize) - { - throw new InvalidDataException(SR.InvalidDataException_SseExceededMaxLength); - } - byte[]? toReturn = buffer; buffer = ArrayPool.Shared.Rent(Math.Max(minimumLength, DefaultArrayPoolRentSize)); if (toReturn is not null) diff --git a/src/libraries/System.Net.ServerSentEvents/tests/SseParserTests.cs b/src/libraries/System.Net.ServerSentEvents/tests/SseParserTests.cs index e4a1feafed1972..ee607b4d03a131 100644 --- a/src/libraries/System.Net.ServerSentEvents/tests/SseParserTests.cs +++ b/src/libraries/System.Net.ServerSentEvents/tests/SseParserTests.cs @@ -946,6 +946,8 @@ public async Task Parse_LongLineCap_Throws(string newline, bool useAsync) } [Theory] + [InlineData(false, MinConfigurableMaxBufferSize)] + [InlineData(true, MinConfigurableMaxBufferSize)] [InlineData(false, MinConfigurableMaxBufferSize + 1)] [InlineData(true, MinConfigurableMaxBufferSize + 1)] public async Task Parse_MaxBufferSize_AllowsConfiguredLimit(bool useAsync, int maxBufferSize) From 65cb3d975338ee532c870287e1dc0f01cf5a69d1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 18 Aug 2026 12:05:59 +0000 Subject: [PATCH 15/33] Adjust SSE max-buffer enforcement to grow-time check Co-authored-by: mrek-msft <188900745+mrek-msft@users.noreply.github.com> --- .../Net/ServerSentEvents/SseParser_1.cs | 42 +++++++++++++++---- .../tests/SseParserTests.cs | 7 +++- 2 files changed, 38 insertions(+), 11 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 a8a41a2bcb6815..fab31e7ce2081b 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 @@ -324,7 +324,15 @@ private void ShiftOrGrowLineBufferIfNecessary() newLength = int.MaxValue; } - GrowBuffer(ref _lineBuffer, newLength); + if (newLength > _maxBufferSize) + { + newLength = _maxBufferSize; + } + + if (newLength > _lineBuffer.Length) + { + GrowBuffer(ref _lineBuffer, newLength); + } } } } @@ -506,6 +514,18 @@ private int FillLineBuffer() int offset = _lineOffset + _lineLength; int count = _lineBuffer.Length - offset; + if (count == 0) + { + int probeBytesRead = _stream.Read(new byte[1], 0, 1); + if (probeBytesRead == 0) + { + _eof = true; + return 0; + } + + throw new InvalidDataException(SR.InvalidDataException_SseExceededMaxLength); + } + int bytesRead = _stream.Read( #if NET _lineBuffer.AsSpan(offset, count)); @@ -516,10 +536,6 @@ private int FillLineBuffer() if (bytesRead > 0) { _lineLength += bytesRead; - if (_lineLength > _maxBufferSize) - { - throw new InvalidDataException(SR.InvalidDataException_SseExceededMaxLength); - } } else { @@ -536,15 +552,23 @@ private async ValueTask FillLineBufferAsync(CancellationToken cancellationT ShiftOrGrowLineBufferIfNecessary(); int offset = _lineOffset + _lineLength; + if (offset == _lineBuffer.Length) + { + int probeBytesRead = await _stream.ReadAsync(new byte[1].AsMemory(), cancellationToken).ConfigureAwait(false); + if (probeBytesRead == 0) + { + _eof = true; + return 0; + } + + throw new InvalidDataException(SR.InvalidDataException_SseExceededMaxLength); + } + int bytesRead = await _stream.ReadAsync(_lineBuffer.AsMemory(offset), cancellationToken).ConfigureAwait(false); if (bytesRead > 0) { _lineLength += bytesRead; - if (_lineLength > _maxBufferSize) - { - throw new InvalidDataException(SR.InvalidDataException_SseExceededMaxLength); - } } else { diff --git a/src/libraries/System.Net.ServerSentEvents/tests/SseParserTests.cs b/src/libraries/System.Net.ServerSentEvents/tests/SseParserTests.cs index ee607b4d03a131..e6f642f27c7c89 100644 --- a/src/libraries/System.Net.ServerSentEvents/tests/SseParserTests.cs +++ b/src/libraries/System.Net.ServerSentEvents/tests/SseParserTests.cs @@ -978,9 +978,12 @@ public async Task Parse_MaxBufferSize_AllowsConfiguredLimit(bool useAsync, int m [Theory] [InlineData(false, MinConfigurableMaxBufferSize + 1)] [InlineData(true, MinConfigurableMaxBufferSize + 1)] - public async Task Parse_MaxBufferSize_ThrowsAfterConfiguredLimit(bool useAsync, int maxBufferSize) + public async Task Parse_MaxBufferSize_ThrowsForLongLine(bool useAsync, int maxBufferSize) { - using Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(new string('a', maxBufferSize + 1))); + // ArrayPool can return larger buffers than requested; use a much longer line to ensure we're beyond + // whichever rented bucket size ends up backing the parser. + int lineLength = maxBufferSize + (MinConfigurableMaxBufferSize * 4); + using Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(new string('a', lineLength))); var options = new SseParserOptions(static (_, bytes) => Encoding.UTF8.GetString(bytes.ToArray())) { MaxBufferSize = maxBufferSize From 158a15dfb88c7acf16889e620588b47ed949a33c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=BD=C5=AFrek?= <188900745+mrek-msft@users.noreply.github.com> Date: Thu, 20 Aug 2026 12:56:35 +0200 Subject: [PATCH 16/33] Manual corrections and logic movements. --- .../Net/ServerSentEvents/SseParser_1.cs | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 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 fab31e7ce2081b..be1b999a84d2d3 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 @@ -333,8 +333,15 @@ private void ShiftOrGrowLineBufferIfNecessary() { GrowBuffer(ref _lineBuffer, newLength); } + else + { + throw new InvalidDataException(SR.InvalidDataException_SseExceededMaxLength); + } } } + + // Storage avalaible for at least one byte + Debug.Assert(_lineOffset + _lineLength < _lineBuffer.Length); } /// Processes a complete line from the SSE stream. @@ -516,13 +523,6 @@ private int FillLineBuffer() int count = _lineBuffer.Length - offset; if (count == 0) { - int probeBytesRead = _stream.Read(new byte[1], 0, 1); - if (probeBytesRead == 0) - { - _eof = true; - return 0; - } - throw new InvalidDataException(SR.InvalidDataException_SseExceededMaxLength); } @@ -552,15 +552,9 @@ private async ValueTask FillLineBufferAsync(CancellationToken cancellationT ShiftOrGrowLineBufferIfNecessary(); int offset = _lineOffset + _lineLength; - if (offset == _lineBuffer.Length) + int count = _lineBuffer.Length - offset; + if (count == 0) { - int probeBytesRead = await _stream.ReadAsync(new byte[1].AsMemory(), cancellationToken).ConfigureAwait(false); - if (probeBytesRead == 0) - { - _eof = true; - return 0; - } - throw new InvalidDataException(SR.InvalidDataException_SseExceededMaxLength); } @@ -595,8 +589,13 @@ private void SkipBomIfPresent() } /// Grows the buffer, returning the existing one to the ArrayPool and renting an ArrayPool replacement. - private static void GrowBuffer([NotNull] ref byte[]? buffer, int minimumLength) + private void GrowBuffer([NotNull] ref byte[]? buffer, int minimumLength) { + if (minimumLength > _maxBufferSize) + { + throw new InvalidDataException(SR.InvalidDataException_SseExceededMaxLength); + } + byte[]? toReturn = buffer; buffer = ArrayPool.Shared.Rent(Math.Max(minimumLength, DefaultArrayPoolRentSize)); if (toReturn is not null) From cc3d2b5ebb56869043c98044d69632751546165a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=BD=C5=AFrek?= <188900745+mrek-msft@users.noreply.github.com> Date: Fri, 21 Aug 2026 11:13:28 +0200 Subject: [PATCH 17/33] Improve comments --- .../src/System/Net/ServerSentEvents/SseParserOptions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 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 77c726ff9dcdff..3c34df2ec21372 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 @@ -20,9 +20,9 @@ public SseParserOptions(SseItemParser 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. + /// Gets or sets the maximum buffer size requested from the underlying allocator, or -1 to use the default limit /// The value set is less than -1. - /// Values below an internal minimum are treated as that minimum, as buffers smaller than that don't meaningfully reduce memory usage. + /// Values below an internal minimum are treated as that minimum. If the underlying memory allocator provides more memory than requested, the parser will use all of it. public int MaxBufferSize { get => _maxBufferSize; From c941e35d4c03b1b04abe1af687da099c63888e07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=BD=C5=AFrek?= <188900745+mrek-msft@users.noreply.github.com> Date: Tue, 25 Aug 2026 13:40:30 +0200 Subject: [PATCH 18/33] Resolving PR comments --- .../Net/ServerSentEvents/SseParserOptions.cs | 2 +- .../Net/ServerSentEvents/SseParser_1.cs | 93 +++++++++---------- 2 files changed, 43 insertions(+), 52 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 3c34df2ec21372..c41396ebdc1721 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 @@ -22,7 +22,7 @@ public SseParserOptions(SseItemParser itemParser) /// Gets or sets the maximum buffer size requested from the underlying allocator, or -1 to use the default limit /// The value set is less than -1. - /// Values below an internal minimum are treated as that minimum. If the underlying memory allocator provides more memory than requested, the parser will use all of it. + /// The limit is enforced on a best-effort basis. The permitted memory consumption may be several times higher than the configured limit. public int MaxBufferSize { get => _maxBufferSize; 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 be1b999a84d2d3..0e6bd14ad9856d 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 @@ -314,29 +314,7 @@ private void ShiftOrGrowLineBufferIfNecessary() } else if (_lineLength == _lineBuffer.Length) { - int newLength; - try - { - newLength = checked(_lineBuffer.Length * 2); - } - catch (OverflowException) - { - newLength = int.MaxValue; - } - - if (newLength > _maxBufferSize) - { - newLength = _maxBufferSize; - } - - if (newLength > _lineBuffer.Length) - { - GrowBuffer(ref _lineBuffer, newLength); - } - else - { - throw new InvalidDataException(SR.InvalidDataException_SseExceededMaxLength); - } + GrowBuffer(ref _lineBuffer, () => checked(_lineBuffer.Length + 1)); } } @@ -423,20 +401,7 @@ private bool ProcessLine(out SseItem 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, () => checked(_dataLength + _lineLength + 1)); // Append a newline if there's already content in the buffer. // Then copy the field value to the data buffer @@ -521,11 +486,6 @@ private int FillLineBuffer() int offset = _lineOffset + _lineLength; int count = _lineBuffer.Length - offset; - if (count == 0) - { - throw new InvalidDataException(SR.InvalidDataException_SseExceededMaxLength); - } - int bytesRead = _stream.Read( #if NET _lineBuffer.AsSpan(offset, count)); @@ -552,12 +512,6 @@ private async ValueTask FillLineBufferAsync(CancellationToken cancellationT ShiftOrGrowLineBufferIfNecessary(); int offset = _lineOffset + _lineLength; - int count = _lineBuffer.Length - offset; - if (count == 0) - { - throw new InvalidDataException(SR.InvalidDataException_SseExceededMaxLength); - } - int bytesRead = await _stream.ReadAsync(_lineBuffer.AsMemory(offset), cancellationToken).ConfigureAwait(false); if (bytesRead > 0) @@ -589,15 +543,52 @@ 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) + /// The buffer to extend on input; the newly extended buffer on output. + /// A lambda that computes the required buffer length using checked operations. + private void GrowBuffer([NotNull] ref byte[]? buffer, Func checkedRequiredLength) { - if (minimumLength > _maxBufferSize) + int currentSize = buffer?.Length ?? 0; + int prefferedGrowth; + try + { + prefferedGrowth = checked(currentSize * 2); + } + catch (OverflowException) + { + prefferedGrowth = int.MaxValue; + } + if (prefferedGrowth < DefaultArrayPoolRentSize) + { + prefferedGrowth = DefaultArrayPoolRentSize; + } + if (prefferedGrowth > _maxBufferSize) + { + prefferedGrowth = _maxBufferSize; + } + + int requestedGrowth; + try + { + requestedGrowth = checkedRequiredLength(); + } + catch (OverflowException) + { + throw new InvalidDataException(SR.InvalidDataException_SseExceededMaxLength); + } + if (requestedGrowth > _maxBufferSize) { throw new InvalidDataException(SR.InvalidDataException_SseExceededMaxLength); } + int actualGrowth = Math.Max(prefferedGrowth, requestedGrowth); + + if (buffer is not null && currentSize > actualGrowth) + { + return; + } + byte[]? toReturn = buffer; - buffer = ArrayPool.Shared.Rent(Math.Max(minimumLength, DefaultArrayPoolRentSize)); + buffer = ArrayPool.Shared.Rent(actualGrowth); if (toReturn is not null) { Array.Copy(toReturn, buffer, toReturn.Length); From 9655642de3eed263bed2db0faa61d5b408a77547 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=BD=C5=AFrek?= <188900745+mrek-msft@users.noreply.github.com> Date: Tue, 25 Aug 2026 13:49:32 +0200 Subject: [PATCH 19/33] Remove duplicate test --- .../tests/SseParserTests.cs | 30 ------------------- 1 file changed, 30 deletions(-) diff --git a/src/libraries/System.Net.ServerSentEvents/tests/SseParserTests.cs b/src/libraries/System.Net.ServerSentEvents/tests/SseParserTests.cs index e6f642f27c7c89..0fca96bf8312b9 100644 --- a/src/libraries/System.Net.ServerSentEvents/tests/SseParserTests.cs +++ b/src/libraries/System.Net.ServerSentEvents/tests/SseParserTests.cs @@ -975,36 +975,6 @@ public async Task Parse_MaxBufferSize_AllowsConfiguredLimit(bool useAsync, int m } } - [Theory] - [InlineData(false, MinConfigurableMaxBufferSize + 1)] - [InlineData(true, MinConfigurableMaxBufferSize + 1)] - public async Task Parse_MaxBufferSize_ThrowsForLongLine(bool useAsync, int maxBufferSize) - { - // ArrayPool can return larger buffers than requested; use a much longer line to ensure we're beyond - // whichever rented bucket size ends up backing the parser. - int lineLength = maxBufferSize + (MinConfigurableMaxBufferSize * 4); - using Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(new string('a', lineLength))); - 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()); - } - } - [Theory] [InlineData(false, 0)] [InlineData(true, 0)] From 9866b9a4a70fce5701ec3257dcade05cfa3a86ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=BD=C5=AFrek?= <188900745+mrek-msft@users.noreply.github.com> Date: Tue, 25 Aug 2026 16:43:40 +0200 Subject: [PATCH 20/33] SImplify code, fix too late check for skipping if large enough --- .../Net/ServerSentEvents/SseParserOptions.cs | 2 +- .../Net/ServerSentEvents/SseParser_1.cs | 27 ++++++++----------- 2 files changed, 12 insertions(+), 17 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 c41396ebdc1721..2fd2756728fa72 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 @@ -20,7 +20,7 @@ public SseParserOptions(SseItemParser 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 requested from the underlying allocator, or -1 to use the default limit + /// Gets or sets the maximum buffer size requested from the underlying allocator, or -1 to use the default limit. /// The value set is less than -1. /// The limit is enforced on a best-effort basis. The permitted memory consumption may be several times higher than the configured limit. public int MaxBufferSize 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 0e6bd14ad9856d..994a44b0b2cb30 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 @@ -548,23 +548,18 @@ private void SkipBomIfPresent() private void GrowBuffer([NotNull] ref byte[]? buffer, Func checkedRequiredLength) { int currentSize = buffer?.Length ?? 0; - int prefferedGrowth; + + int preferredGrowth; try { - prefferedGrowth = checked(currentSize * 2); + preferredGrowth = checked(currentSize * 2); } catch (OverflowException) { - prefferedGrowth = int.MaxValue; - } - if (prefferedGrowth < DefaultArrayPoolRentSize) - { - prefferedGrowth = DefaultArrayPoolRentSize; - } - if (prefferedGrowth > _maxBufferSize) - { - prefferedGrowth = _maxBufferSize; + preferredGrowth = int.MaxValue; } + preferredGrowth = Math.Min(preferredGrowth, _maxBufferSize); + preferredGrowth = Math.Max(preferredGrowth, DefaultArrayPoolRentSize); int requestedGrowth; try @@ -575,17 +570,17 @@ private void GrowBuffer([NotNull] ref byte[]? buffer, Func checkedRequiredL { throw new InvalidDataException(SR.InvalidDataException_SseExceededMaxLength); } + if (buffer is not null && currentSize > requestedGrowth) + { + return; + } if (requestedGrowth > _maxBufferSize) { throw new InvalidDataException(SR.InvalidDataException_SseExceededMaxLength); } - int actualGrowth = Math.Max(prefferedGrowth, requestedGrowth); + int actualGrowth = Math.Max(preferredGrowth, requestedGrowth); - if (buffer is not null && currentSize > actualGrowth) - { - return; - } byte[]? toReturn = buffer; buffer = ArrayPool.Shared.Rent(actualGrowth); From f33b397a2e8582c9e95660778839bfba4006534b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=BD=C5=AFrek?= <188900745+mrek-msft@users.noreply.github.com> Date: Tue, 25 Aug 2026 17:08:37 +0200 Subject: [PATCH 21/33] Code cleanup --- .../src/System/Net/ServerSentEvents/SseParser_1.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 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 994a44b0b2cb30..3dd4092246812b 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 @@ -485,12 +485,11 @@ private int FillLineBuffer() ShiftOrGrowLineBufferIfNecessary(); int offset = _lineOffset + _lineLength; - int count = _lineBuffer.Length - offset; int bytesRead = _stream.Read( #if NET - _lineBuffer.AsSpan(offset, count)); + _lineBuffer.AsSpan(offset)); #else - _lineBuffer, offset, count); + _lineBuffer, offset, _lineBuffer.Length - offset); #endif if (bytesRead > 0) From 7d836a93ec0d5335f59629c1df6e08c9c3e87eeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=BD=C5=AFrek?= <188900745+mrek-msft@users.noreply.github.com> Date: Tue, 25 Aug 2026 17:29:57 +0200 Subject: [PATCH 22/33] Simplify code, work in uint to prevent explicit overflow checks, add Array.MaxLength check. --- .../Net/ServerSentEvents/SseParserOptions.cs | 14 +++++- .../Net/ServerSentEvents/SseParser_1.cs | 43 ++++++------------- 2 files changed, 27 insertions(+), 30 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 2fd2756728fa72..b873b52bbf4a99 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 @@ -21,7 +21,7 @@ public SseParserOptions(SseItemParser itemParser) public SseItemParser ItemParser { get; } /// Gets or sets the maximum buffer size requested from the underlying allocator, or -1 to use the default limit. - /// The value set is less than -1. + /// The value set is less than -1 or greater then Array.MaxLength. /// The limit is enforced on a best-effort basis. The permitted memory consumption may be several times higher than the configured limit. public int MaxBufferSize { @@ -33,6 +33,18 @@ public int MaxBufferSize ThrowHelper.ThrowArgumentOutOfRangeException(nameof(value)); } +#if NET + if (value > Array.MaxLength) + { + ThrowHelper.ThrowArgumentOutOfRangeException(nameof(value)); + } +#else + if (value > 0x7FFFFFC7) + { + ThrowHelper.ThrowArgumentOutOfRangeException(nameof(value)); + } +#endif + _maxBufferSize = value; } } 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 3dd4092246812b..ebc254df009cb4 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 @@ -314,7 +314,7 @@ private void ShiftOrGrowLineBufferIfNecessary() } else if (_lineLength == _lineBuffer.Length) { - GrowBuffer(ref _lineBuffer, () => checked(_lineBuffer.Length + 1)); + GrowBuffer(ref _lineBuffer, (uint)_lineBuffer.Length + 1); } } @@ -401,7 +401,7 @@ private bool ProcessLine(out SseItem sseItem, out int advance) } // We need to copy the data from the line buffer to the data buffer. Make sure there's enough room. - GrowBuffer(ref _dataBuffer, () => checked(_dataLength + _lineLength + 1)); + GrowBuffer(ref _dataBuffer, (uint)_dataLength + (uint)_lineLength + 1); // Append a newline if there's already content in the buffer. // Then copy the field value to the data buffer @@ -543,46 +543,31 @@ private void SkipBomIfPresent() /// Grows the buffer, returning the existing one to the ArrayPool and renting an ArrayPool replacement. /// The buffer to extend on input; the newly extended buffer on output. - /// A lambda that computes the required buffer length using checked operations. - private void GrowBuffer([NotNull] ref byte[]? buffer, Func checkedRequiredLength) + /// A lambda that computes the required buffer length using checked operations. + private void GrowBuffer([NotNull] ref byte[]? buffer, uint minimumSize) { int currentSize = buffer?.Length ?? 0; - int preferredGrowth; - try - { - preferredGrowth = checked(currentSize * 2); - } - catch (OverflowException) - { - preferredGrowth = int.MaxValue; - } - preferredGrowth = Math.Min(preferredGrowth, _maxBufferSize); - preferredGrowth = Math.Max(preferredGrowth, DefaultArrayPoolRentSize); + uint prefferedSize = (uint)currentSize * 2; + prefferedSize = Math.Min(prefferedSize, (uint)_maxBufferSize); + prefferedSize = Math.Max(prefferedSize, DefaultArrayPoolRentSize); + Debug.Assert(prefferedSize < int.MaxValue); - int requestedGrowth; - try - { - requestedGrowth = checkedRequiredLength(); - } - catch (OverflowException) + if (minimumSize > _maxBufferSize) { throw new InvalidDataException(SR.InvalidDataException_SseExceededMaxLength); } - if (buffer is not null && currentSize > requestedGrowth) + Debug.Assert(minimumSize < int.MaxValue); + + if (buffer is not null && currentSize > minimumSize) { return; } - if (requestedGrowth > _maxBufferSize) - { - throw new InvalidDataException(SR.InvalidDataException_SseExceededMaxLength); - } - - int actualGrowth = Math.Max(preferredGrowth, requestedGrowth); + int rentedSize = Math.Max((int)prefferedSize, (int)minimumSize); byte[]? toReturn = buffer; - buffer = ArrayPool.Shared.Rent(actualGrowth); + buffer = ArrayPool.Shared.Rent(rentedSize); if (toReturn is not null) { Array.Copy(toReturn, buffer, toReturn.Length); From 4ad284e96b1a08125f5bf331c96405772af82a49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=BD=C5=AFrek?= <188900745+mrek-msft@users.noreply.github.com> Date: Tue, 25 Aug 2026 17:35:14 +0200 Subject: [PATCH 23/33] Do not "grow" if current sioze equals requested. --- .../src/System/Net/ServerSentEvents/SseParser_1.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ebc254df009cb4..4b14cd8aed274e 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 GrowBuffer([NotNull] ref byte[]? buffer, uint minimumSize) } Debug.Assert(minimumSize < int.MaxValue); - if (buffer is not null && currentSize > minimumSize) + if (buffer is not null && currentSize >= minimumSize) { return; } From 3cc0fa5029d4e621219609129a7b3f0e8b94220d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=BD=C5=AFrek?= <188900745+mrek-msft@users.noreply.github.com> Date: Tue, 25 Aug 2026 17:45:59 +0200 Subject: [PATCH 24/33] Typos in comments --- .../src/System/Net/ServerSentEvents/SseParserOptions.cs | 2 +- .../src/System/Net/ServerSentEvents/SseParser_1.cs | 2 +- 2 files changed, 2 insertions(+), 2 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 b873b52bbf4a99..a956347c0bf317 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 @@ -21,7 +21,7 @@ public SseParserOptions(SseItemParser itemParser) public SseItemParser ItemParser { get; } /// Gets or sets the maximum buffer size requested from the underlying allocator, or -1 to use the default limit. - /// The value set is less than -1 or greater then Array.MaxLength. + /// The value set is less than -1 or greater than Array.MaxLength. /// The limit is enforced on a best-effort basis. The permitted memory consumption may be several times higher than the configured limit. public int MaxBufferSize { 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 4b14cd8aed274e..0bf027f5de3575 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 @@ -318,7 +318,7 @@ private void ShiftOrGrowLineBufferIfNecessary() } } - // Storage avalaible for at least one byte + // Storage available for at least one byte Debug.Assert(_lineOffset + _lineLength < _lineBuffer.Length); } From d1962d40b7ec1839b668fb605e5d411e1c7f063e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=BD=C5=AFrek?= <188900745+mrek-msft@users.noreply.github.com> Date: Tue, 25 Aug 2026 17:46:40 +0200 Subject: [PATCH 25/33] Removing confusing comments --- .../src/System/Net/ServerSentEvents/SseParser_1.cs | 2 -- 1 file changed, 2 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 0bf027f5de3575..d80c72e6b0a9f0 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 @@ -542,8 +542,6 @@ private void SkipBomIfPresent() } /// Grows the buffer, returning the existing one to the ArrayPool and renting an ArrayPool replacement. - /// The buffer to extend on input; the newly extended buffer on output. - /// A lambda that computes the required buffer length using checked operations. private void GrowBuffer([NotNull] ref byte[]? buffer, uint minimumSize) { int currentSize = buffer?.Length ?? 0; From 975536b57a2de0a37f8f8df8e9ddc75e89ff6886 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=BD=C5=AFrek?= <188900745+mrek-msft@users.noreply.github.com> Date: Tue, 25 Aug 2026 17:47:48 +0200 Subject: [PATCH 26/33] Refactor typos in code. --- .../src/System/Net/ServerSentEvents/SseParser_1.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 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 d80c72e6b0a9f0..dffd13e242e7a4 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 @@ -546,10 +546,10 @@ private void GrowBuffer([NotNull] ref byte[]? buffer, uint minimumSize) { int currentSize = buffer?.Length ?? 0; - uint prefferedSize = (uint)currentSize * 2; - prefferedSize = Math.Min(prefferedSize, (uint)_maxBufferSize); - prefferedSize = Math.Max(prefferedSize, DefaultArrayPoolRentSize); - Debug.Assert(prefferedSize < int.MaxValue); + uint preferedSize = (uint)currentSize * 2; + preferedSize = Math.Min(preferedSize, (uint)_maxBufferSize); + preferedSize = Math.Max(preferedSize, DefaultArrayPoolRentSize); + Debug.Assert(preferedSize < int.MaxValue); if (minimumSize > _maxBufferSize) { @@ -562,7 +562,7 @@ private void GrowBuffer([NotNull] ref byte[]? buffer, uint minimumSize) return; } - int rentedSize = Math.Max((int)prefferedSize, (int)minimumSize); + int rentedSize = Math.Max((int)preferedSize, (int)minimumSize); byte[]? toReturn = buffer; buffer = ArrayPool.Shared.Rent(rentedSize); From ff27e4803fba9c98229e572169c4e04af30773d4 Mon Sep 17 00:00:00 2001 From: MichalZ <188900745+mrek-msft@users.noreply.github.com> Date: Wed, 26 Aug 2026 11:18:44 +0200 Subject: [PATCH 27/33] Update src/libraries/System.Net.ServerSentEvents/src/System/Net/ServerSentEvents/SseParser_1.cs Improve assert in int.MaxValue edge case Co-authored-by: Miha Zupan --- .../src/System/Net/ServerSentEvents/SseParser_1.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 dffd13e242e7a4..50c35c9af926c9 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 @@ -555,7 +555,7 @@ private void GrowBuffer([NotNull] ref byte[]? buffer, uint minimumSize) { throw new InvalidDataException(SR.InvalidDataException_SseExceededMaxLength); } - Debug.Assert(minimumSize < int.MaxValue); + Debug.Assert(minimumSize <= int.MaxValue); if (buffer is not null && currentSize >= minimumSize) { From afa6232d3c076a8bcdbe32196f398ba0f439cc38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=BD=C5=AFrek?= <188900745+mrek-msft@users.noreply.github.com> Date: Wed, 26 Aug 2026 11:31:09 +0200 Subject: [PATCH 28/33] remove Array.MaxLength check --- .../Net/ServerSentEvents/SseParserOptions.cs | 14 +------------- 1 file changed, 1 insertion(+), 13 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 a956347c0bf317..2fd2756728fa72 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 @@ -21,7 +21,7 @@ public SseParserOptions(SseItemParser itemParser) public SseItemParser ItemParser { get; } /// Gets or sets the maximum buffer size requested from the underlying allocator, or -1 to use the default limit. - /// The value set is less than -1 or greater than Array.MaxLength. + /// The value set is less than -1. /// The limit is enforced on a best-effort basis. The permitted memory consumption may be several times higher than the configured limit. public int MaxBufferSize { @@ -33,18 +33,6 @@ public int MaxBufferSize ThrowHelper.ThrowArgumentOutOfRangeException(nameof(value)); } -#if NET - if (value > Array.MaxLength) - { - ThrowHelper.ThrowArgumentOutOfRangeException(nameof(value)); - } -#else - if (value > 0x7FFFFFC7) - { - ThrowHelper.ThrowArgumentOutOfRangeException(nameof(value)); - } -#endif - _maxBufferSize = value; } } From 0ed6ad890d9479b050c14b24e2ee56e0814c20f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=BD=C5=AFrek?= <188900745+mrek-msft@users.noreply.github.com> Date: Wed, 26 Aug 2026 12:48:34 +0200 Subject: [PATCH 29/33] Handle above Array.MaxLength edge case --- .../src/System/Net/ServerSentEvents/SseParser_1.cs | 6 ++++++ 1 file changed, 6 insertions(+) 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 50c35c9af926c9..cd3dc02f78e360 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 @@ -100,6 +100,12 @@ internal SseParser(Stream stream, SseParserOptions options) _stream = stream; _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 } /// Gets an enumerable of the server-sent events from this parser. From 27de8f51483faeca769adcc03fa4617644c7ec1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=BD=C5=AFrek?= <188900745+mrek-msft@users.noreply.github.com> Date: Wed, 26 Aug 2026 12:51:44 +0200 Subject: [PATCH 30/33] Fix typo --- .../src/System/Net/ServerSentEvents/SseParser_1.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 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 cd3dc02f78e360..47e43c1c4dc915 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 @@ -552,10 +552,10 @@ private void GrowBuffer([NotNull] ref byte[]? buffer, uint minimumSize) { int currentSize = buffer?.Length ?? 0; - uint preferedSize = (uint)currentSize * 2; - preferedSize = Math.Min(preferedSize, (uint)_maxBufferSize); - preferedSize = Math.Max(preferedSize, DefaultArrayPoolRentSize); - Debug.Assert(preferedSize < int.MaxValue); + uint preferredSize = (uint)currentSize * 2; + preferredSize = Math.Min(preferredSize, (uint)_maxBufferSize); + preferredSize = Math.Max(preferredSize, DefaultArrayPoolRentSize); + Debug.Assert(preferredSize < int.MaxValue); if (minimumSize > _maxBufferSize) { @@ -568,7 +568,7 @@ private void GrowBuffer([NotNull] ref byte[]? buffer, uint minimumSize) return; } - int rentedSize = Math.Max((int)preferedSize, (int)minimumSize); + int rentedSize = Math.Max((int)preferredSize, (int)minimumSize); byte[]? toReturn = buffer; buffer = ArrayPool.Shared.Rent(rentedSize); From 4a10b2a024647eb6c1b38a37b20126357c4a2dae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=BD=C5=AFrek?= <188900745+mrek-msft@users.noreply.github.com> Date: Wed, 26 Aug 2026 13:23:57 +0200 Subject: [PATCH 31/33] Fix bad comparison operator --- .../src/System/Net/ServerSentEvents/SseParser_1.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 47e43c1c4dc915..9a74b758dc15d3 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 @@ -555,7 +555,7 @@ private void GrowBuffer([NotNull] ref byte[]? buffer, uint minimumSize) uint preferredSize = (uint)currentSize * 2; preferredSize = Math.Min(preferredSize, (uint)_maxBufferSize); preferredSize = Math.Max(preferredSize, DefaultArrayPoolRentSize); - Debug.Assert(preferredSize < int.MaxValue); + Debug.Assert(preferredSize <= int.MaxValue); if (minimumSize > _maxBufferSize) { From 93c49c269185929f1917b90dd0dbd23010e54218 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=BD=C5=AFrek?= <188900745+mrek-msft@users.noreply.github.com> Date: Wed, 26 Aug 2026 13:29:18 +0200 Subject: [PATCH 32/33] Improve comment --- .../src/System/Net/ServerSentEvents/SseParserOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 2fd2756728fa72..0118cf42259afa 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 @@ -22,7 +22,7 @@ public SseParserOptions(SseItemParser itemParser) /// Gets or sets the maximum buffer size requested from the underlying allocator, or -1 to use the default limit. /// The value set is less than -1. - /// The limit is enforced on a best-effort basis. The permitted memory consumption may be several times higher than the configured limit. + /// 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. public int MaxBufferSize { get => _maxBufferSize; From 16ebdf16a281eccced0e2cd6a659f09864beaba2 Mon Sep 17 00:00:00 2001 From: MihaZupan Date: Thu, 27 Aug 2026 20:14:28 +0200 Subject: [PATCH 33/33] Fix 'allows' test --- .../System.Net.ServerSentEvents/tests/SseParserTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Net.ServerSentEvents/tests/SseParserTests.cs b/src/libraries/System.Net.ServerSentEvents/tests/SseParserTests.cs index 0fca96bf8312b9..842de991903fa8 100644 --- a/src/libraries/System.Net.ServerSentEvents/tests/SseParserTests.cs +++ b/src/libraries/System.Net.ServerSentEvents/tests/SseParserTests.cs @@ -950,9 +950,9 @@ public async Task Parse_LongLineCap_Throws(string newline, bool useAsync) [InlineData(true, MinConfigurableMaxBufferSize)] [InlineData(false, MinConfigurableMaxBufferSize + 1)] [InlineData(true, MinConfigurableMaxBufferSize + 1)] - public async Task Parse_MaxBufferSize_AllowsConfiguredLimit(bool useAsync, int maxBufferSize) + public async Task Parse_MaxBufferSize_AllowsDataBelowConfiguredLimit(bool useAsync, int maxBufferSize) { - using Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(new string('a', 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