Skip to content

Fix HttpStream Close() race + AddStreamFinal() streamType override#474

Merged
corinagum merged 3 commits into
mainfrom
cg/http-stream-race-and-streamtype-fix
May 7, 2026
Merged

Fix HttpStream Close() race + AddStreamFinal() streamType override#474
corinagum merged 3 commits into
mainfrom
cg/http-stream-race-and-streamtype-fix

Conversation

@corinagum

@corinagum corinagum commented May 4, 2026

Copy link
Copy Markdown
Collaborator

Fixes #473

Summary

Two bugs in the streaming path. Parity of microsoft/teams.ts#553, but Bug 1 manifests via a different mechanism in C# (??=) than in TS.

Bug 1: AddStreamFinal() cannot override existing ChannelData.StreamType

MessageActivity.AddStreamFinal() used null-coalescing assignment:

ChannelData.StreamType ??= StreamType.Final;

In the streaming Close() path:

  1. Stream.Update("Thinking...") accumulates StreamType.Informative into _channelData.
  2. Close() calls WithData(_channelData), populating activity.ChannelData.StreamType = Informative.
  3. AddStreamFinal() runs ??= against Informativeno-op. The wire channelData.streamType stays as "informative" on the final message, even though the streaminfo entity correctly has "final".

Fix: direct assignment. The method's contract per its name is to mark the activity as a final stream message, so it must be able to override prior values.

Note: This is a minor public-API behavior change. Any caller that pre-set ChannelData.StreamType and then called AddStreamFinal() expecting their value to persist will now have it overridden. The method's name and contract make the override semantic the obvious one; we judged this acceptable.

Bug 2: Race condition in Stream.Close()

Close()'s wait loop checked _id and _queue but not whether Flush() was holding _lock. During a flush mid-await (queue drained, SendActivity() awaits pending), Close() would exit the loop and send the final activity, racing the in-flight chunk.

Fix: also wait while _lock.CurrentCount == 0 (semaphore held). This is the same primitive already used in Close()'s early-return guard on line 88.

Changes

  • Libraries/Microsoft.Teams.Api/Activities/Message/MessageActivity.csAddStreamFinal() direct assignment.
  • Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/AspNetCorePlugin.Stream.csClose() lock-aware wait.
  • Tests in MessageActivityTests.cs and AspNetCorePluginStreamTests.cs.

Test plan

  • AddStreamFinal_OverridesExistingStreamTypeMessageActivity with StreamType.Informative pre-set, AddStreamFinal() overrides to Final.
  • Stream_Close_FinalMessageHasStreamTypeFinal_AfterInformativeUpdate — full Update→Emit→Close flow asserts final activity has StreamType.Final on both ChannelData and the streaminfo entity.
  • Stream_Close_WaitsForInFlightFlushToComplete — uses a controllable Send delegate to block the second flush mid-await; asserts Close() does not progress until the lock releases.
  • All existing MessageActivityTests (21 passed) and AspNetCorePluginStreamTests (6 passed) still pass on net10.0. (net8.0 testhost not installed locally; CI will cover.)
  • Full solution builds clean.

Copilot AI review requested due to automatic review settings May 4, 2026 17:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR fixes two streaming-path issues: ensuring final stream activities always carry StreamType.Final, and making Stream.Close() wait for an in-flight flush before sending the final activity.

Changes:

  • Update MessageActivity.AddStreamFinal() to assign final stream metadata directly.
  • Update AspNetCorePlugin.Stream.Close() to wait while a flush still holds the semaphore.
  • Add tests covering final-message stream type behavior and the Close()/Flush() race.

Reviewed changes

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

File Description
Tests/Microsoft.Teams.Plugins.AspNetCore.Tests/AspNetCorePluginStreamTests.cs Adds integration-style tests for final stream metadata and the in-flight flush race.
Tests/Microsoft.Teams.Api.Tests/Activities/Message/MessageActivityTests.cs Adds a unit test for AddStreamFinal() overriding an existing stream type.
Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/AspNetCorePlugin.Stream.cs Changes Close() to also wait while the flush semaphore is held.
Libraries/Microsoft.Teams.Api/Activities/Message/MessageActivity.cs Changes AddStreamFinal() from null-coalescing assignment to direct assignment.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread Libraries/Microsoft.Teams.Api/Activities/Message/MessageActivity.cs Outdated
Comment thread Tests/Microsoft.Teams.Plugins.AspNetCore.Tests/AspNetCorePluginStreamTests.cs Outdated
Comment thread Tests/Microsoft.Teams.Plugins.AspNetCore.Tests/AspNetCorePluginStreamTests.cs Outdated
corinagum added a commit that referenced this pull request May 4, 2026
- AddStreamFinal: keep StreamId as ??= (don't clobber an existing value)
- Stream tests: replace Task.Delay-based waits with TaskCompletionSource
  signals from the Send delegate; simplify Bug 1 integration test to rely
  on Close()'s wait loop instead of fixed sleeps
corinagum added a commit that referenced this pull request May 5, 2026
- AddStreamFinal: keep StreamId as ??= (don't clobber an existing value)
- Stream tests: replace Task.Delay-based waits with TaskCompletionSource
  signals from the Send delegate; simplify Bug 1 integration test to rely
  on Close()'s wait loop instead of fixed sleeps
Copilot AI review requested due to automatic review settings May 5, 2026 17:14
@corinagum
corinagum force-pushed the cg/http-stream-race-and-streamtype-fix branch from 75b0c96 to 5ceed67 Compare May 5, 2026 17:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

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


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

corinagum added a commit that referenced this pull request May 6, 2026
- AddStreamFinal: keep StreamId as ??= (don't clobber an existing value)
- Stream tests: replace Task.Delay-based waits with TaskCompletionSource
  signals from the Send delegate; simplify Bug 1 integration test to rely
  on Close()'s wait loop instead of fixed sleeps
@corinagum
corinagum force-pushed the cg/http-stream-race-and-streamtype-fix branch from 5ceed67 to 336a647 Compare May 6, 2026 22:11
corinagum added a commit that referenced this pull request May 6, 2026
- AddStreamFinal: keep StreamId as ??= (don't clobber an existing value)
- Stream tests: replace Task.Delay-based waits with TaskCompletionSource
  signals from the Send delegate; simplify Bug 1 integration test to rely
  on Close()'s wait loop instead of fixed sleeps
Copilot AI review requested due to automatic review settings May 6, 2026 23:14
@corinagum
corinagum force-pushed the cg/http-stream-race-and-streamtype-fix branch from 336a647 to 94cdba9 Compare May 6, 2026 23:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

Libraries/Microsoft.Teams.Api/Activities/Message/MessageActivity.cs:217

  • AddStreamFinal() sets StreamInfoEntity.StreamId from Id, but leaves ChannelData.StreamId as ??= Id. If a caller pre-sets ChannelData.StreamId (or Id is null), this can produce a mismatch between channelData.streamId and the streaminfo entity’s streamId. Consider making ChannelData.StreamId assignment consistent with the entity (or vice versa) so the final activity is internally consistent.
        ChannelData ??= new();
        ChannelData.StreamId ??= Id;
        ChannelData.StreamType = StreamType.Final;

        AddEntity(new StreamInfoEntity()
        {
            StreamId = Id,
            StreamType = StreamType.Final

Comment thread Tests/Microsoft.Teams.Plugins.AspNetCore.Tests/AspNetCorePluginStreamTests.cs Outdated

@lilyydu lilyydu left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM though copilot's comments seem worthy to evaluate

corinagum added 3 commits May 7, 2026 10:41
…de (#473)

Two bugs in the streaming path:

1. AddStreamFinal() used `??=` for ChannelData.StreamId / StreamType, so it
   could not override an existing StreamType. The streaming Close() path
   accumulates `_channelData.StreamType = Informative` from prior typing
   updates and merges that into the final activity via WithData() before
   calling AddStreamFinal() — which then no-op'd, leaving the wire
   `channelData.streamType` as "informative" on the final message even
   though the streaminfo entity correctly said "final".

   Fix: direct assignment in AddStreamFinal(). The method's contract per
   its name is to mark the activity as a final stream message, so it must
   be able to override prior values.

2. Stream.Close() polled `_id` and `_queue` but not `_lock` — during a
   flush mid-await (queue drained, SendActivity awaits pending), Close()
   would proceed and send the final message before the in-flight chunk
   completed.

   Fix: also wait while `_lock.CurrentCount == 0`. This is the same
   primitive already used in Close()'s early-return guard.
- AddStreamFinal: keep StreamId as ??= (don't clobber an existing value)
- Stream tests: replace Task.Delay-based waits with TaskCompletionSource
  signals from the Send delegate; simplify Bug 1 integration test to rely
  on Close()'s wait loop instead of fixed sleeps
Copilot AI review requested due to automatic review settings May 7, 2026 17:41
@corinagum
corinagum force-pushed the cg/http-stream-race-and-streamtype-fix branch from 3ce81e0 to acb6187 Compare May 7, 2026 17:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

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

@corinagum
corinagum merged commit 5dbe296 into main May 7, 2026
12 checks passed
@corinagum
corinagum deleted the cg/http-stream-race-and-streamtype-fix branch May 7, 2026 17:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: HttpStream Close() race condition + AddStreamFinal channelData.StreamType override

3 participants