Skip to content

[http-client-python] Add automatic SSE reconnection - #11904

Merged
Libba Lawrence (l0lawrence) merged 17 commits into
microsoft:mainfrom
l0lawrence:l0lawrence-python-sse-reconnection
Sep 22, 2026
Merged

Libba Lawrence (l0lawrence) merged 17 commits into
microsoft:mainfrom
l0lawrence:l0lawrence-python-sse-reconnection

Conversation

@l0lawrence

@l0lawrence Libba Lawrence (l0lawrence) commented Sep 9, 2026 •

Copy link
Copy Markdown
Member

Structured SSE streams currently stop at unexpected EOF and require callers to manually resume with last_event_id through an internal workaround. This adds automatic reconnection so generated clients follow the SSE processing model without requiring application-owned reconnect loops.

The generated stream runtime now:

  • reconnects after unexpected EOF using the default three-second delay or the latest valid retry: value --- Do we want a 3 second default or would we prefer no time delay, just immediately try to reconnect?
  • preserves and sends the latest event ID through Last-Event-ID
  • stops reconnecting after terminal events and HTTP 204 responses (per the spec)
  • routes reconnect failures through the generated operation's normal error mapping
  • supports both sync and async streams

Closes #11761

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 6bf64cba-786a-41ee-8e45-8567e1cf8568
@microsoft-github-policy-service microsoft-github-policy-service Bot added the emitter:client:python Issue for the Python client emitter: @typespec/http-client-python label Sep 9, 2026
@pkg-pr-new

pkg-pr-new Bot commented Sep 9, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@typespec/http-client-python@11904

commit: bacc909

@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

All changed packages have been documented.

  • ✅ @typespec/http-client-python
Show changes

@typespec/http-client-python - feature ✏️

Automatically reconnect SSE streams after an unexpected disconnect, honoring server-provided retry delays and resuming with the latest event ID.,> ,> python,> with client.events.stream() as stream:,> for event in stream:,> print(event),>

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 6bf64cba-786a-41ee-8e45-8567e1cf8568
@azure-sdk-automation

azure-sdk-automation Bot commented Sep 9, 2026 •

Copy link
Copy Markdown

You can try these changes here

🛝 Playground 🌐 Website 🛝 VSCode Extension

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 6bf64cba-786a-41ee-8e45-8567e1cf8568
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 6bf64cba-786a-41ee-8e45-8567e1cf8568
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 6bf64cba-786a-41ee-8e45-8567e1cf8568
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 6bf64cba-786a-41ee-8e45-8567e1cf8568
Use the public PipelineClient.send_request API when reconnecting SSE streams so generated Azure clients pass protected-access lint checks.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 6bf64cba-786a-41ee-8e45-8567e1cf8568

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.

🟡 Changes recommended

The reconnect implementation conflicts with the stated azure-core requirement, and event ID header handling has an unresolved bug.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds automatic sync and async SSE reconnection for generated Python clients, including retry delays, event ID resumption, terminal-event handling, and HTTP 204 stopping.

Changes:

  • Extends streaming runtimes with reconnection behavior.
  • Generates reconnect callbacks through the request and error-mapping pipeline.
  • Adds sync/async tests and a feature changelog.
File summaries
File Summary
packages/http-client-python/tests/unit/test_sse_streaming.py Tests generated reconnect behavior.
packages/http-client-python/tests/mock_api/shared/test_streaming_sse.py Adds synchronous SSE reconnection coverage.
packages/http-client-python/tests/mock_api/shared/asynctests/test_streaming_sse_async.py Adds asynchronous SSE reconnection coverage.
packages/http-client-python/generator/pygen/codegen/templates/streaming_base.py.jinja2 Adds reconnect runtime behavior. Critical (3 votes): Resolve the requirement to use an azure-core reconnect primitive instead of a generator-vendored loop.
packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py Generates reconnect callbacks. Moderate (2 votes): Omit Last-Event-ID when the event ID buffer is empty. Nit (1 vote each): Add redirect coverage and end-to-end reconnect error-mapping coverage.
.chronus/changes/http-client-python-sse-reconnection-2026-09-08.md Records the feature release change.
Review details

Suppressed comments (3)

packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py:1405

  • The normal operation path deserializes declared custom error responses and attaches them as HttpResponseError.model after map_error (the logic at builder_serializer.py:1170-1274). This reconnect path only calls map_error and then raises a bare HttpResponseError, so an unmapped custom or ranged error status during reconnect loses the generated error model and does not follow the operation's normal error behavior.
                "        map_error(status_code=_reconnect_response.status_code, "
                "response=_reconnect_response, error_map=error_map)"
            )
            retval.append("        raise HttpResponseError(response=_reconnect_response)")

packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py:1394

  • The linked requirement also calls out preserving 301/307 redirect handling, but the new sync/async integration tests only exercise 200/204 responses and do not drive a redirect during reconnect. Add a mock-server case for both modes so the generated send_request path and redirect policy are verified rather than only asserted as generated text.
                f"    _reconnect_response = {self._call_method}self._client.send_request("
                "_request, stream=True, **kwargs)"

packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py:1403

  • The new reconnect-failure branch is only checked as generated text; neither sync nor async tests execute a generated SSE operation that returns a non-success reconnect response and verifies the error mapping. Add an end-to-end generated-client case for this branch, including a custom error response, so this behavior is protected.
            retval.append(f"    if _reconnect_response.status_code not in {reconnect_status_codes!r}:")
            retval.append(f"        {self._call_method}_reconnect_response.read()")
            retval.append(
                "        map_error(status_code=_reconnect_response.status_code, "
                "response=_reconnect_response, error_map=error_map)"
  • Files reviewed: 6/6 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Remove Last-Event-ID from reconnect requests when the SSE ID buffer is empty, including after an empty id field clears a prior value.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 6bf64cba-786a-41ee-8e45-8567e1cf8568
…n-sse-reconnection' into l0lawrence-python-sse-reconnection
Move Last-Event-ID mutation into the shared SSE runtime so generated retrieve operations remain within Pylint's statement limit.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 6bf64cba-786a-41ee-8e45-8567e1cf8568
Add Azure SDK parameter documentation for the generated SSE reconnection header helper.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 6bf64cba-786a-41ee-8e45-8567e1cf8568

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.

🟡 Changes recommended

Two moderate findings remain unresolved regarding error-model preservation and use of the required azure-core streaming implementation.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (1)

packages/http-client-python/generator/pygen/codegen/templates/streaming_base.py.jinja2:709

  • The linked #11761 requirements explicitly call for using the azure-core streaming/transport implementation rather than a generator-vendored reconnect loop. This code puts the state machine in streaming_base.py.jinja2, which is copied into every generated client, so the generated runtime will fork from core behavior and miss future core fixes; integrate the core implementation (or update the stated requirement/dependency) instead.
        terminal = False
        try:
            while True:
  • Files reviewed: 7/7 changed files
  • Comments generated: 1
  • Review effort level: Lite

Keep parsed event IDs pending until a blank line completes the SSE block so incomplete events cannot advance the reconnect cursor.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 6bf64cba-786a-41ee-8e45-8567e1cf8568
Copilot AI review requested due to automatic review settings September 17, 2026 16:35

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.

🔵 Needs a closer look

The implementation embeds the reconnect loop in generated code and loses declared error models for reconnect failures.

Review details

Suppressed comments (2)

packages/http-client-python/generator/pygen/codegen/templates/streaming_base.py.jinja2:713

  • The linked issue explicitly defers this behavior until the azure-core streaming/transport layer provides a reconnect primitive and says not to add a generator-vendored reconnect loop. This new while True loop in the generated streaming runtime (together with the generated callback below) reintroduces that deferred architecture, so please integrate the azure-core primitive instead of embedding reconnection policy in every generated client.
            while True:
                if getattr(self._response, "status_code", None) == 204:
                    break

packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py:1404

  • This does not follow the operation's normal error path for declared error models: handle_error_response deserializes the reconnect error body and attaches model=error to HttpResponseError, whereas this branch only calls map_error and then raises without a model. A reconnect that returns a custom 4xx/5xx therefore loses the generated error payload; reuse the same error deserialization/mapping path for the reconnect response.
            retval.append(
                "        map_error(status_code=_reconnect_response.status_code, "
                "response=_reconnect_response, error_map=error_map)"
            )
            retval.append("        raise HttpResponseError(response=_reconnect_response)")
  • Files reviewed: 7/7 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

pull Bot pushed a commit to jrcribb/cadl that referenced this pull request Sep 21, 2026
…microsoft#12014)

## Summary

Enable Python emitter diff reports **and sticky comments** for fork pull
requests. The blanket job-level fork guard skipped every step of [the
emitter diff run for
microsoft#11904](https://github.com/microsoft/typespec/actions/runs/35247580542),
even though generation and report upload do not need a write token.

This follows the Go emitter's artifact-relay pattern and trusted
head-repository/branch targeting fix: Azure/typespec-azure#5124 and
Azure/typespec-azure#5191 ([current Go
commenter](https://github.com/Azure/typespec-azure/blob/main/.github/workflows/ci-emitter-diff-go-commenter.yml)).

- Keep generation on `pull_request`, with no fork guard, only `contents:
read`, and checkout credential persistence disabled. Preserve the HTML
report, inline summary, manual dispatch, and nonzero tool/build
failures; generated-code changes remain informational.
- Replace same-repository-only commenting with a small
`emitter-diff-python-comment` JSON artifact containing only comment
text. Save/upload it for PR runs, including setup/generation failures;
manual dispatch does not produce PR comments.
- Add `python / emitter diff commenter`, triggered by completed
`workflow_run` events for exactly `python / emitter diff`, with only
`actions: read` and `pull-requests: write`.
- Update the README and add durable tests that execute the workflow's
actual inline scripts with mocked APIs/filesystem.

## Privilege boundary and targeting

The listener **never checks out, builds, or executes PR code or artifact
scripts**. The producer setup/build paths use public source/dependency
downloads and require no privileged credentials. This does not use
`pull_request_target` or a privileged dispatch mechanism.

The listener verifies the producer workflow path and resolves exactly
one open PR from trusted `workflow_run` head repository owner/full name,
branch, and SHA. It lists PRs by `owner:branch`, then checks exact head
SHA/ref/repository and base repository. It does not trust artifact PR
numbers/markers, `workflow_run.pull_requests` (empty for forks), or
commit-association lookup.

Artifact data is only a nonempty string body (maximum 8,000 characters)
in a regular JSON file of at most 16 KiB. The marker and run stamp are
hardcoded/derived from trusted metadata. Artifact and comment lists are
paginated; only the intended GitHub Actions bot-owned marker comment can
be updated. The listener skips stale/closed/ambiguous PRs, rechecks the
current head immediately before posting, and combines per-branch
serialization with a run/attempt stamp to prevent older runs overwriting
newer comments.

Cancelled/skipped runs skip explicitly. Failed runs without a comment
artifact emit an explicit warning. Missing artifacts on successful runs,
expired/duplicate artifacts, malformed payloads, ambiguous bot comments,
download errors, and API errors surface as failures rather than being
silently ignored.

## Reviewing reports

Open the PR's **python / emitter diff** check, follow **Details**, and
read the job summary or download **emitter-diff-html** from the run's
**Artifacts** section (seven-day retention). After the listener is
active, fork and same-repository PRs also receive a sticky comment. The
small comment artifact is retained for one day.

## Validation

- **39 new durable workflow tests pass**, covering
fork/same-repository/manual conditions, trusted PR selection,
stale/closed/ambiguous/missing PRs, workflow identity, artifact errors,
bounded malformed data, bot ownership, pagination, newer-run guards, and
API failures. These execute the inline producer/listener scripts locally
with mocked APIs, not a hosted privileged listener.
- Twelve existing engineering path-selection tests pass (51 tests
total). `pnpm check:eng` and focused type-aware oxlint for the new tests
pass.
- Actionlint v1.7.12 (the repository CI version) passes for both
workflows; optional shellcheck/pyflakes integrations were disabled
because those tools are not installed locally.
- Required `pnpm format` completed with only the four intended files
changed. Targeted Prettier, cspell, and `git diff --check` pass.
- `pnpm chronus add @typespec/http-client-python --kind internal ...`
reported “No package changed”; `pnpm chronus verify --since origin/main`
passes. No package changelog is needed for workflow/docs/tests-only
changes.
- Full `pnpm lint` and `pnpm lint:fix` were reattempted and remain
blocked by the unchanged compiler tsconfig error that writing
`dist/src/server/tmlanguage.d.ts` would overwrite an input file. No
unrelated repair is included.
- Earlier full-build attempt completed 35 tasks before `typespec-vs`
failed NuGet restore due to local TLS failure reaching `api.nuget.org`
(NU1301). The compiler smoke check passed then; no compiler/emitter
implementation changes are introduced here.
- The prior revision's [hosted fork producer
run](https://github.com/microsoft/typespec/actions/runs/35305191236)
passed generation and uploaded HTML. That verifies removal of the
generation fork gate, **not this new listener**. End-to-end hosted fork
commenting cannot be verified until the listener lands on the default
branch.

## Activation for microsoft#11904

This PR does not modify microsoft#11904 or its author's branch. The new
`workflow_run` listener must first land on the repository's **default
branch**. Then update the SSE PR branch from `microsoft/typespec` main
and push a new producer run containing the comment-artifact changes
(approve the fork run if repository policy requires it). Rerunning the
original skipped run retains its old workflow revision and does not
apply this fix. No merges or privileged dispatches were performed.

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 21, 2026 19:08

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.

Copilot review overview

🔵 Needs a closer look

Reconnect failures lose operation-specific error mapping, and the implementation does not use the azure-core reconnect primitive required by the linked issue.

Review effort: Lite
Findings: 1 Medium severity

Open (1)

@github-actions

github-actions Bot commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Python emitter diff

Baseline gh:bcd8ff10ca996d912eebff01b25a777fbf5bd51c vs this PR.

Diff summary: 24 file(s), +1476 / -244

Rendered diff: inline on the run summary, or the emitter-diff-html artifact.

Informational check (eng/emitter-diff); does not block the PR.

Reuse normal operation error handling for reconnect responses, including declared and default error model deserialization.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 6bf64cba-786a-41ee-8e45-8567e1cf8568
Copilot AI review requested due to automatic review settings September 22, 2026 17:19

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.

Copilot review overview

🔵 Needs a closer look

Moderate unresolved issues remain in the reconnect runtime and event ID handling.

Review effort: Lite
Findings: None

Resolved since last review (1)
Previously missed (1)

In code that hasn't changed since last review

Medium severity Persist SSE IDs parsed from incomplete event blocks

packages/​http-client-python/​generator/​pygen/​codegen/​templates/​streaming_base.py.jinja2:491

A valid SSE id: field updates the last-event-ID buffer when it is parsed, even if the current event block is incomplete. Deferring _last_id until _dispatch() means an EOF after id: second reconnects with a stale ID, so update the committed buffer in this branch as well as the pending value, and adjust the new incomplete-block test accordingly.

Move guarded reconnect response reads into the streaming runtime so generated operations remain below Pylint's statement limit.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 6bf64cba-786a-41ee-8e45-8567e1cf8568
Copilot AI review requested due to automatic review settings September 22, 2026 18:02

@iscai-msft iscai-msft left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

yay amazing

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.

Copilot review overview

🟡 Changes recommended

Unresolved findings include incorrect incomplete-event ID handling and missing or inconsistent reconnect integration and coverage.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 High severity · 2 Medium severity

Open (3)

@l0lawrence
Libba Lawrence (l0lawrence) added this pull request to the merge queue Sep 22, 2026
Merged via the queue into microsoft:main with commit b74df9c Sep 22, 2026
37 checks passed
@l0lawrence
Libba Lawrence (l0lawrence) deleted the l0lawrence-python-sse-reconnection branch September 22, 2026 19:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

emitter:client:python Issue for the Python client emitter: @typespec/http-client-python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[http-client-python] Add SSE reconnection

5 participants