Skip to content

OutboxRouter does not forward routers to the inner broker #158

Closed as not planned

Description

@lesnik512

What

OutboxRouter.__init__ (faststream_outbox/fastapi/router.py) mirrors most of
OutboxBroker.__init__'s kwargs and forwards them through StreamRouter's
**connection_kwars, but routers is not among them. OutboxBroker(..., routers=[...]) works;
OutboxRouter(..., routers=[...]) is a TypeError.

dlq_table and metrics_recorder were the other two gaps here and were forwarded in #88, which
left routers as the only unforwarded OutboxBroker argument.

Why it is still open

Unlike dlq_table / metrics_recorder, this is not mechanical. OutboxRouter is itself a
StreamRouter, so app.include_router(router) already contributes the router's own subscribers via
the FastAPI lifespan. A separate routers sequence would introduce a second, differently-timed
source of subscribers into the same broker, and its behaviour is unsettled on two axes:

  • Start / lifespan — when do the nested routers' subscribers start relative to the FastAPI
    lifespan, and what happens if the app never includes the outer router?
  • AsyncAPI — how the nested routers' channels and operations compose into the document the
    router serves at schema_url (default /asyncapi).

Forwarding the kwarg without settling those would ship whichever behaviour falls out of
StreamRouter's ordering, which is not obviously the one a user would expect.

Workaround

Register subscribers directly on the OutboxRouter — @router.subscriber(\"queue\") — rather than
composing sub-routers under it. Nothing is unreachable today.

Revisit trigger

A concrete "include a sub-router under the FastAPI outbox router" demand from someone hitting it,
rather than a hypothesised one. That report is also what would pin down the expected lifespan and
AsyncAPI semantics, which is the part currently missing.

Context

Carried over from planning/deferred.md, retired when the repo adopted the PR-body-as-spec
convention. Originally surfaced as F8-01 in the 2026-06-14 pass-3 audit, which confirmed the
dlq_table / metrics_recorder half was mechanically forwardable and flagged routers as the part
warranting a design call.

Activity

  1. lesnik512 commented on Sep 6, 2026

    @lesnik512
    MemberAuthor

    This was generated by AI during triage.

    Triage outcome: needs-info

    Relabeling needs-triage → needs-info. Staying enhancement: nothing is broken, a capability is absent, and the issue documents a workaround that leaves nothing unreachable.

    Reproduced, exactly as described:

    OutboxRouter has routers : False
    OutboxRouter has **kwargs: False
    OutboxBroker  has routers : True
    
    TypeError: OutboxRouter.__init__() got an unexpected keyword argument 'routers'. Did you mean 'routes'?
    

    OutboxBroker.__init__ takes routers: Sequence[Registrator[OutboxInnerMessage]] = () (broker.py:133) and forwards it (broker.py:190). OutboxRouter.__init__ (fastapi/router.py:68) enumerates every keyword explicitly with no **kwargs catch-all, and routers is not among them — so this is a hard TypeError, not a silently-ignored kwarg. dlq_table (:73) and metrics_recorder (:84) are both present and forwarded at :122-123, confirming routers as the last unforwarded OutboxBroker argument after #88.

    Worth noting from the reproduction: CPython suggests routes, which is a valid OutboxRouter parameter — the FastAPI APIRouter one, meaning something entirely different. A user following that suggestion gets no error and no subscribers. That raises the cost of the current gap slightly above "a TypeError tells you immediately".

    Why it stays open rather than being actioned: the design question in the body is real and unanswered. Forwarding the kwarg is one line; deciding when nested routers' subscribers start relative to the FastAPI lifespan, and how their channels compose into the /asyncapi document, is not. Shipping whatever falls out of StreamRouter's ordering would be a guess.

    What would move it:

    • A concrete "include a sub-router under the FastAPI outbox router" case from someone hitting it — the composition they wanted, and what they expected to happen at startup and in the AsyncAPI document.
    • Absent that, the two axes have no forcing function and any answer is speculative.
  2. added
    needs-infoWaiting on reporter for more information
    and removed
    needs-triageMaintainer needs to evaluate this issue
    on Sep 6, 2026
  3. lesnik512 commented on Oct 3, 2026

    @lesnik512
    MemberAuthor

    Closing as not planned: routers= should not be forwarded, and grouping subscribers under the FastAPI router already works.

    The supported path is router.include_router(...). StreamRouter.include_router accepts a broker-level router (faststream_outbox.OutboxRouter), wraps each of its subscribers with the FastAPI compatibility decorator, then includes it on the broker:

    orders = faststream_outbox.OutboxRouter()
    
    @orders.subscriber("orders")
    async def handle(body: dict, session: AsyncSession = Depends(get_session)) -> None: ...
    
    router = faststream_outbox.fastapi.OutboxRouter(engine, outbox_table=outbox_table)
    router.include_router(orders)

    That also answers the two open axes. The nested subscribers belong to the same broker, so they start with it in the FastAPI lifespan, and their channels appear in the /asyncapi document.

    Forwarding routers= would be harmful. It calls include_routers in the broker constructor, which skips the FastAPI wrap, and the FastAPI router runs with apply_types=False. A handler with d: str = Depends(dep) then receives the raw OutboxMessage as its body and the Depends(...) object as d, without raising. None of FastStream's own FastAPI routers (Kafka, Rabbit, Redis, NATS, Confluent) accepts routers either.

    #203 documents the pattern in docs/usage/fastapi.md and adds an invariant test for it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestneeds-infoWaiting on reporter for more information

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions