Skip to content

fix: support bytes in StreamingResponse generators#1308

Open
sansyrox wants to merge 5 commits intomainfrom
fix/sse-downloads
Open

fix: support bytes in StreamingResponse generators#1308
sansyrox wants to merge 5 commits intomainfrom
fix/sse-downloads

Conversation

@sansyrox
Copy link
Member

@sansyrox sansyrox commented Feb 14, 2026

Description

This PR fixes #1236

Summary

This PR

PR Checklist

Please ensure that:

  • The PR contains a descriptive title
  • The PR contains a descriptive summary of the changes
  • You build and test your changes before submitting a PR.
  • You have added relevant documentation
  • You have added relevant tests. We prefer integration tests wherever possible

Pre-Commit Instructions:

Summary by CodeRabbit

  • New Features

    • Add support for streaming binary and mixed byte/text content from generators, including direct file-download streaming.
    • Streaming responses now accept both bytes and strings as chunk types.
  • Bug Fixes

    • SSE-related headers are only added for SSE content types, preventing unexpected headers on binary/text streams.
  • Tests

    • New integration tests validating binary, file and text streaming behavior, chunk integrity, and header handling.

@vercel
Copy link

vercel bot commented Feb 14, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
robyn Error Error Feb 15, 2026 5:18pm

@coderabbitai
Copy link

coderabbitai bot commented Feb 14, 2026

📝 Walkthrough

Walkthrough

Adds byte-capable streaming across Python and Rust layers: StreamingResponse/SSEResponse now accept generators yielding bytes or str; StreamingResponse carries media_type to conditionally emit SSE headers; three new streaming endpoints and integration tests for binary, file, and text streaming were added.

Changes

Cohort / File(s) Summary
Python response types
robyn/responses.py
Accept sync/async generators that yield str or bytes in AsyncGeneratorWrapper, StreamingResponse, and SSEResponse signatures.
Integration test routes
integration_tests/base_routes.py
Import StreamingResponse and add endpoints: /stream/bytes, /stream/bytes_file, /stream/mixed_text returning StreamingResponse with appropriate media types and headers.
Integration tests
integration_tests/test_binary_streaming.py
New tests validating binary chunk streaming, file streaming (Content-Disposition + content equality), absence of SSE headers for non-SSE responses, and text streaming.
Rust response handling
src/types/response.rs
Add media_type: String to StreamingResponse; constructor/FromPyObject updated to accept media_type; generator yields of bytes streamed as-is, str converted to bytes; SSE headers set only when media_type == "text/event-stream".

Sequence Diagram

sequenceDiagram
    participant Client
    participant Router as Robyn Router
    participant Handler as Rust Streaming Handler
    participant PyGen as Python Generator

    Client->>Router: GET /stream/bytes
    Router->>Handler: invoke endpoint -> StreamingResponse(media_type, generator)
    Handler->>Client: send response headers (status, Content-Type)
    Handler->>PyGen: start generator (sync/async)
    loop per yield
        PyGen->>Handler: yield (bytes or str)
        Handler->>Handler: if str -> encode to bytes
        Handler->>Client: stream bytes chunk
    end
    PyGen->>Handler: StopIteration / end
    Handler->>Client: close stream
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I nibble bytes from branch to burrow,
Yielding chunks both small and thorough,
Files and text hop down my trail,
No more casts that made me wail,
Stream on—my fluffy tail's a sail!

🚥 Pre-merge checks | ✅ 3 | ❌ 3
❌ Failed checks (3 warnings)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is incomplete with only a placeholder issue reference (#1236) and minimal summary ('This PR'). Required sections like detailed summary, documentation, and testing status are unfilled or unchecked. Complete the summary section explaining the bytes-streaming implementation, mark relevant checklist items as completed, and provide evidence of testing and documentation updates.
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (9 files):

⚔️ .github/workflows/release-CI.yml (content)
⚔️ Cargo.lock (content)
⚔️ Cargo.toml (content)
⚔️ docs_src/public/llms.txt (content)
⚔️ integration_tests/base_routes.py (content)
⚔️ llms.txt (content)
⚔️ pyproject.toml (content)
⚔️ robyn/responses.py (content)
⚔️ src/types/response.rs (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix: support bytes in StreamingResponse generators' directly addresses the main change: enabling StreamingResponse to handle bytes from generators, matching the PR's primary objective from issue #1236.
Linked Issues check ✅ Passed Code changes comprehensively address issue #1236 requirements: StreamingResponse now accepts Union[str, bytes] in Python/Rust, generators handle bytes without string conversion, and appropriate media_type/headers support is implemented.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the linked issue #1236: response types updated for bytes support, test endpoints and comprehensive integration tests added, with no unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/sse-downloads
⚔️ Resolve merge conflicts (beta)
  • Auto-commit resolved conflicts to branch fix/sse-downloads
  • Create stacked PR with resolved conflicts
  • Post resolved changes as copyable diffs in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@sansyrox sansyrox changed the title fix: sse downloads fix: support bytes in StreamingResponse generators Feb 14, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/types/response.rs (1)

562-577: ⚠️ Potential issue | 🟡 Minor

Fallback to "text/event-stream" on extraction failure could mask bugs.

If media_type extraction fails (lines 570, 575), the code silently falls back to "text/event-stream", which would cause SSE headers to be injected for what might be a binary streaming response. Consider returning an error instead, since a missing or non-extractable media_type on an object that passed the has_media_type check (line 510) would indicate a real problem.

🤖 Fix all issues with AI agents
In `@src/types/response.rs`:
- Around line 115-123: The SSE-specific headers are being added twice: once
during extraction in FromPyObject (where Headers may get "Cache-Control" and
"Connection") and again unconditionally in respond_to after
apply_hashmap_headers; update respond_to (the method that calls
apply_hashmap_headers and then appends SSE headers) to only append each SSE
header if it is not already present on self.headers (or use whatever lookup
method the Headers type provides), so avoid duplicate "Connection",
"Cache-Control", "X-Accel-Buffering", "Pragma", and "Expires" values; reference
respond_to, apply_hashmap_headers, FromPyObject, and the Headers struct to
locate and implement the conditional checks.
🧹 Nitpick comments (2)
integration_tests/base_routes.py (1)

1359-1372: Redundant Content-Type header alongside media_type.

The Content-Type is specified both in headers and implicitly via media_type. Currently this works because media_type only auto-sets headers for "text/event-stream", but if that logic changes, these could conflict. Consider setting only media_type and letting the framework derive the Content-Type header, or document why both are needed.

This same pattern repeats in the /stream/bytes_file (line 1391-1394) and /stream/mixed_text (line 1411) endpoints.

robyn/responses.py (1)

124-151: Consider auto-setting Content-Type from media_type for non-SSE streaming responses.

Currently, StreamingResponse.__init__ only sets Content-Type when media_type == "text/event-stream" (line 149). For other media types (e.g., "application/octet-stream"), users must manually pass the Content-Type header themselves. This creates a subtle API pitfall where media_type controls SSE header injection but does not set the response's Content-Type for non-SSE types.

Suggested improvement
         # Set default SSE headers
         if media_type == "text/event-stream":
             self.headers.set("Content-Type", "text/event-stream")
             # Cache-Control and Connection headers are set by Rust layer with optimized headers
+        else:
+            # Set Content-Type from media_type if not already provided
+            if not self.headers.contains("Content-Type"):
+                self.headers.set("Content-Type", media_type)

@codspeed-hq
Copy link

codspeed-hq bot commented Feb 14, 2026

Merging this PR will not alter performance

✅ 169 untouched benchmarks
🆕 4 new benchmarks

Performance Changes

Benchmark BASE HEAD Efficiency
🆕 test_stream_bytes_file N/A 2.7 ms N/A
🆕 test_stream_bytes_no_sse_headers N/A 2.5 ms N/A
🆕 test_stream_bytes_basic N/A 2.8 ms N/A
🆕 test_stream_text_still_works N/A 2.6 ms N/A

Comparing fix/sse-downloads (3e0435a) with main (0472e42)

Open in CodSpeed

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/types/response.rs (1)

564-580: ⚠️ Potential issue | 🟡 Minor

Risky default: falling back to "text/event-stream" on extraction error.

If media_type is present but has an unexpected type (passes the hasattr check on line 513 but fails extract::<String>), this silently defaults to "text/event-stream". That would inject SSE headers into a binary streaming response — contrary to the user's intent.

Consider returning the extraction error instead of silently defaulting:

Proposed fix
         let media_type: String = match obj.getattr("media_type") {
             Ok(attr) => match attr.extract() {
                 Ok(media_type) => {
                     debug!("Successfully extracted media_type: {}", media_type);
                     media_type
                 }
                 Err(e) => {
                     debug!("Failed to extract media_type: {}", e);
-                    "text/event-stream".to_string()
+                    return Err(e);
                 }
             },
             Err(e) => {
                 debug!("Failed to get media_type attribute: {}", e);
-                "text/event-stream".to_string()
+                return Err(e);
             }
         };

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.

About using StreamingResponse

1 participant