Skip to content

feat: graceful stream cancellation on 403#513

Merged
heyitsaamir merged 3 commits intomainfrom
feat/stream-cancellation
Apr 13, 2026
Merged

feat: graceful stream cancellation on 403#513
heyitsaamir merged 3 commits intomainfrom
feat/stream-cancellation

Conversation

@heyitsaamir
Copy link
Copy Markdown
Collaborator

@heyitsaamir heyitsaamir commented Apr 8, 2026

Summary

  • Detect when Teams stops a stream (user presses Stop or 2-min timeout) via 403 response and cancel immediately instead of silently retrying until timeout
  • Add StreamCancelledError class and canceled property to IStreamer interface
  • HttpStream.send() catches 403, sets canceled flag, and throws StreamCancelledError
  • emit() and close() bail out immediately when canceled
  • retry() skips retries for StreamCancelledError (no point retrying a permanent cancellation)
  • flush() suppresses error log for expected cancellation
  • app.process catches StreamCancelledError and returns 200 so Teams doesn't retry

Ports teams.py#337 to TypeScript.

To test, I ran a streaming bot, press Stop mid-stream — verify no held connection, verify future messages weren't impacted, verified that stream didn't restart b/c of a 500.

Copy link
Copy Markdown
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.

Pull request overview

Note

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

This PR ports Teams stream-cancellation handling to TypeScript by detecting “stream stopped” via HTTP 403 and short-circuiting future streaming work (no retries, no noisy logs), while ensuring the inbound request still returns 200 to prevent Teams retries.

Changes:

  • Introduces StreamCancelledError and adds a canceled flag to IStreamer.
  • Updates HttpStream to mark streams canceled on 403, block emit/send/close when canceled, and suppress expected cancellation logs.
  • Updates retry() and $process() to avoid retrying/handling cancellations as failures.

Reviewed changes

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

Show a summary per file
File Description
packages/apps/src/utils/promises/retry.ts Skips retries by rethrowing StreamCancelledError immediately.
packages/apps/src/types/streamer.ts Adds StreamCancelledError and extends IStreamer with canceled.
packages/apps/src/http/http-stream.ts Detects 403 cancellations, sets _canceled, bails early in emit/close/send, and suppresses cancellation logging.
packages/apps/src/http/http-stream.spec.ts Adds test coverage around 403-driven cancellation and post-cancel behavior.
packages/apps/src/app.process.ts Treats StreamCancelledError as a successful (200) outcome so Teams won’t retry.
Comments suppressed due to low confidence (1)

packages/apps/src/http/http-stream.ts:1

  • Given the while ((this.queue.length || !this.id) && !this._canceled) loop condition, control flow shouldn’t reach the if (!this.id) block unless the earlier timeout/cancel paths return (and cancel already returns just above). This makes the no stream id set warning effectively unreachable/redundant; consider removing it or restructuring the loop/conditions so the warning is emitted on a reachable path.
import {

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

Comment thread packages/apps/src/types/streamer.ts Outdated
Comment thread packages/apps/src/types/streamer.ts Outdated
Comment thread packages/apps/src/http/http-stream.ts
Comment thread packages/apps/src/http/http-stream.ts
Comment thread packages/apps/src/http/http-stream.ts
Comment thread packages/apps/src/http/http-stream.ts
Comment thread packages/apps/src/utils/promises/retry.ts Outdated
Comment thread packages/apps/src/http/http-stream.spec.ts
heyitsaamir and others added 3 commits April 10, 2026 17:41
Detect when Teams stops a stream (user presses Stop or 2-minute timeout)
via 403 response and cancel the stream immediately instead of silently
retrying until timeout.

- Add StreamCancelledError class and canceled property to IStreamer
- HttpStream.send() catches 403 and sets canceled flag
- emit() and close() bail out immediately when canceled
- retry() skips retries for StreamCancelledError
- flush() suppresses error log for expected cancellation
- app.process catches StreamCancelledError and returns 200

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
- Standardize on 'canceled' spelling and use default error message
- Update StreamCancelledError docstring to cover both 403 and post-cancel cases
- Add err.name fallback for instanceof checks in retry and process
- Keep !this.id guard in close() for type narrowing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@heyitsaamir heyitsaamir force-pushed the feat/stream-cancellation branch from 545f427 to 10f8669 Compare April 11, 2026 00:42
@heyitsaamir
Copy link
Copy Markdown
Collaborator Author

This change is part of the following stack:

Change managed by git-spice.

@rajan-chari
Copy link
Copy Markdown
Contributor

Reviewed this with a few folks who have streaming context. Looks good — clean design.

Architecture trace works end-to-end: 403 in send()_canceled flag → StreamCancelledErrorretry() re-throws immediately → flush() suppresses log → emit()/close() bail early → app.process returns 200. Each layer does one thing.

403 assumption is safe. Mid-stream, auth has already succeeded (first send() established the stream ID). A 403 at that point is cancellation — Stop button or 2-min timeout. Token expiry would be 401, and permission failures would hit on the first send. Even edge cases like admin revoking permissions mid-stream — treating as cancellation (stop retrying, return 200) is the correct behavior.

Minor notes (non-blocking):

  • IStreamer.canceled is technically breaking for custom implementors, but only HttpStream implements it — worth a minor version note
  • Double cancel check in close() (before and after wait loop) is good — catches the mid-wait race
  • Nice to have: test cases for retry() with StreamCancelledError and mid-wait cancellation in close()

LGTM

@heyitsaamir heyitsaamir merged commit 3e3abc3 into main Apr 13, 2026
12 checks passed
@heyitsaamir heyitsaamir deleted the feat/stream-cancellation branch April 13, 2026 16:44
@heyitsaamir heyitsaamir mentioned this pull request Apr 16, 2026
heyitsaamir added a commit that referenced this pull request Apr 16, 2026
## Summary
- Merges all changes from `main` since v2.0.7 into `release`
- Sets `version.json` to stable `2.0.8`

### What's included
- **feat:** Sovereign cloud support (GCCH, DoD, China) (#500)
- **feat:** Add missing endpoints — Paged Members, Meeting Notifications
& client gaps (#516)
- **feat:** Graceful stream cancellation on 403 (#513)
- **feat:** GitHub issue analysis → Teams notification workflow (#517)
- **fix:** Improve error message when app credentials are missing (#527)
- **fix:** Surface Graph API error body in GraphError (#524)
- **fix:** Resolve missing deps, broken JSON, type errors, and typos in
CLI templates (#521)
- **fix:** Drain entire queue per flush cycle (#520)
- **fix:** Merge User-Agent headers when cloning HTTP client (#508)
- Dependency bumps: hono, axios, vite, @hono/node-server

## Post-merge
1. Trigger the [release
pipeline](https://dev.azure.com/DomoreexpGithub/Github_Pipelines/_build?definitionId=52&_a=summary)
for `release` with **Public** publish type
2. Bump `version.json` on `main` to `2.0.9-preview.{height}`

🤖 Generated with [Claude Code](https://claude.com/claude-code)
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.

4 participants