Skip to content

fix: support bytes in StreamingResponse generators#1308

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

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

Conversation

@sansyrox
Copy link
Copy Markdown
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

    • Added streaming endpoints for binary chunks, file download streaming, and mixed text; streaming accepts both bytes and strings and honors declared media types.
  • Bug Fixes

    • SSE-specific headers are applied only for SSE media type and are not duplicated on other streams.
  • Tests

    • New integration tests cover binary streaming, file-stream integrity, text streaming, and header presence/absence.

@vercel
Copy link
Copy Markdown

vercel bot commented Feb 14, 2026

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

Project Deployment Actions Updated (UTC)
robyn Ready Ready Preview, Comment Mar 28, 2026 7:32pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 14, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds byte-capable streaming end-to-end: Python StreamingResponse/SSEResponse now accept generators yielding bytes or str; Rust StreamingResponse gains a media_type field and handles bytes vs strings; three new streaming endpoints and integration tests exercise binary, file, and text streams.

Changes

Cohort / File(s) Summary
Python response types
robyn/responses.py
Expand AsyncGeneratorWrapper/StreamingResponse/SSEResponse signatures to accept generators yielding str or bytes.
Integration test routes
integration_tests/base_routes.py
Import StreamingResponse and add GET endpoints: /stream/bytes, /stream/bytes_file, /stream/mixed_text that stream binary, file, and text chunks with appropriate headers.
Integration tests
integration_tests/test_binary_streaming.py
New tests validating chunked binary streaming, file-download streaming (Content-Disposition + byte-for-byte 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; require media_type in constructor/from-Python conversion; handle generator yields as PyBytes or String (bytes streamed as-is, strings encoded); apply SSE headers only when media_type == "text/event-stream".

Sequence Diagram

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

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

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 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 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is mostly incomplete. It references issue #1236 but the summary section is a placeholder ('This PR...') and the checklist items are unchecked, with no actual details of the changes provided. Expand the summary to describe what was changed (added support for bytes in StreamingResponse generators, updated Rust and Python code, added tests), and check relevant checklist items to confirm testing and documentation were completed.
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.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: adding support for bytes in StreamingResponse generators, which directly addresses the core issue.
Linked Issues check ✅ Passed The implementation fully addresses the linked issue #1236: it enables StreamingResponse generators to yield bytes directly, prevents type conversion errors, eliminates str/bytes overhead, and maintains header support.
Out of Scope Changes check ✅ Passed All changes are within scope: the test file adds benchmarks for the new streaming functionality, and the Rust/Python changes specifically implement bytes support in StreamingResponse as required.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/sse-downloads

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
Copy Markdown

@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
Copy Markdown

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
Copy Markdown

@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);
             }
         };

Copy link
Copy Markdown

@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

🧹 Nitpick comments (1)
src/types/response.rs (1)

151-159: Consider logging a warning for unsupported yield types.

When the generator yields a value that is neither PyBytes nor String, the stream terminates silently by returning None. This could make debugging difficult for users who accidentally yield an unsupported type.

💡 Proposed enhancement
                     Ok(value) => {
                         if let Ok(py_bytes) = value.downcast::<PyBytes>() {
                             Some((py_bytes.as_bytes().to_vec(), generator))
                         } else if let Ok(s) = value.extract::<String>() {
                             Some((s.into_bytes(), generator))
                         } else {
+                            log::warn!(
+                                "Generator yielded unsupported type: {}. Expected bytes or str.",
+                                value.get_type().name().unwrap_or("unknown")
+                            );
                             None
                         }
                     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/types/response.rs` around lines 151 - 159, In the Ok(value) arm in
src/types/response.rs (the branch that checks for PyBytes and String), add a
warning log before returning None when the yielded value is neither PyBytes nor
String; include the Python object's type name and a short repr/str in the
warning so users can see what unsupported type was yielded (use the existing
logging facility in the project, e.g. tracing::warn! or log::warn!, and gather
type/repr from the PyAny/Python API) and then return None as before; this change
should be made around the existing symbols value, PyBytes, String, and
generator.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/types/response.rs`:
- Around line 166-170: There are extra closing braces and an orphan `None` in
the block around the function in src/types/response.rs (look for the function or
impl that returns an Option/Result near the end of the method containing the
stray `None`); remove the superfluous `}` characters and either remove or
correctly place the `None` return so the function's control flow and indentation
match the intended match/if expression (ensuring the function's final return
matches its signature), then reformat to restore correct brace alignment for the
surrounding function/impl.

---

Nitpick comments:
In `@src/types/response.rs`:
- Around line 151-159: In the Ok(value) arm in src/types/response.rs (the branch
that checks for PyBytes and String), add a warning log before returning None
when the yielded value is neither PyBytes nor String; include the Python
object's type name and a short repr/str in the warning so users can see what
unsupported type was yielded (use the existing logging facility in the project,
e.g. tracing::warn! or log::warn!, and gather type/repr from the PyAny/Python
API) and then return None as before; this change should be made around the
existing symbols value, PyBytes, String, and generator.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 99db1d81-ba76-453c-8900-622371472c94

📥 Commits

Reviewing files that changed from the base of the PR and between 3e0435a and d2ec793.

📒 Files selected for processing (4)
  • integration_tests/base_routes.py
  • integration_tests/test_binary_streaming.py
  • robyn/responses.py
  • src/types/response.rs
✅ Files skipped from review due to trivial changes (1)
  • integration_tests/test_binary_streaming.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • integration_tests/base_routes.py

Comment on lines +166 to 170
}
}
None
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Malformed code structure will cause compilation failure.

Lines 166-170 contain extra closing braces and an orphan None statement with incorrect indentation. This appears to be leftover from a merge or edit error and will prevent the Rust code from compiling.

🐛 Proposed fix
                     }
                     Err(e) => {
                         if !e.is_instance_of::<pyo3::exceptions::PyStopIteration>(py) {
                             log::error!("Generator error: {}", e);
                         }
                         None
                     }
                 }
-                        }
-                        None
-                    }
             })
         })
         .await
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
}
}
None
}
}
}
})
})
.await
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/types/response.rs` around lines 166 - 170, There are extra closing braces
and an orphan `None` in the block around the function in src/types/response.rs
(look for the function or impl that returns an Option/Result near the end of the
method containing the stray `None`); remove the superfluous `}` characters and
either remove or correctly place the `None` return so the function's control
flow and indentation match the intended match/if expression (ensuring the
function's final return matches its signature), then reformat to restore correct
brace alignment for the surrounding function/impl.

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