Skip to content

Fix SSE/JSONL stream_item_type lost through include_router - #15426

Closed
rzsultan-boop wants to merge 2 commits into
fastapi:masterfrom
rzsultan-boop:fix/issue-15401-sse-stream-item-type-include-router
Closed

rzsultan-boop wants to merge 2 commits into
fastapi:masterfrom
rzsultan-boop:fix/issue-15401-sse-stream-item-type-include-router

Conversation

@rzsultan-boop

Copy link
Copy Markdown

Fixes #15401.

Problem

APIRoute.__init__ only detects streaming return annotations inside the
isinstance(response_model, DefaultPlaceholder) branch. include_router
calls add_api_route with the already-resolved response_model (None
for streaming routes), so the detection is skipped on the merged route.
Both stream_item_type and stream_item_field stay None.

Consequences:

  • OpenAPI schema drops contentSchema / itemSchema for the route.
  • The request handler, which is built in __init__ via
    request_response(self.get_route_handler()), closes over
    stream_item_field=None, so runtime validation and serialization of
    stream items are silently disabled.

Fix

In APIRouter.include_router, right after add_api_route appends the
merged route, copy stream_item_type and stream_item_field from the
source route, then rebuild new_route.app so the handler closure picks
up the restored field. An isinstance(new_route, APIRoute) check
narrows Starlette's BaseRoute for mypy.

Both SSE (EventSourceResponse) and JSONL routes go through the same
path, so both are covered.

Tests

Four tests in tests/test_sse.py:

  • test_stream_item_type_preserved_through_include_router asserts both
    attributes are set after include_router.
  • test_sse_stream_item_field_set_on_direct_route baseline for
    directly-registered routes.
  • test_sse_openapi_schema_after_include_router verifies the item
    model appears in components.schemas.
  • test_sse_runtime_serialization_after_include_router end-to-end
    HTTP call via TestClient checking the serialized frame body.

Full suite passes locally. ruff check, ruff format --check, and
mypy fastapi/routing.py are clean.

@rzsultan-boop

Copy link
Copy Markdown
Author

Hi @tiangolo / @lbernick, could you please add the bug label to this PR? The check-labels workflow is failing because the auto-labeler only covers docs/.github/ and translation paths — it has no rule for fastapi/ source changes, so no label was applied automatically.

Thanks!

@YuriiMotov

Copy link
Copy Markdown
Member

I don't like this approach as the logic is not clear - stream_item_type is set to None in __init__ and then being overridden in include_router.

Please, review the alternative approach: #15077

@codspeed

codspeed Bot commented Apr 27, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 20 untouched benchmarks


Comparing rzsultan-boop:fix/issue-15401-sse-stream-item-type-include-router (947cf0c) with master (b445855)

Open in CodSpeed

When SSE/JSONL endpoints are defined on an APIRouter and included via
include_router(), the stream_item_type and stream_item_field attributes
were dropped because the detection in APIRoute.__init__ only runs when
response_model is a DefaultPlaceholder — but include_router passes the
already-resolved value.

After add_api_route, copy both stream_item_type and stream_item_field
from the original route, then re-bake self.app so the handler closure
captures the correct stream_item_field for runtime validation.

Without re-baking, both runtime validation and OpenAPI schema
generation remain broken because:
  - get_route_handler() closes over stream_item_field at __init__ time
  - openapi/utils.py reads stream_item_field, not stream_item_type
self.routes[-1] is typed as Starlette's BaseRoute, which lacks the
stream_item_type/stream_item_field/app attributes used by APIRoute.
Narrow the type with an isinstance check so mypy is satisfied and the
access is defensively guarded even though the preceding
isinstance(route, APIRoute) plus route_class_override=type(route) make
it true in practice.
@rzsultan-boop
rzsultan-boop force-pushed the fix/issue-15401-sse-stream-item-type-include-router branch from b47295e to 137f8f4 Compare April 27, 2026 15:50
@rzsultan-boop

Copy link
Copy Markdown
Author

Closing in favor of #15077, which threads stream_item_type through init/add_api_route/include_router parameters — the established pattern for this codebase (see strict_content_type). Thanks for the review.

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.

SSE stream_item_type not propagated through APIRouter + include_router

2 participants