fix(http-stream): resolve race condition in close() and streamType override in final message#553
Merged
corinagum merged 2 commits intoMay 4, 2026
Conversation
…erride in final message - Add || this._flushing to close() wait loop to prevent sending final message before ongoing flush completes - Swap .withChannelData() and .addStreamFinal() order so streamType final is not overwritten by accumulated channelData - Add unit tests for both fixes
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes two correctness issues in HttpStream.close() to ensure the final streaming activity is sent with the correct streamType and only after any in-flight flush work has completed.
Changes:
- Prevent
streamType: 'final'from being overwritten by previously accumulatedchannelDataby reorderingwithChannelData()vsaddStreamFinal()inclose(). - Fix a race in
close()by waiting on_flushingin addition toqueue.lengthandid. - Add unit tests covering both the final
streamTypebehavior and the_flushingwait behavior.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| packages/apps/src/http/http-stream.ts | Updates close() wait condition to include _flushing and reorders activity builder calls so the final message retains streamType: 'final'. |
| packages/apps/src/http/http-stream.spec.ts | Adds regression tests for final streamType after update() and for close() waiting until _flushing completes. |
Comments suppressed due to low confidence (1)
packages/apps/src/http/http-stream.ts:132
- The while condition now waits on
this._flushing, but the debug log inside the loop still says it’s waiting for the id/queue. Updating the log message to mention flushing would make diagnostics match the actual behavior.
while ((this.queue.length || !this.id || this._flushing) && !this._canceled) {
if (Date.now() - start > this._totalTimeout) {
this._logger.warn('Timeout while waiting for id and queue to flush');
return;
}
this._logger.debug('waiting for id to be set or queue to be empty');
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Contributor
Author
|
@microsoft-github-policy-service agree |
Co-authored-by: Copilot <[email protected]>
corinagum
approved these changes
May 4, 2026
corinagum
left a comment
Collaborator
There was a problem hiding this comment.
Thank you for the contribution!
As a side note, Python and C# will need some equivalent updates. Filed here:
microsoft/teams.py#418
microsoft/teams.net#473
I've started working on these :)
corinagum
added a commit
to microsoft/teams.py
that referenced
this pull request
May 4, 2026
… message (#419) Fixes #418 ## Summary `HttpStream.close()` could send the final stream message while `_flush()` was still mid-await, causing the final message to arrive before in-flight chunks finished sending. This is the Python parity of [microsoft/teams.ts#553](microsoft/teams.ts#553) (Bug 2 only — Bug 1 from the TS PR does not affect Python; `close()` already calls `with_channel_data()` before `add_stream_final()`). ## Root cause `_wait_for_id_and_queue()` polled: ```python while (self._queue or not self._id) and not self._canceled: ``` This checks the queue and `_id` but not whether `_flush()` is currently holding `self._lock`. After the inner `while self._queue` loop drains the queue, `_flush()` awaits `_send_activity(...)` calls under the lock. During those awaits: - `self._queue` is empty (drained) - `self._id` is set (after the first chunk) - `self._lock` is still held The predicate becomes false, the wait exits, and `close()` proceeds to send the final activity — racing the in-flight chunk. ## Fix Add `self._lock.locked()` to the wait predicate. The lock primitive is already used in `close()`'s early-return guard at line 157, so this is consistent with existing patterns. ## Test plan - [x] Added `test_close_waits_for_flush_to_complete` — holds `stream._lock` manually, asserts `close()` does not send the final message until the lock is released. - [x] All existing http-stream tests still pass (14/14). - [x] Full apps test suite passes (271 passed). - [x] `poe check` passes (ruff format + lint).
Merged
heyitsaamir
added a commit
that referenced
this pull request
May 6, 2026
Merges main into release and sets version to 2.0.10. ## Commits since last release - 7147cd8 fix(apps): support AAD v1 issuers in token validation (#556) - eb8037e address model gaps (#525) - ac21af6 fix(http-stream): resolve race condition in close() and streamType override in final message (#553) - 6b0da8a MCP Server Example on MCP SDK (#534) - a749172 chore: bump version to 2.0.10-preview (#555)
corinagum
added a commit
to microsoft/teams.net
that referenced
this pull request
May 7, 2026
) Fixes #473 ## Summary Two bugs in the streaming path. Parity of [microsoft/teams.ts#553](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: ```csharp 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 `Informative` → **no-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.cs` — `AddStreamFinal()` direct assignment. - `Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/AspNetCorePlugin.Stream.cs` — `Close()` lock-aware wait. - Tests in `MessageActivityTests.cs` and `AspNetCorePluginStreamTests.cs`. ## Test plan - [x] `AddStreamFinal_OverridesExistingStreamType` — `MessageActivity` with `StreamType.Informative` pre-set, `AddStreamFinal()` overrides to `Final`. - [x] `Stream_Close_FinalMessageHasStreamTypeFinal_AfterInformativeUpdate` — full Update→Emit→Close flow asserts final activity has `StreamType.Final` on both `ChannelData` and the `streaminfo` entity. - [x] `Stream_Close_WaitsForInFlightFlushToComplete` — uses a controllable `Send` delegate to block the second flush mid-await; asserts `Close()` does not progress until the lock releases. - [x] All existing `MessageActivityTests` (21 passed) and `AspNetCorePluginStreamTests` (6 passed) still pass on net10.0. (net8.0 testhost not installed locally; CI will cover.) - [x] Full solution builds clean.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #549
Summary
Fixes two bugs in HttpStream:
Bug 1: streamType overwritten to informative in final message
close() calls .addStreamFinal().withChannelData(this.channelData). Since this.channelData contains streamType: informative from typing updates, it overwrites the final value.
Bug 2: Race condition in close()
The while loop checks queue.length and this.id but not this._flushing. If flush() drained the queue but is still awaiting pushStreamChunk() calls, close() proceeds immediately.
Fixes