Skip to content

[azure-core/corehttp] Add JSONL and SSE streaming support - #48077

Open
Libba Lawrence (l0lawrence) wants to merge 19 commits into
Azure:feature/azure-core-streamingfrom
l0lawrence:l0lawrence-jsonl-stream-followup
Open

[azure-core/corehttp] Add JSONL and SSE streaming support#48077
Libba Lawrence (l0lawrence) wants to merge 19 commits into
Azure:feature/azure-core-streamingfrom
l0lawrence:l0lawrence-jsonl-stream-followup

Conversation

@l0lawrence

@l0lawrence Libba Lawrence (l0lawrence) commented Jul 15, 2026

Copy link
Copy Markdown
Member

Adds streaming support to both azure-core (azure.core.streaming) and corehttp (corehttp.streaming), kept in sync so the two implementations stay identical (only the rest-path docstrings differ).

What's included (per package)

Core

  • Stream / AsyncStream — decoder-agnostic iterators (__iter__/__aiter__ return self)
  • deserialization_callback receives (response, event) so generated code can support the cls custom-deserializer pattern

JSONL

  • JSONLDecoder / AsyncJSONLDecoder with incremental UTF-8 line decoding
  • Lines are split only on \n (with \r\n tolerated), not str.splitlines(), so a JSON string value containing other Unicode line boundaries (e.g. \u2028, \x85) is not split mid-record

SSE (Server-Sent Events)

  • ServerSentEvent value object (event, data, id, retry) plus SSEDecoder / AsyncSSEDecoder, conforming to the same decoder protocol as JSONL so they drop into Stream/AsyncStream unchanged
  • Spec-faithful parsing per the WHATWG SSE spec: CR / LF / CRLF line splitting (incl. a lone \r split across chunks), :-comment lines ignored, one leading space stripped from values, multiple data: lines joined with \n, id persisted across events (NUL rejected), retry parsed only when ASCII-digits
  • Always decoded as UTF-8: a single leading BOM is dropped only when present, and invalid byte sequences become U+FFFD instead of crashing the stream. JSONL stays strict (the JSONL spec forbids a BOM).

Per package also: sync + async tests, test-server JSONL/SSE routes, sample (samples/sample_stream.py), CHANGELOG entry + README section (azure-core also bumps version to 1.42.0).

Automatic reconnection (honoring retry / re-sending Last-Event-ID on a dropped connection) is intentionally out of scope here — the decoders are pure parsers, and the reconnect loop belongs in a higher layer. Tracked as a follow-up.

Validation (both packages)

  • Streaming suite passes: 48 tests each (sync + async), covering JSONL, SSE field/line-ending edge cases, BOM handling, and invalid-UTF-8 replacement

What client code to return a Stream() would look like:
image

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
7 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@l0lawrence
Libba Lawrence (l0lawrence) force-pushed the l0lawrence-jsonl-stream-followup branch from f10ef48 to 785e97e Compare July 15, 2026 22:47
@l0lawrence Libba Lawrence (l0lawrence) changed the title [corehttp] JSONL streaming (follow-up on #39478) [azure-core] Add JSONL streaming support Jul 15, 2026
Adds a stream-agnostic Stream/AsyncStream iterator plus
JSONLDecoder/AsyncJSONLDecoder to both azure-core (azure.core.streaming)
and corehttp (corehttp.streaming), kept in sync. The
deserialization_callback receives the response and each decoded event so
generated code can support the cls custom-deserializer pattern.

Each package includes sync/async tests, test-server JSONL routes, a
sample, CHANGELOG entry, and a README section (azure-core also bumps the
version to 1.42.0).

Based on the design in
https://gist.github.com/kristapratico/d330af39962ea05b10384b865e37b36f
and the original corehttp prototype in Azure#39478.
Part of Azure#38806.

Co-authored-by: Krista Pratico <krpratic@microsoft.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@l0lawrence
Libba Lawrence (l0lawrence) force-pushed the l0lawrence-jsonl-stream-followup branch from 785e97e to 8fe667c Compare July 15, 2026 22:59
@l0lawrence Libba Lawrence (l0lawrence) changed the title [azure-core] Add JSONL streaming support [azure-core/corehttp] Add JSONL streaming support Jul 15, 2026
Comment thread sdk/core/azure-core/azure/core/streaming/_decoders.py Outdated
Comment thread sdk/core/azure-core/azure/core/streaming/_stream.py Outdated
Comment thread sdk/core/corehttp/corehttp/streaming/_stream.py Outdated
- iter_lines/aiter_lines split only on \n (tolerating \r\n) instead of
  str.splitlines(), preserving Unicode boundaries (\u2028/\u2029/\x85)
  inside JSONL record string values.
- Stream.__iter__/AsyncStream.__aiter__ return self (typed Self).
- Fix corehttp _stream.py docstring cross-refs to _decoders.
- Add Server-Sent Events (SSE) support: ServerSentEvent, SSEDecoder,
  AsyncSSEDecoder in both packages, with server routes and sync/async tests.

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

Copilot-Session: 08d2aa98-be13-48ab-89ea-531b494b9482
- Guard SSE retry parsing with isascii() so Unicode digits (e.g. superscript) that pass str.isdigit() but fail int() no longer crash the stream.
- Restore azure-core test_stream_jsonl_basic, whose def line was accidentally removed while editing SSE test fixtures.

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

Copilot-Session: 08d2aa98-be13-48ab-89ea-531b494b9482
SSE is always UTF-8 per the WHATWG spec. Switch the SSE line decoders to
the incremental utf-8-sig decoder with errors=replace so that:
- a single leading BOM is dropped only when present, and
- invalid byte sequences become U+FFFD instead of crashing the stream.

JSONL decoding is intentionally left strict (the JSONL spec forbids a BOM).
Add server routes and sync/async tests covering a leading BOM, a BOM split
across chunks, and invalid UTF-8 for both packages.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 08d2aa98-be13-48ab-89ea-531b494b9482
@l0lawrence Libba Lawrence (l0lawrence) changed the title [azure-core/corehttp] Add JSONL streaming support [azure-core/corehttp] Add JSONL and SSE streaming support Jul 31, 2026
Move JSONL decoders into _jsonl.py and SSE decoders into _sse.py, keeping
the shared StreamDecoder/AsyncStreamDecoder protocols in _decoders.py.
Public API is unchanged.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 08d2aa98-be13-48ab-89ea-531b494b9482
An all-ASCII-digit retry value longer than CPython's int-string conversion
limit would raise ValueError and abort the stream. Ignore such values per
the WHATWG SSE spec's robustness intent. Adds sync + async regression tests
in both packages.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 08d2aa98-be13-48ab-89ea-531b494b9482
Generalize the Stream/AsyncStream summaries (they consume any decoded event
stream, not just JSONL) and point the decoder cross-references at the public
StreamDecoder/AsyncStreamDecoder exports so both packages match.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 08d2aa98-be13-48ab-89ea-531b494b9482
…ample

Add a parametrized chunk-size sweep (sync + async) that replays an SSE
payload at chunk sizes down to a single byte, exercising line-separator
and multibyte UTF-8 splits across arbitrary chunk boundaries. Also add
sync/async SSE samples to sample_stream.py demonstrating OpenAI-style
consumption, including the non-JSON `data: [DONE]` sentinel.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: fffeb0d7-6fcb-4485-8917-02315a708f72
Align the initial last-event ID with the WHATWG SSE processing model by
using an empty string instead of None. Update the public event model,
documentation, and sync/async expectations in both packages.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: fffeb0d7-6fcb-4485-8917-02315a708f72
Select SSE decoding for text/event-stream responses and default other
responses to JSONL when no decoder is supplied. Normalize media type
parameters and casing while preserving explicit custom decoder overrides.
Update samples and sync/async coverage in both packages.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: fffeb0d7-6fcb-4485-8917-02315a708f72
Limit the public streaming namespace to Stream and AsyncStream. Keep decoder
protocols, concrete JSONL/SSE decoders, and ServerSentEvent as private
implementation details, and update samples, tests, and documentation links
to reflect the reduced API surface.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: fffeb0d7-6fcb-4485-8917-02315a708f72
The streaming deserialization_callback receives a JSONLEvent or
ServerSentEvent, so export both from the streaming package and add them to
__all__ (decoders stay internal). Update the decoder-method docstring
cross-references to point at the now-public streaming.<Type> paths.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 08d2aa98-be13-48ab-89ea-531b494b9482
Comment thread sdk/core/corehttp/corehttp/streaming/_stream.py Outdated
…rror

Wrap streaming iteration in try/finally so the response is deterministically
closed even when a caller iterates without a context manager and something
raises mid-stream (e.g. malformed JSONL). Async paths also aclose child async
generators via contextlib.aclosing to avoid RuntimeWarning on GC.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 08d2aa98-be13-48ab-89ea-531b494b9482
@l0lawrence
Libba Lawrence (l0lawrence) changed the base branch from main to feature/azure-core-streaming August 10, 2026 17:15
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 006b8a95-78b4-4e02-820e-debf125a5a6b
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
7 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

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

Adds mirrored JSONL and SSE streaming infrastructure to azure-core and corehttp.

Changes:

  • Adds synchronous/asynchronous stream abstractions and decoders.
  • Adds parser edge-case tests and test-server routes.
  • Adds samples, documentation, release notes, and an azure-core version bump.

Reviewed changes

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

Show a summary per file
File Description
sdk/core/corehttp/tests/testserver_tests/coretestserver/coretestserver/test_routes/streams.py Adds streaming test endpoints.
sdk/core/corehttp/tests/test_stream.py Adds synchronous streaming tests.
sdk/core/corehttp/tests/async_tests/test_stream_async.py Adds asynchronous streaming tests.
sdk/core/corehttp/samples/sample_stream.py Adds JSONL usage samples.
sdk/core/corehttp/README.md Documents streaming usage.
sdk/core/corehttp/corehttp/streaming/_stream.py Implements stream iterators.
sdk/core/corehttp/corehttp/streaming/_sse.py Implements SSE decoding.
sdk/core/corehttp/corehttp/streaming/_jsonl.py Implements JSONL decoding.
sdk/core/corehttp/corehttp/streaming/_decoders.py Defines decoder protocols.
sdk/core/corehttp/corehttp/streaming/__init__.py Defines public streaming exports.
sdk/core/corehttp/CHANGELOG.md Records the feature.
sdk/core/azure-core/tests/testserver_tests/coretestserver/coretestserver/test_routes/streams.py Adds streaming test endpoints.
sdk/core/azure-core/tests/test_stream.py Adds synchronous streaming tests.
sdk/core/azure-core/tests/async_tests/test_stream_async.py Adds asynchronous streaming tests.
sdk/core/azure-core/samples/sample_stream.py Adds JSONL and SSE samples.
sdk/core/azure-core/README.md Documents streaming usage.
sdk/core/azure-core/CHANGELOG.md Records the preview feature.
sdk/core/azure-core/azure/core/streaming/_stream.py Implements stream iterators.
sdk/core/azure-core/azure/core/streaming/_sse.py Implements SSE decoding.
sdk/core/azure-core/azure/core/streaming/_jsonl.py Implements JSONL decoding.
sdk/core/azure-core/azure/core/streaming/_decoders.py Defines decoder protocols.
sdk/core/azure-core/azure/core/streaming/__init__.py Defines public streaming exports.
sdk/core/azure-core/azure/core/_version.py Sets version 1.43.0b1.
Suppressed comments (4)

sdk/core/azure-core/samples/sample_stream.py:62

  • The async JSONL decoder also passes a JSONLEvent, so this annotation and return value are incorrect and the sample yields the wrapper instead of decoded JSON. Parse event.json() as in the streaming tests.
    def deserialize(response: AsyncHttpResponse, event: MutableMapping[str, Any]) -> MutableMapping[str, Any]:
        # Deserialize each decoded JSON object into a model. Here we just return it as-is.
        return event

sdk/core/corehttp/samples/sample_stream.py:59

  • The async JSONL decoder also passes a JSONLEvent, so this annotation and return value are incorrect and the sample yields the wrapper instead of decoded JSON. Parse event.json() as in the streaming tests.
    def deserialize(response: AsyncHttpResponse, event: MutableMapping[str, Any]) -> MutableMapping[str, Any]:
        # Deserialize each decoded JSON object into a model. Here we just return it as-is.
        return event

sdk/core/azure-core/azure/core/streaming/_jsonl.py:103

  • The trailing split component is still incomplete, so stripping \r from it before the next async chunk arrives can silently alter the JSON record. Strip CR only from completed components and preserve the final remainder until a following \n or EOF determines its role.
                decoded_lines = [line[:-1] if line.endswith("\r") else line for line in decoded.split("\n")]
                for line in decoded_lines[:-1]:
                    yield line
                decoded = decoded_lines[-1]

sdk/core/corehttp/corehttp/streaming/_jsonl.py:103

  • The trailing split component is still incomplete, so stripping \r from it before the next async chunk arrives can silently alter the JSON record. Strip CR only from completed components and preserve the final remainder until a following \n or EOF determines its role.
                decoded_lines = [line[:-1] if line.endswith("\r") else line for line in decoded.split("\n")]
                for line in decoded_lines[:-1]:
                    yield line
                decoded = decoded_lines[-1]

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +27 to +37
from ._stream import Stream, AsyncStream
from ._jsonl import JSONLEvent
from ._sse import ServerSentEvent


__all__ = [
"Stream",
"AsyncStream",
"JSONLEvent",
"ServerSentEvent",
]
Comment on lines +27 to +37
from ._stream import Stream, AsyncStream
from ._jsonl import JSONLEvent
from ._sse import ServerSentEvent


__all__ = [
"Stream",
"AsyncStream",
"JSONLEvent",
"ServerSentEvent",
]
Comment on lines +145 to +149
finally:
aclose = getattr(events, "aclose", None)
if aclose is not None:
await aclose()
await self._response.close()
Comment on lines +145 to +149
finally:
aclose = getattr(events, "aclose", None)
if aclose is not None:
await aclose()
await self._response.close()
Comment thread sdk/core/corehttp/README.md Outdated

### Streaming

`corehttp` provides a stream-agnostic `Stream` iterator for consuming streaming responses. Currently, JSON Lines (JSONL) streaming is supported via the `JSONLDecoder`. Pass the streamed response together with a decoder and a `deserialization_callback` that receives the response and each decoded event:
Comment on lines +27 to +35
from corehttp.streaming import Stream

client: PipelineClient[HttpRequest, HttpResponse] = PipelineClient("https://example.com")
request = HttpRequest("GET", "https://example.com/stream")
response = client.send_request(request, stream=True)

def deserialize(response: HttpResponse, event: MutableMapping[str, Any]) -> MutableMapping[str, Any]:
# Deserialize each decoded JSON object into a model. Here we just return it as-is.
return event
Comment thread sdk/core/azure-core/azure/core/streaming/_jsonl.py Outdated
Comment thread sdk/core/corehttp/corehttp/streaming/_jsonl.py Outdated
Comment on lines +32 to +37
__all__ = [
"Stream",
"AsyncStream",
"JSONLEvent",
"ServerSentEvent",
]
Comment on lines +32 to +37
__all__ = [
"Stream",
"AsyncStream",
"JSONLEvent",
"ServerSentEvent",
]
@github-actions

This comment has been minimized.

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

Copilot-Session: f257ae36-21ec-4fde-8c4e-d37d40c0e87d
…ders

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

Copilot-Session: f257ae36-21ec-4fde-8c4e-d37d40c0e87d
@github-actions

Copy link
Copy Markdown
Contributor
[Pilot] PR Pipeline Failure Analysis

A CI pipeline failed on this pull request. Here is an automated analysis of what went wrong and how to get the build green.

What failed

Three Azure DevOps builds failed:

  1. Build 6687399 — Infrastructure: ERR_PNPM_PNPM_ENGINE_IDENTITY_MISMATCH
    The pnpm engine registry signature could not be verified (pnpm@9.5.0 has no registry signatures). This is a toolchain/infra issue unrelated to the PR's code changes.

  2. Build 6687398 — Validation: CSpell spelling errors in azure-core
    The spell-checker failed on files introduced/modified by this PR:

    • sdk/core/azure-core/azure/core/streaming/_sse.py — unknown words: WHATWG (lines 191, 210)
    • tests/async_tests/test_stream_async.py — unknown words: πthis, messageπ, ufffd
    • tests/test_stream.py — unknown words: πthis, messageπ, ufffd
    • tests/testserver_tests/.../streams.py — unknown words: bthis, xbfdata

    This build also shows test failures in azure-ai-textanalytics (macOS, Ubuntu, Windows) and azure-identity IMDS tests — these may be pre-existing or environment-related.

  3. Build 6687400 — Same CSpell errors plus azure-identity IMDS and azure-ai-textanalytics test failures across platforms.

Recommended next steps

  • Fix CSpell errors (actionable): Add WHATWG to the cspell dictionary (.vscode/cspell.json), since it is a valid proper noun (WHATWG streaming spec). For test files using Unicode/binary literals (π, ufffd, bthis, xbfdata), add these to the cspell ignore list or wrap affected lines in # cSpell:disable comments. See the spell-check guide.
  • Re-run the pnpm build (Build 6687399) — the ERR_PNPM_PNPM_ENGINE_IDENTITY_MISMATCH failure is an infrastructure issue. Retrying the pipeline may resolve it.
  • Investigate azure-identity and azure-ai-textanalytics test failures — these fail across all platforms and Python versions; check whether they were already failing on main before this PR.
  • See the CI troubleshooting guide: https://aka.ms/ci-fix
  • Push new commits to address the CSpell failures; this comment updates automatically on the next failing run.
Raw pipeline analysis (azsdk ci analyze)
Build: 6687399 — ERR_PNPM_PNPM_ENGINE_IDENTITY_MISMATCH
Pipeline: https://dev.azure.com/azure-sdk/public/_build/results?buildId=6687399

Error: ERR_PNPM_PNPM_ENGINE_IDENTITY_MISMATCH
Refusing to run pnpm@9.5.0: registry signature could not be verified.

---

Build: 6687398 — CSpell errors + test failures
Pipeline: https://dev.azure.com/azure-sdk/public/_build/results?buildId=6687398

CSpell errors (10 issues in 4 files):
  azure/core/streaming/_sse.py:191:28 - Unknown word (WHATWG)
  azure/core/streaming/_sse.py:210:28 - Unknown word (WHATWG)
  tests/async_tests/test_stream_async.py:248 - Unknown words (πthis, messageπ)
  tests/async_tests/test_stream_async.py:365 - Unknown word (ufffd)
  tests/test_stream.py:248 - Unknown words (πthis, messageπ)
  tests/test_stream.py:349 - Unknown word (ufffd)
  tests/testserver_tests/.../streams.py:159 - Unknown word (bthis)
  tests/testserver_tests/.../streams.py:365 - Unknown word (xbfdata)

Failed tests:
  azure-identity: TestImds (system_assigned, user_assigned variants) on ubuntu2404
  azure-ai-textanalytics: TestTextAnalysisCase (14 test methods) on macOS/Ubuntu/Windows
  azure-cosmos: test_clear_cache_triggers_repopulation_async, test_cross_region_retry[502]

Copilot detected the failing pipeline and generated the analysis above. To have it attempt a fix automatically, reply with `@copilot please fix the failing pipeline on this PR`.

Generated by Pipeline Analysis - Next Steps · 35.5 AIC · ⌖ 6.37 AIC · ⊞ 6.6K ·

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants