Skip to content

fix(a2a): parse multi-line SSE events - #1155

Merged
jarvis9443 merged 3 commits into
api7:mainfrom
mat973252:zjz/fix-a2a-sse-frames
Sep 24, 2026
Merged

jarvis9443 merged 3 commits into
api7:mainfrom
mat973252:zjz/fix-a2a-sse-frames

Conversation

@mat973252

@mat973252 mat973252 commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #1143.

An A2A agent can split a JSON-RPC envelope across several data: fields in one SSE event. The relay previously parsed each line separately and terminated the stream on the first incomplete JSON fragment.

Buffer until the event's blank line, join its data fields with newlines, and parse the envelope once, following the SSE event-stream rules. This handles LF/CRLF framing and network chunk boundaries while ignoring comments and metadata. The existing 16 MiB limit now bounds the accumulated event. Existing compatibility for an unterminated final event is retained.

Malformed JSON is now reported when the complete event arrives, or at EOF, rather than after its first data line. Existing configurations need no changes.

Regression coverage includes multiline payloads, interspersed metadata, split CRLF/JSON writes, malformed JSON, and an oversized event made of individually small lines. The HTTP round-trip regression fails against the original reader with malformed JSON-RPC event and passes with this change.

Maintainer follow-ups on top: a bare \r now also ends a line (with a directly following \n swallowed, including across chunk boundaries), and a UTF-8 BOM at the very start of the stream is stripped, both per the same spec section. Before, a CR-framed agent's events never ended, and a leading BOM silently dropped the first envelope. A DP e2e case now has the stub agent write each envelope across several CRLF data: lines and asserts every event reaches the client as a single-line frame, which covers the gateway's re-serialisation layer.

Validation

Validated on Rust 1.93.1 in a Linux checkout, at commit 7f73bb63b569288a3284559dcbb28d741c911567:

  • cargo fmt --all --check passed.
  • cargo check --locked --workspace passed.
  • cargo clippy --locked --workspace -- -D warnings passed.
  • A2A: 34 unit tests and 23 HTTP integration tests passed.
  • cargo test --locked --workspace --no-fail-fast has one failing test: aisix-mcp::bridge::tests::connect_timeout_bounds_an_unreachable_upstream.

The same full-workspace test command, in the same environment, on unmodified upstream main (c8e96aa) fails only that same MCP test. It exceeds the test's eight-second bound while reaching the unreachable test address. No tests were skipped or weakened to hide this failure. Local proxy/network behavior affects this test, so this draft does not claim a green full-workspace test run; upstream CI still needs to validate it.

The separate repository E2E suite has not been run locally. This change is limited to the A2A reader and its tests.

Summary by CodeRabbit

  • Bug Fixes
    • Improved streaming event handling when responses are split across multiple data fields or network chunks.
    • Correctly reassembles multiline event payloads, including streams using CRLF or bare carriage-return line endings and those containing keep-alive messages.
    • Handles streams with a leading UTF-8 byte-order mark.
    • Rejects oversized events with a clear stream error, including when many smaller lines combine into an event that exceeds the size limit.

@CLAassistant

CLAassistant commented Sep 8, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@coderabbitai

coderabbitai Bot commented Sep 8, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: d74e781d-28a9-422a-bbbc-fa1ea3ce8410

📥 Commits

Reviewing files that changed from the base of the PR and between 7625bec and 13047f8.

📒 Files selected for processing (4)
  • crates/aisix-a2a/src/bridge.rs
  • crates/aisix-a2a/tests/upstream_roundtrip.rs
  • tests/e2e/src/cases/a2a-gateway-e2e.test.ts
  • tests/e2e/src/harness/upstream-a2a.ts

Included review availability: Your plan provides up to 2 included reviews per hour; 1 remains after this review.


📝 Walkthrough

Walkthrough

The SSE relay now buffers complete events, joins multiple data: fields with newlines, parses the combined JSON payload, and enforces the size limit across each event. Tests cover multiline frames, line-ending variants, chunk boundaries, a leading BOM, and oversized events.

Changes

SSE frame reassembly

Layer / File(s) Summary
Frame buffering and parsing
crates/aisix-a2a/src/bridge.rs
The reader buffers data until an SSE frame ends, enforces MAX_SSE_EVENT_BYTES across the complete frame, joins all data: fields, and parses the combined payload. It also handles a leading UTF-8 BOM and trailing data without a final newline. Unit tests cover newline variants and malformed JSON.
Streaming integration and size validation
crates/aisix-a2a/tests/upstream_roundtrip.rs
The streaming fixture emits multiline CRLF data across writes, including a split CRLF and comment line. Tests cover a leading BOM, bare CR line endings, and oversized multiline events.
Gateway multiline stream coverage
tests/e2e/src/harness/upstream-a2a.ts, tests/e2e/src/cases/a2a-gateway-e2e.test.ts
The upstream harness can emit JSON-RPC payloads across multiple CRLF data: fields. The gateway test verifies that each output frame has one data: line and that all three events retain their expected sequence, ID, and completion state.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix · Severity of issue fixed: Medium

Sequence Diagram(s)

sequenceDiagram
  participant UpstreamAgent
  participant A2aBridge
  participant GatewayClient
  UpstreamAgent->>A2aBridge: Send SSE event with multiple data fields
  A2aBridge->>A2aBridge: Join data fields and parse JSON payload
  A2aBridge->>GatewayClient: Emit reframed event
Loading

Suggested reviewers: jarvis9443, moonming

Merge Risk: ⚪ Minimal · up to 13047

No concrete issue remains that should block merging after the normal checks.

🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Issue #1143 requires event-based SSE parsing. crates/aisix-a2a/src/bridge.rs now buffers through the blank event boundary, joins all data: fields with newline characters, ignores metadata and comm…
Out of Scope Changes check ✅ Passed The changed Rust tests, the upstream multiline SSE fixture, and the A2A gateway end-to-end test directly validate issue #1143. The multiLineData harness option only supports that end-to-end scenario…
E2e Test Quality Review ✅ Passed The PR adds a real gateway E2E path: it starts an SSE upstream, seeds the agent, starts the gateway, sends message/stream, and verifies all three events after relay re-framing. The test uses clear n…
Security Check ✅ Passed PASS. The authoritative diff changes only A2A SSE event buffering, parsing, and test fixtures. No explicit security-check failure is introduced. - Category 1: No issues found. The new parser does not …
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: parsing multi-line SSE events in A2A.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR

Comment @coderabbitai help to get the list of available commands.

@mat973252
mat973252 marked this pull request as ready for review September 8, 2026 23:53
@mat973252
mat973252 force-pushed the zjz/fix-a2a-sse-frames branch from 7f73bb6 to a79362b Compare September 15, 2026 14:39
@mat973252
mat973252 force-pushed the zjz/fix-a2a-sse-frames branch from a79362b to 7625bec Compare September 22, 2026 01:09
@mat973252

Copy link
Copy Markdown
Contributor Author

Hi @jarvis9443, a gentle follow-up on this fix when you have a chance.

I've rebased the branch onto the latest main at 4875c2d; the PR head is now 7625bec. The rebase was conflict-free, and git range-diff confirms the original fix is unchanged.

Validation on the rebased revision (Rust 1.93.1, Linux/WSL):

  • cargo test -p aisix-a2a --locked: all 57 tests pass (34 unit + 23 HTTP integration tests).
  • cargo fmt --all -- --check: pass.
  • cargo clippy -p aisix-a2a --all-targets --locked -- -D warnings: pass.

The regression coverage includes multiline SSE data, chunk boundaries, split CRLF, malformed trailing data, and the event-size limit. These are local checks, not a claim that the full project CI has passed.

The new CI run and the other fork workflows are still awaiting approval. Could a maintainer approve them and help review this when convenient? If there's anything you'd like changed in the approach or additional coverage you'd find useful, I'm happy to follow up. Thanks for your time!

The event-stream grammar lets a bare `\r` end a line and lets the stream
open with a UTF-8 BOM. The frame reader ended lines only on `\n`, so a
CR-framed agent's events never ended, and a leading BOM made the first
field name unrecognisable, silently dropping the first envelope.

A `\r` now ends a line and a directly following `\n` is swallowed,
tracked across chunk boundaries; a BOM is stripped at stream start only.

Adds a DP e2e case where the agent writes each envelope across several
CRLF `data:` lines, asserting every event reaches the client as a
single-line frame.
@jarvis9443
jarvis9443 merged commit 313b3cd into api7:main Sep 24, 2026
17 checks passed
@jarvis9443

Copy link
Copy Markdown
Contributor

Thanks @mat973252-coder, this is a clean fix and the regression coverage made it easy to land. We added a few small follow-ups on top before merging: bare-CR line endings, stripping a leading BOM, and an e2e case that covers the gateway's re-framing of multi-line events. Merged now, thanks again!

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.

aisix-a2a: an SSE frame whose JSON-RPC envelope spans several data: lines fails the stream

3 participants