Skip to content

fix: adapt to faststream 0.7.6, and make the AsyncAPI document non-blank - #85

Merged
lesnik512 merged 2 commits into
mainfrom
fix/faststream-0-7-6
Sep 20, 2026
Merged

lesnik512 merged 2 commits into
mainfrom
fix/faststream-0-7-6

Conversation

@lesnik512

@lesnik512 lesnik512 commented Sep 20, 2026

Copy link
Copy Markdown
Member

FastStream 0.7.6 released today and broke this package five ways at once. uv.lock is gitignored, so CI resolves fresh on every run: lint and all six pytest jobs on #83 are red on this right now.

A sixth fix is folded in: the assembled AsyncAPI document was blank, and has been since long before 0.7.6.

The five 0.7.6 changes

1. try_it_out._get_broker_registry is gone. __init__.py monkeypatched it inside a bare try/except Exception: pass, so the failure was silent — the AsyncAPI "try it out" page just stopped knowing about TimersBroker. 0.7.6 replaces it with a declarative hook, so the 20-line block and its imports are deleted and the class statement carries broker=TimersBroker.

2. SubscriberSpec and PublisherSpec now require address. The channel key is topic:Handle / topic:Publisher, which names nothing subscribable; address is the field a reader can act on. Both get the prefixed topic, since a broker-prefixed deployment addressed by the bare topic points at a topic nothing reads.

3. SubscriberSpecification.channel_labels is abstract. TimersSubscriberSpecification could not be instantiated at all. The name override is replaced by channel_labels, and the base's _channel_key builds the key.

4. dependencies narrowed from Iterable[Dependant] to Sequence[Dependant] in broker.py, router.py and registrator.py.

5. TasksMixin.add_task no longer returns the task. The only change upstream was dropping the return task line — the supervisor and restart_on_failure behaviour are byte-identical — but subscriber/usecase.py did await consume_task on the result, so a timed-out start raised TypeError: 'NoneType' object can't be awaited instead of cancelling its poll task. Now reads self.tasks[-1], which stop() has not cleared yet at that point.

6. The AsyncAPI document was blank, and predates all of this

AsyncAPI(broker).to_specification() returned no servers, no channels and no operations for a TimersBroker. I reproduced it on main against faststream 0.7.5 before concluding anything, so it is not from the 0.7.6 work.

Upstream collects channels only for brokers that reach its broker_servers mapping, and it fills that mapping inside:

for url in specification.url:
    ...
    broker_servers.append((broker, server))

TimersBroker passed url=[], so it never entered the mapping and get_broker_channels was never called for it. Every per-endpoint get_schema() stayed correct throughout, which is exactly why specification-level tests could not see it — and why the fix in items 2 and 3 above would otherwise have shipped into a document nobody could read.

The URL is now derived from the client, and rebuilt from the pool's connection parameters rather than the caller's DSN. The document is published — FastStream serves it from the ASGI app — so echoing back a Redis.from_url("redis://user:secret@...") would publish the password. Username and password are never read, so there is nothing to mask.

Before / after, same broker:

servers:    {}                    →  {"development": {"host": "cache.example:6399", "pathname": "/3", ...}}
channels:   {}                    →  {"app:reminders:Handle": "app:reminders", "app:reminders:Publisher": "app:reminders"}
operations: []                    →  ["app:reminders:HandleSubscribe", "app:reminders:Publisher"]

built from Redis.from_url("redis://user:secret@cache.example:6399/3"), with "secret" in json.dumps(spec) returning False.

One behaviour change worth noticing

Handing the channel key to the base means title_ now names the channel, not just the operation. The old name override ignored title_ entirely. This matches the publisher and every built-in broker, and it changes the generated key for any subscriber declared with title_. Covered by test_subscriber_title_names_the_channel_not_only_the_operation.

Floor, and the cap from #84

#84 capped faststream<0.7.6 to unblock CI, with "lift once this adapts" on the line. It adapts now, so that cap is replaced by a floor of faststream>=0.7.6,<0.8. Items 1–3 use APIs that do not exist below it. The old >=0.7.1 did still import cleanly today — I checked before changing it — but it becomes a lie the moment this lands, and nothing in CI resolves at the lower bound to catch that.

Tests

Written first, red before each change:

FAILED tests/test_unit.py::test_timers_broker_registered_in_test_broker_registry
FAILED tests/test_unit.py::test_subscriber_specification_addresses_the_channel_by_its_full_topic
FAILED tests/test_unit.py::test_subscriber_title_names_the_channel_not_only_the_operation
FAILED tests/test_unit.py::test_publisher_specification_addresses_the_channel_by_its_full_topic
FAILED tests/test_unit.py::test_asyncapi_document_carries_the_declared_channels
FAILED tests/test_unit.py::test_specification_url_carries_no_credentials

The two pre-existing test_isolation.py failures cover item 5 and are green again.

Two notes on test quality, since both let real defects read as passing:

  • The replaced spec tests asserted only "my-topic" in name and assert schema, which is why a specification naming an unaddressable channel looked fine.
  • test_specification_url_shapes covers the unix-socket and clientless branches explicitly. The coverage gate was already satisfied for them by a MagicMock client — mock.connection_pool.connection_kwargs.get("path") is truthy, so it walked into the socket branch by accident. 100% line coverage was not evidence that branch worked.

Verification

just lint-ci clean (eof-fixer, ruff format, ruff check, ty). Full suite against Redis 8: 167 passed, 100% coverage.

Separately, not addressed here

Under TestTimersBroker, a publisher declared on a prefixed router gets a spurious extra fake subscriber: create_publisher_fake_subscriber matches on full_topic but falls back to broker.subscriber(publisher.config.topic), which re-registers the unprefixed topic. It shows up as a stray reminders:PublisherResponseSubscriber channel alongside the correct app:reminders:* ones. Test-broker path only, unrelated to 0.7.6, and out of scope here.

@lesnik512 lesnik512 changed the title fix: adapt to faststream 0.7.6 and raise its floor fix: adapt to faststream 0.7.6, and make the AsyncAPI document non-blank Sep 20, 2026
0.7.6 removed the try_it_out registry hook, made SubscriberSpec/PublisherSpec
carry an address, made channel_labels abstract, narrowed dependencies to
Sequence, and dropped add_task's return value. Registration is now declarative
and the channel key comes from the base, so title_ names the channel too.
Upstream collects channels only for brokers that reach its broker_servers
mapping, which it fills inside `for url in specification.url`. TimersBroker
passed url=[], so the whole document rendered blank while every per-endpoint
get_schema() stayed correct. The url is rebuilt from the pool's connection
parameters, never the caller's DSN, so no credential reaches the document.
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.

1 participant