Skip to content

fix(http-stream): resolve race condition in close() and streamType override in final message#553

Merged
corinagum merged 2 commits into
microsoft:mainfrom
chonlaphoom:fix/http-stream-race-condition-and-streamtype
May 4, 2026
Merged

fix(http-stream): resolve race condition in close() and streamType override in final message#553
corinagum merged 2 commits into
microsoft:mainfrom
chonlaphoom:fix/http-stream-race-condition-and-streamtype

Conversation

@chonlaphoom

Copy link
Copy Markdown
Contributor

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

  1. Swap .withChannelData() and .addStreamFinal() order
  2. Add || this._flushing to the while condition"

…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
Copilot AI review requested due to automatic review settings May 1, 2026 07:51

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

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 accumulated channelData by reordering withChannelData() vs addStreamFinal() in close().
  • Fix a race in close() by waiting on _flushing in addition to queue.length and id.
  • Add unit tests covering both the final streamType behavior and the _flushing wait 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.

Comment thread packages/apps/src/http/http-stream.spec.ts Outdated
@chonlaphoom

Copy link
Copy Markdown
Contributor Author

@microsoft-github-policy-service agree

@corinagum corinagum left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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
corinagum merged commit ac21af6 into microsoft:main May 4, 2026
3 checks passed
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).
@heyitsaamir heyitsaamir mentioned this pull request May 6, 2026
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.
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 - race condition in close() and streamType override in final message

3 participants