Skip to content

fix(router): flush SSE/multipart response head before first message - #3052

Merged
endigma merged 5 commits into
mainfrom
issue-1981
Jul 7, 2026
Merged

fix(router): flush SSE/multipart response head before first message#3052
endigma merged 5 commits into
mainfrom
issue-1981

Conversation

@endigma

@endigma endigma commented Jul 6, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • Bug Fixes
    • Improved streaming responses so the connection starts immediately when heartbeat mode is enabled, instead of waiting for the first message.
    • Ensured streaming headers are sent right away for both supported streaming modes.
  • Tests
    • Added coverage to verify the response is flushed and the correct streaming content type is set before any payload is written.
  • SSE and multipart subscription responses set their Content-Type headers but never flushed them
  • So we buffered the response head until the first subgraph message
  • Some clients blocked establishing the connection and could time out when no message was streamed within their idle window
  • This flushes the response head as soon as the subscription is established
  • Adds a test asserting the head is flushed before any message is written.

Fixes #1981.

Checklist

Open Source AI Manifesto

This project follows the principles of the Open Source AI Manifesto. Please ensure your contribution aligns with its principles.

Subscription responses set the text/event-stream (or multipart) headers but
never flushed them, so Go buffered the response head until the first subgraph
message. Clients blocked establishing the connection and could time out when
no message was streamed within their idle window. Flush the head as soon as
the subscription is established.
@endigma
endigma requested a review from a team as a code owner July 6, 2026 15:14
@github-actions github-actions Bot added the router label Jul 6, 2026
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The change adds an immediate flusher.Flush() call in GetSubscriptionResponseWriter when multipart or SSE mode is enabled and heartbeats are configured, flushing response headers before any message is streamed. A corresponding test validates the flush occurs before writing.

Changes

Immediate Flush for Streaming Subscriptions

Layer / File(s) Summary
Flush response head immediately for SSE/multipart
router/core/subscription_response_writer.go
Calls flusher.Flush() inside the UseMultipart || UseSse branch of GetSubscriptionResponseWriter to flush headers before the first streamed message, preventing client timeouts while waiting for the first message.
Test coverage for immediate flush
router/core/subscription_response_writer_test.go
Adds TestGetSubscriptionResponseWriter, which uses httptest to verify the SSE Content-Type header and confirm the response head is flushed before any message is written; updates imports to support the new test.

Estimated code review effort: 1 (Trivial) | ~5 minutes

Related issues: #1981 — Fixes the router blocking SSE subscription responses until a message is received, causing timeouts, by flushing the response head immediately.

Suggested reviewers: none identified

🐰 A flush, a hop, a stream held tight,
No more waiting through the timeout night,
Headers sent before the first word's told,
SSE connections now flow bold. 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The change addresses the reported SSE blocking by flushing the response head immediately, and the test covers the expected behavior.
Out of Scope Changes check ✅ Passed No clear unrelated code changes are introduced beyond the subscription response flush and its test.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and accurately describes the main change: flushing SSE and multipart response heads before the first message.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

Router image scan passed

✅ No security vulnerabilities found in image:

ghcr.io/wundergraph/cosmo/router:sha-d31066de6c12778ba00dfea77335ca4202a7d94f

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (1)
router/core/subscription_response_writer_test.go (1)

130-146: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a multipart counterpart test.

The fix flushes for both UseSse and UseMultipart, but the test only covers SSE. Add a symmetric subtest for multipart to guard against regressions in that path too.

♻️ Suggested additional subtest
 	t.Run("flushes the SSE response head before any message is written", func(t *testing.T) {
 		recorder := httptest.NewRecorder()
 		req := httptest.NewRequest(http.MethodPost, "/graphql", nil)
 		req.Header.Set("Accept", sseMimeType)
 
 		_, _, ok := GetSubscriptionResponseWriter(resolve.NewContext(context.Background()), req, recorder, false)
 		require.True(t, ok)
 
 		assert.Equal(t, sseMimeType, recorder.Header().Get("Content-Type"))
 		assert.True(t, recorder.Flushed, "expected the SSE response head to be flushed before any message is written")
 	})
+
+	t.Run("flushes the multipart response head before any message is written", func(t *testing.T) {
+		recorder := httptest.NewRecorder()
+		req := httptest.NewRequest(http.MethodPost, "/graphql", nil)
+		req.Header.Set("Accept", multipartMimeType)
+
+		_, _, ok := GetSubscriptionResponseWriter(resolve.NewContext(context.Background()), req, recorder, false)
+		require.True(t, ok)
+
+		assert.True(t, recorder.Flushed, "expected the multipart response head to be flushed before any message is written")
+	})

(Adjust multipartMimeType/negotiation to match how multipart mode is actually detected in NegotiateSubscriptionParams.)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@router/core/subscription_response_writer_test.go` around lines 130 - 146, The
current test in TestGetSubscriptionResponseWriter only verifies the SSE flush
path, but the response writer also flushes for multipart subscriptions. Add a
symmetric subtest alongside the existing SSE case that exercises the multipart
negotiation path used by GetSubscriptionResponseWriter and asserts the
Content-Type is multipart and the recorder is flushed before any message is
written; use the same test helpers and negotiation behavior already used by
NegotiateSubscriptionParams so both UseSse and UseMultipart are covered.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@router/core/subscription_response_writer_test.go`:
- Around line 130-146: The current test in TestGetSubscriptionResponseWriter
only verifies the SSE flush path, but the response writer also flushes for
multipart subscriptions. Add a symmetric subtest alongside the existing SSE case
that exercises the multipart negotiation path used by
GetSubscriptionResponseWriter and asserts the Content-Type is multipart and the
recorder is flushed before any message is written; use the same test helpers and
negotiation behavior already used by NegotiateSubscriptionParams so both UseSse
and UseMultipart are covered.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f5309390-787d-4cfd-845c-539b00a701a1

📥 Commits

Reviewing files that changed from the base of the PR and between f704e34 and ac6e31a.

📒 Files selected for processing (2)
  • router/core/subscription_response_writer.go
  • router/core/subscription_response_writer_test.go

@codecov

codecov Bot commented Jul 6, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 61.48%. Comparing base (d161722) to head (9394da1).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3052      +/-   ##
==========================================
+ Coverage   61.28%   61.48%   +0.20%     
==========================================
  Files         261      261              
  Lines       30604    30605       +1     
==========================================
+ Hits        18757    18819      +62     
+ Misses      10324    10254      -70     
- Partials     1523     1532       +9     
Files with missing lines Coverage Δ
router/core/subscription_response_writer.go 80.89% <100.00%> (+0.12%) ⬆️

... and 22 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@endigma
endigma merged commit 10e9731 into main Jul 7, 2026
37 checks passed
@endigma
endigma deleted the issue-1981 branch July 7, 2026 12:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cosmo router is blocking the response when subscribing via SSE, until at least one message is received which causes a timeout

3 participants