Audit of the official RabbitMQ AMQP 1.0 Python client. Performed 2026-06-01 against the local checkout.
Repository: https://github.com/rabbitmq/rabbitmq-amqp-python-client
- Python 3.10+, built on
python-qpid-proton(C extension for AMQP 1.0) - Sync + async API (async wraps sync via
run_in_executor) - Poetry-based build, no unit tests (integration tests only, require live broker)
- Vendors a modified fork of the qpid/proton Python layer (24 files)
consumer.py:85 - _update_connection() creates a fully-attached receiver via self._receiver = self._create_receiver(addr), then immediately overwrites self._receiver at lines 89/98/109 with a new one. The first receiver's AMQP link is never closed, leaking a link on the broker and potentially stealing messages during the brief window it exists.
consumer.py:97 - self._consumer_options.offset(self._handler.offset - 1) calls a nonexistent method. StreamConsumerOptions only has _offset() (private). Stream consumers cannot reconnect after disconnection - this is a crash bug (AttributeError) in the recovery path.
environment.py:113 - Iterates self._connections directly while connection.close() (connection.py:359) removes the connection from the same list. This skips connections (every other one) or raises RuntimeError on some Python versions. Compare with connection.py:349 which correctly iterates a copy (self._publishers[:]).
address_helper.py:151 - binding_path_with_exchange_exchange uses dstq= (destination queue) instead of dste= (destination exchange). Exchange-to-exchange bindings cannot be removed via the returned path.
exceptions.py:1,10 - ValidationCodeException and ArgumentOutOfRangeException inherit from BaseException. They escape standard except Exception: handlers, propagating through cleanup code and surprising every user. Library exceptions must inherit Exception.
asyncio/connection.py:256-262 - The async path passes versions 4.0.0 through 4.2.0 but omits 4.3.0. QuorumConsumerOptions.validate() checks versions.get("4.3.0", False) which will always be False in the async path. SAC state handler validation incorrectly rejects on any server version when using the async API.
qpid/proton/_utils.py:670 - event.container.schedule(0, self._on_disconnection_handler()) - the parentheses call the handler immediately. Connection._on_disconnection calls time.sleep() in a loop. This blocks the reactor's process() call for potentially minutes. The schedule(0, None) after is a no-op. Then line 672 still raises ConnectionClosed, which propagates to the user even though reconnection may have succeeded.
connection.py:348-353 - Publishers and consumers are closed, but self._managements links are never closed. If the user calls connection.close() without first calling management.close(), the management sender/receiver links leak.
management.py:413-414 - _validate_reponse_code unconditionally raises on 409 before the loop that checks expected codes. Both declare_exchange (line 207) and declare_queue (line 252) include 409 in their expected codes (suggesting idempotent declaration was intended), but it always throws.
Connection, Publisher, Consumer, and Management have no locking. Environment docstring mentions "Connection is not thread-safe" (line 81) in a buried comment, but there are no protections. Sharing a connection across threads corrupts internal state. The async wrapper correctly uses asyncio.Lock, but the sync API is completely unprotected.
utils.py:25 - "".join(map(chr, body)) treats each byte as a Unicode code point. For UTF-8 encoded text with non-ASCII characters, this produces garbage. Should use body.decode('utf-8').
asyncio/publisher.py:78-79 - Comment says "We don't need the lock here because Publisher.init doesn't send any network traffic". This is false - Publisher.__init__ calls _open() which calls _create_sender() which performs AMQP ATTACH. The code is safe only because AsyncConnection.publisher() already holds the lock, but the comment is dangerously wrong for anyone refactoring.
Only Environment supports with syntax. Users must manually manage close/cleanup for every other resource, making leaks easy. The async API does better with __aenter__/__aexit__ on several classes.
connection.py:147 - Comment says "normally in case of oauth user should be '' but the internal library gives error". Hardcoding "no" as a username is a fragile undocumented workaround.
entities.py:551 - Mixed naming: active_recovery and back_off_reconnect_interval are snake_case, but MaxReconnectAttempts is PascalCase. Violates PEP 8.
management.py:410
The qpid/ directory contains 24 Python files that import cproton (the C extension from python-qpid-proton). The project declares python-qpid-proton = "^0.40.0" as a dependency AND vendors a modified copy of the Python layer. Modifications include _flow_properties.py (SAC support), _utils.py (disconnection handler), and _io.py.
This creates version-skew risk: if python-qpid-proton upgrades its C layer but the vendored Python is stale, things break silently. It's also a significant maintenance burden.
options.py:34 - Should be SenderOptionUnsettled.
- Default credit of 10 in consumer.py:80,183 with no named constant
- Reconnect max delay hardcoded to
timedelta(minutes=1)in connection.py:435 - Default timeout of 60 seconds in
_utils.py:445 - Should be configurable or at least named constants
All tests require a live RabbitMQ server - no unit tests (except test_server_validation.py which mocks). No tests for: concurrent access, error recovery paths (stream reconnection), exchange-to-exchange bindings, OAuth token refresh, edge cases in encode_path_segment, async consumer stop_processing path.
connection.py:362, publisher.py passim - Mixed use of None vs "" as "no value". Consumer uses Optional[str] properly while publisher uses str = "".
connection.py:102
exceptions.py - Custom exceptions store self.msg but don't pass it to BaseException.__init__(), so str(exc) and args attribute don't work as expected.
-
The vendored proton fork is the biggest long-term risk. Maintaining 24 modified files from an upstream C-extension library is fragile. Changes in proton's C layer can silently break the vendored Python.
-
The sync API's threading model is fundamentally limited. The qpid-proton reactor is single-threaded; the sync API blocks on it. The async API wraps sync calls in
run_in_executor, which is correct but means the async API's concurrency is limited to one operation at a time (serialized by the executor + lock). True async would require a native asyncio transport. -
Recovery is the weakest area. The reconnection logic blocks the reactor, leaks links, crashes for streams, and has an unclear contract about what state is preserved vs. lost.
-
No unit tests means regressions are caught late. Every test requires a running broker, so CI is slow and failures are hard to reproduce locally.