Skip to content

Refactor HTTP/3 server stream handling to consolidate exception handling for stream resets#122561

Merged
MihaZupan merged 7 commits intomainfrom
copilot/fix-http3-unidirectional-streams
Dec 18, 2025
Merged

Refactor HTTP/3 server stream handling to consolidate exception handling for stream resets#122561
MihaZupan merged 7 commits intomainfrom
copilot/fix-http3-unidirectional-streams

Conversation

Copy link
Contributor

Copilot AI commented Dec 15, 2025

Description

When NetEventSource tracing is enabled, the HTTP/3 client throws an unhandled QuicException if the server resets a unidirectional stream while the client reads a multi-byte stream type header. This violates RFC 9114, which requires tolerating stream resets before receiving the complete stream header.

Refactored the stream type reading logic to consolidate exception handling in a single location before the switch statement:

  • Moved stream type reading before switch: All stream type reading (variable-length integer) now happens in one loop before processing, eliminating duplicate exception handling code
  • Single exception handler: Added try-catch for QuicException with QuicError.StreamAborted that covers all reads needed to parse the stream type, not just the diagnostic logging path
  • Enhanced logging: Now logs all server-initiated unidirectional stream types when tracing is enabled, not just unknown types
  • Preserved validation order: Bidirectional stream check remains at original position (before any I/O) for efficiency
  • Simplified switch statement: Uses switch ((Http3StreamType)streamType) with clean enum case labels instead of casting each case
  • Improved variable scoping: Moved bytesRead variable into loop scope for better code quality

The refactoring consolidates the exception handling pattern that existed at line 598 to cover all stream type reads, removing 32 lines of duplicate code while ensuring RFC 9114 compliance.

Customer Impact

With tracing enabled, server-side stream resets (legitimate per RFC 9114) cause the entire HTTP/3 connection to abort with HttpProtocolException, breaking all in-flight requests. Without this fix, enabling diagnostics paradoxically makes the client less robust.

Regression

No. This is a pre-existing bug exposed only when NetEventSource tracing is enabled and the server sends a stream type ≥ 0x40 (requiring 2+ bytes) then resets before sending all bytes.

Testing

  • Build succeeded
  • Code review: addressed all feedback
  • CodeQL scan: clean
  • Refactoring consolidates existing exception handling pattern to single location
  • Net reduction of 32 lines of duplicate code
  • Bidirectional stream check correctly positioned before any I/O operations
  • Variable scoping improvements for code quality

Risk

Low. Refactoring moves existing exception handling logic to cover all stream type reads instead of only the first byte. Bidirectional stream check remains at original efficient position. Variable scoping changes are purely code quality improvements with no functional impact. No behavior change when streams complete normally. Actually reduces risk by eliminating code duplication and making the exception handling more comprehensive.

Original prompt

This section details on the original issue you should resolve

<issue_title>Incorrect handling of unidirectional server-initiated streams in Http3Connection</issue_title>
<issue_description>### Description

The HTTP/3 RFC specifies the following for unidirectional streams:

A sender can close or reset a unidirectional stream unless otherwise specified. A receiver MUST tolerate unidirectional streams being closed or reset prior to the reception of the unidirectional stream header.

This behavior is violated when receiving a unidirectional server-initated stream here. The code is missing a try-catch block, because the QuicStream.ReadAsync may throw, which leads to an abort of the whole H/3 connection.

Note that the behavior is violated only when tracing is enabled.

Reproduction Steps

The problem depends on timing, because the first read from the received stream handles an exception properly (see here). Nonetheless, I will describe the inputs and the timings that lead to the incorrect behavior.

  1. The connection is established.
  2. The server opens a unidirectional stream with stream type that is encoded into 2 bytes or more.
  3. The server sends the first byte of the stream type.
  4. The client receives the unidirectional stream.
  5. The client reads the first byte successfully and reaches L674.
  6. The server resets the stream.
  7. The client throws on line L674, which causes an abort - a connection level error.

Expected behavior

The stream reset by the server should not cause a connection level error.

Actual behavior

The stream reset by the server causes the client to close the connection, because it considers it a connection level error.

Regression?

No response

Known Workarounds

Disable tracing.

Configuration

No response

Other information

No response</issue_description>

Comments on the Issue (you are @copilot in this section)


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Add try-catch block around ReadAsync in ProcessServerStreamAsync to handle
QuicException with StreamAborted error when reading multi-byte stream type
headers with tracing enabled. This ensures compliance with RFC 9114 which
requires tolerating stream resets prior to receiving the complete unidirectional
stream header.

Co-authored-by: MihaZupan <[email protected]>
Copilot AI changed the title [WIP] Fix handling of unidirectional server-initiated streams in Http3Connection Fix unhandled QuicException when server resets unidirectional stream during tracing Dec 15, 2025
Copilot AI requested a review from MihaZupan December 15, 2025 15:54
Copy link
Member

@MihaZupan MihaZupan left a comment

Choose a reason for hiding this comment

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

I'd also be fine with deleting this whole if (NetEventSource.Log.IsEnabled()) block if we don't see the value in this particular message.

@MihaZupan MihaZupan added this to the 11.0.0 milestone Dec 15, 2025
@MihaZupan MihaZupan requested a review from a team December 15, 2025 16:38
@MihaZupan MihaZupan marked this pull request as ready for review December 15, 2025 16:38
Copilot AI review requested due to automatic review settings December 15, 2025 16:38
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copy link
Contributor Author

Copilot AI commented Dec 17, 2025

Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • badhost
    • Triggering command: /home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-linux-Debug-x64/dotnet /home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-linux-Debug-x64/dotnet exec --runtimeconfig System.Net.Http.Functional.Tests.runtimeconfig.json --depsfile System.Net.Http.Functional.Tests.deps.json /home/REDACTED/.nuget/packages/microsoft.dotnet.xunitconsoleREDACTED/2.9.3-beta.25609.108/build/../tools/net/xunit.console.dll System.Net.Http.Functional.Tests.dll -xml testResults.xml -nologo -notrait category=OuterLoop -notrait category=failing /usr/bin/grep Http3StreamType ts/DataTests.cs cal/bin/grep grep (dns block)
  • does.not.exist.sorry
    • Triggering command: /home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-linux-Debug-x64/dotnet /home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-linux-Debug-x64/dotnet exec --runtimeconfig /home/REDACTED/work/runtime/runtime/artifacts/bin/System.Net.Http.Functional.Tests/Debug/net11.0-linux/System.Net.Http.Functional.Tests.runtimeconfig.json --depsfile /home/REDACTED/work/runtime/runtime/artifacts/bin/System.Net.Http.Functional.Tests/Debug/net11.0-linux/System.Net.Http.Functional.Tests.deps.json /home/REDACTED/work/runtime/runtime/artifacts/bin/System.Net.Http.Functional.Tests/Debug/net11.0-linux/Microsoft.DotNet.RemoteExecutor.dll System.Net.Http.Functional.Tests, Version=11.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51 System.Net.Http.Functional.Tests.DiagnosticsTest &lt;SendAsync_ConnectionFailure_RecordsActivitiesWithCorrectErrorInfo&gt;g__RunTest|18_0 /tmp/dfxttxzd.jkr 1.1 False dns System.Private.Xml/src/System/Xml/Dom/XPathNodeList.cs ndor/bin/grep Http3StreamType /InsertAfterTest/usr/share/dotnet/sdk/10.0.100/MSBuild.dll /usr/bin/grep grep (dns block)
    • Triggering command: /home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-linux-Debug-x64/dotnet /home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-linux-Debug-x64/dotnet exec --runtimeconfig /home/REDACTED/work/runtime/runtime/artifacts/bin/System.Net.Http.Functional.Tests/Debug/net11.0-linux/System.Net.Http.Functional.Tests.runtimeconfig.json --depsfile /home/REDACTED/work/runtime/runtime/artifacts/bin/System.Net.Http.Functional.Tests/Debug/net11.0-linux/System.Net.Http.Functional.Tests.deps.json /home/REDACTED/work/runtime/runtime/artifacts/bin/System.Net.Http.Functional.Tests/Debug/net11.0-linux/Microsoft.DotNet.RemoteExecutor.dll System.Net.Http.Functional.Tests, Version=11.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51 System.Net.Http.Functional.Tests.DiagnosticsTest &lt;SendAsync_ConnectionFailure_RecordsActivitiesWithCorrectErrorInfo&gt;g__RunTest|18_0 /tmp/dlicyomz.k3m 1.1 True dns System.Private.Xml/src/System/Xml/Dom/XPathNodeList.cs nfig/composer/vendor/bin/grep Http3StreamType /InsertAfterTest/usr/share/dotnet/sdk/10.0.100/MSBuild.dll rep grep (dns block)
    • Triggering command: /home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-linux-Debug-x64/dotnet /home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-linux-Debug-x64/dotnet exec --runtimeconfig /home/REDACTED/work/runtime/runtime/artifacts/bin/System.Net.Http.Functional.Tests/Debug/net11.0-linux/System.Net.Http.Functional.Tests.runtimeconfig.json --depsfile /home/REDACTED/work/runtime/runtime/artifacts/bin/System.Net.Http.Functional.Tests/Debug/net11.0-linux/System.Net.Http.Functional.Tests.deps.json /home/REDACTED/work/runtime/runtime/artifacts/bin/System.Net.Http.Functional.Tests/Debug/net11.0-linux/Microsoft.DotNet.RemoteExecutor.dll System.Net.Http.Functional.Tests, Version=11.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51 System.Net.Http.Functional.Tests.DiagnosticsTest &lt;SendAsync_ConnectionFailure_RecordsActivitiesWithCorrectErrorInfo&gt;g__RunTest|18_0 /tmp/rbyllbfr.lzf 2.0 True dns System.Private.Xml/src/System/Xml/Dom/XmlDocument.cs k/runtime/runtime/.dotnet/grep Http3StreamType s/XmlPreloadedReexec rgo/bin/grep grep (dns block)
  • nosuchhost.invalid
    • Triggering command: /home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-linux-Debug-x64/dotnet /home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-linux-Debug-x64/dotnet exec --runtimeconfig /home/REDACTED/work/runtime/runtime/artifacts/bin/System.Net.Http.Functional.Tests/Debug/net11.0-linux/System.Net.Http.Functional.Tests.runtimeconfig.json --depsfile /home/REDACTED/work/runtime/runtime/artifacts/bin/System.Net.Http.Functional.Tests/Debug/net11.0-linux/System.Net.Http.Functional.Tests.deps.json /home/REDACTED/work/runtime/runtime/artifacts/bin/System.Net.Http.Functional.Tests/Debug/net11.0-linux/Microsoft.DotNet.RemoteExecutor.dll System.Net.Http.Functional.Tests, Version=11.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51 System.Net.Http.Functional.Tests.DiagnosticsTest&#43;&lt;&gt;c &lt;SendAsync_ExpectedDiagnosticExceptionLogging&gt;b__9_0 /tmp/awc34xz5.byl 1.1 True Http3StreamType System.Private.Xml/src/System/Xml/Dom/XmlNodeChangedEventArgs.cs k/runtime/runtime/.dotnet/grep Http3StreamType ts/TargetTests.cpush rgo/bin/grep grep (dns block)
    • Triggering command: /home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-linux-Debug-x64/dotnet /home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-linux-Debug-x64/dotnet exec --runtimeconfig System.Net.Http.Functional.Tests.runtimeconfig.json --depsfile System.Net.Http.Functional.Tests.deps.json /home/REDACTED/.nuget/packages/microsoft.dotnet.xunitconsoleREDACTED/2.9.3-beta.25609.108/build/../tools/net/xunit.console.dll System.Net.Http.Functional.Tests.dll -xml testResults.xml -nologo -notrait category=OuterLoop -notrait category=failing /usr/bin/grep Http3StreamType ts/DataTests.cs cal/bin/grep grep (dns block)
    • Triggering command: /home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-linux-Debug-x64/dotnet /home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-linux-Debug-x64/dotnet exec --runtimeconfig /home/REDACTED/work/runtime/runtime/artifacts/bin/System.Net.Http.Functional.Tests/Debug/net11.0-linux/System.Net.Http.Functional.Tests.runtimeconfig.json --depsfile /home/REDACTED/work/runtime/runtime/artifacts/bin/System.Net.Http.Functional.Tests/Debug/net11.0-linux/System.Net.Http.Functional.Tests.deps.json /home/REDACTED/work/runtime/runtime/artifacts/bin/System.Net.Http.Functional.Tests/Debug/net11.0-linux/Microsoft.DotNet.RemoteExecutor.dll System.Net.Http.Functional.Tests, Version=11.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51 System.Net.Http.Functional.Tests.DiagnosticsTest&#43;&lt;&gt;c &lt;SendAsync_ExpectedDiagnosticExceptionActivityLogging&gt;b__24_0 /tmp/nvsf5lxs.wre 1.1 False Http3StreamType System.Private.X-d ep Http3StreamType /AppendTests.cs tnet/tools/grep grep (dns block)
  • www.some.example
    • Triggering command: /home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-linux-Debug-x64/dotnet /home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-linux-Debug-x64/dotnet exec --runtimeconfig System.Net.Http.Functional.Tests.runtimeconfig.json --depsfile System.Net.Http.Functional.Tests.deps.json /home/REDACTED/.nuget/packages/microsoft.dotnet.xunitconsoleREDACTED/2.9.3-beta.25609.108/build/../tools/net/xunit.console.dll System.Net.Http.Functional.Tests.dll -xml testResults.xml -nologo -notrait category=OuterLoop -notrait category=failing /usr/bin/grep Http3StreamType ts/DataTests.cs cal/bin/grep grep (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot AI changed the title Fix unhandled QuicException when server resets unidirectional stream during tracing Refactor HTTP/3 server stream handling to consolidate exception handling for stream resets Dec 17, 2025
Copilot AI requested a review from CarnaViire December 17, 2025 14:39
@MihaZupan MihaZupan enabled auto-merge (squash) December 18, 2025 16:26
@MihaZupan MihaZupan merged commit 25fc25e into main Dec 18, 2025
85 of 87 checks passed
@jkotas jkotas deleted the copilot/fix-http3-unidirectional-streams branch December 22, 2025 05:37
@github-actions github-actions bot locked and limited conversation to collaborators Jan 21, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Incorrect handling of unidirectional server-initiated streams in Http3Connection

6 participants