From 77713f4c2de32f36230fbdd20cdd5af92973ba15 Mon Sep 17 00:00:00 2001 From: Tobias Oberstein Date: Sun, 18 Oct 2026 08:53:05 +0200 Subject: [PATCH 1/4] start new dev branch; add audit file --- .audit/oberstet_fix_1988.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .audit/oberstet_fix_1988.md diff --git a/.audit/oberstet_fix_1988.md b/.audit/oberstet_fix_1988.md new file mode 100644 index 000000000..41352660f --- /dev/null +++ b/.audit/oberstet_fix_1988.md @@ -0,0 +1,8 @@ +- [ ] I did **not** use any AI-assistance tools to help create this pull request. +- [x] I **did** use AI-assistance tools to *help* create this pull request. +- [x] I have read, understood and followed the project's AI_POLICY.md when creating code, documentation etc. for this pull request. + +Submitted by: @oberstet +Date: 2026-10-18 +Related issue(s): #1988 +Branch: oberstet:fix_1988 From 78b4d556ace9eaea064fc8e2980327b8c08b1bd4 Mon Sep 17 00:00:00 2001 From: Tobias Oberstein Date: Sun, 18 Oct 2026 08:54:55 +0200 Subject: [PATCH 2/4] #1988: red - payload codec error in call() on asyncio Add src/autobahn/wamp/test/test_wamp_payload_codec_error.py, which runs on both backends (trial with USE_TWISTED, pytest with USE_ASYNCIO): an ApplicationSession with a payload codec whose encode() raises must re-raise the codec's exception from call(), and send nothing. session.log is not mocked on purpose - a Mock has every method, so it would hide a logger method that one backend lacks. On Twisted the error is logged as a Failure by log.failure(), which trial reports as a test error; the test flushes it (trial TestCase only), since Twisted is not affected by #1988. Defect shown: on asyncio, call() raises AttributeError ("'_TxaioLogWrapper' object has no attribute 'failure'") from wamp/protocol.py:1950 instead of the codec's exception. Local (asgard1): cpy314 + pypy311 - trial PASSED, pytest FAILED with that AttributeError. Expected CI result: RED - the asyncio/pytest runs (unit-tests on all five Pythons, quality-checks) fail in test_call_reraises_payload_codec_error; the Twisted/trial runs and all other jobs are green. Note: This work was completed with AI assistance (Claude Code). --- .../test/test_wamp_payload_codec_error.py | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 src/autobahn/wamp/test/test_wamp_payload_codec_error.py diff --git a/src/autobahn/wamp/test/test_wamp_payload_codec_error.py b/src/autobahn/wamp/test/test_wamp_payload_codec_error.py new file mode 100644 index 000000000..c3e718c03 --- /dev/null +++ b/src/autobahn/wamp/test/test_wamp_payload_codec_error.py @@ -0,0 +1,89 @@ +############################################################################### +# +# The MIT License (MIT) +# +# Copyright (c) typedef int GmbH +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +############################################################################### + +# Issue #1988: ApplicationSession.call() with a payload codec whose encode() +# raises must re-raise the codec's exception, on Twisted AND asyncio. +# Backend-neutral: runs under trial (USE_TWISTED) and pytest (USE_ASYNCIO). +# session.log is deliberately NOT mocked: a Mock has every method, so it +# would hide a logger method that one backend lacks. + +import os + +if os.environ.get("USE_TWISTED", False): + from twisted.trial import unittest + + from autobahn.twisted.wamp import ApplicationSession +else: + import unittest + + from autobahn.asyncio.wamp import ApplicationSession + +from autobahn.wamp.types import TransportDetails + + +class CodecError(Exception): + pass + + +class FailingPayloadCodec: + def encode(self, is_originating, uri, args=None, kwargs=None): + raise CodecError("encode failed") + + def decode(self, is_originating, uri, encoded_payload): + raise NotImplementedError() + + +class MockTransport: + def __init__(self): + self.messages = [] + self._transport_details = TransportDetails() + + def transport_details(self): + return self._transport_details + + def send(self, msg): + self.messages.append(msg) + + def close(self, *args, **kw): + pass + + +class TestPayloadCodecError(unittest.TestCase): + def test_call_reraises_payload_codec_error(self): + session = ApplicationSession() + session._transport = MockTransport() + session._payload_codec = FailingPayloadCodec() + + with self.assertRaises(CodecError): + session.call("com.example.procedure", 1, 2) + + # nothing went out on the wire + self.assertEqual(session._transport.messages, []) + + # trial fails a test that logged a Failure; the error was logged on + # purpose (if it is logged as a Failure at all), so accept it + if hasattr(self, "flushLoggedErrors"): + self.flushLoggedErrors(CodecError) From 7a0b4478f2b4d535d6720c98e9a7214a6ab51740 Mon Sep 17 00:00:00 2001 From: Tobias Oberstein Date: Sun, 18 Oct 2026 09:28:19 +0200 Subject: [PATCH 3/4] #1988: red (2) - one test class per backend, for the type checker The red commit 78b4d556 failed check-typing (ty, quality-checks job): the test class used a base class chosen at import time (twisted.trial.unittest.TestCase or unittest.TestCase), which ty sees as a union type it cannot use as a base ("unsupported-base"). I had not run `just check-typing` before pushing it. Now the test body is a module-level function taking a unittest.TestCase, called from one class per backend: a trial TestCase under USE_TWISTED (it flushes the logged CodecError, as before), a stdlib TestCase otherwise. The test itself is unchanged. Local (asgard1): `just check-typing cpy314` passes; cpy314 + pypy311 - trial PASSED, pytest FAILED with the AttributeError from wamp/protocol.py:1950 (the #1988 defect). Expected CI result: RED - only the asyncio/pytest runs (unit-tests on all five Pythons, quality-checks) fail in test_call_reraises_payload_codec_error; check-typing, the trial runs and all other jobs are green. Note: This work was completed with AI assistance (Claude Code). --- .../test/test_wamp_payload_codec_error.py | 48 ++++++++++++------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/src/autobahn/wamp/test/test_wamp_payload_codec_error.py b/src/autobahn/wamp/test/test_wamp_payload_codec_error.py index c3e718c03..a90bedd69 100644 --- a/src/autobahn/wamp/test/test_wamp_payload_codec_error.py +++ b/src/autobahn/wamp/test/test_wamp_payload_codec_error.py @@ -31,18 +31,17 @@ # would hide a logger method that one backend lacks. import os +import unittest -if os.environ.get("USE_TWISTED", False): - from twisted.trial import unittest +from autobahn.wamp.types import TransportDetails + +USE_TWISTED = bool(os.environ.get("USE_TWISTED", False)) +if USE_TWISTED: from autobahn.twisted.wamp import ApplicationSession else: - import unittest - from autobahn.asyncio.wamp import ApplicationSession -from autobahn.wamp.types import TransportDetails - class CodecError(Exception): pass @@ -71,19 +70,32 @@ def close(self, *args, **kw): pass -class TestPayloadCodecError(unittest.TestCase): - def test_call_reraises_payload_codec_error(self): - session = ApplicationSession() - session._transport = MockTransport() - session._payload_codec = FailingPayloadCodec() +def check_call_reraises_payload_codec_error(test: unittest.TestCase) -> None: + session = ApplicationSession() + session._transport = MockTransport() + session._payload_codec = FailingPayloadCodec() - with self.assertRaises(CodecError): - session.call("com.example.procedure", 1, 2) + with test.assertRaises(CodecError): + session.call("com.example.procedure", 1, 2) - # nothing went out on the wire - self.assertEqual(session._transport.messages, []) + # nothing went out on the wire + test.assertEqual(session._transport.messages, []) - # trial fails a test that logged a Failure; the error was logged on - # purpose (if it is logged as a Failure at all), so accept it - if hasattr(self, "flushLoggedErrors"): + +# One class per backend (a base class chosen at import time is a union type, +# which the type checker cannot use as a base). +if USE_TWISTED: + from twisted.trial import unittest as trial_unittest + + class TestPayloadCodecError(trial_unittest.TestCase): + def test_call_reraises_payload_codec_error(self): + check_call_reraises_payload_codec_error(self) + # trial fails a test that logged a Failure; the error is logged + # on purpose (if it is logged as a Failure at all), so accept it self.flushLoggedErrors(CodecError) + +else: + + class TestPayloadCodecError(unittest.TestCase): + def test_call_reraises_payload_codec_error(self): + check_call_reraises_payload_codec_error(self) From 10cb6c725e83a93f6907b2212e780908584a4717 Mon Sep 17 00:00:00 2001 From: Tobias Oberstein Date: Sun, 18 Oct 2026 08:56:53 +0200 Subject: [PATCH 4/4] #1988: green - log payload codec errors in call() on both backends ApplicationSession.call() logged a payload codec failure with self.log.failure(), which only the Twisted logger has: on asyncio the except block itself raised AttributeError, so the codec's exception was neither logged nor raised to the caller. Log it with log.error() and the traceback from txaio.failure_format_traceback(txaio.create_failure()) - the same pattern as the #1986 asyncio receive-path fix - which works on both backends, then re-raise. The other log.failure() calls in the code base are in Twisted-only modules (autobahn/twisted/...); wamp/protocol.py had only this one. Changelog bullet in the asyncio group. Local (asgard1): full matrix green, 20/20 (cpy311-314 + pypy311 x trial/pytest x NVX on/off), one more test passing per run than before; the asyncio log line shows the procedure and the codec's traceback. Re-run after red (2) restructured the test: still 20/20. ruff check and `just check-typing cpy314` (ty) pass. Expected CI result: GREEN - all jobs, including test_call_reraises_payload_codec_error on both backends. Note: This work was completed with AI assistance (Claude Code). --- docs/changelog.rst | 1 + src/autobahn/wamp/protocol.py | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 01e4b9766..e1b67d848 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -25,6 +25,7 @@ Changelog **asyncio** * Fix ``autobahn.asyncio.component.run()``, ``ApplicationRunner.run()``, ``Component.start()`` and the asyncio WebSocket factories failing on CPython 3.14 with ``RuntimeError: There is no current event loop`` when started from plain synchronous code (and the ``DeprecationWarning`` on 3.12 / 3.13): CPython no longer creates the loop implicitly in ``asyncio.get_event_loop()``. A new helper ``autobahn.asyncio.util.get_or_create_event_loop()`` returns the running loop, else the loop set for the thread, else a new one; ``run()`` was first fixed by Lukas Komischke (#1952, #1953), the remaining call sites follow, with a regression test that runs each case in a fresh interpreter (#1965) +* Fix ``ApplicationSession.call()`` on asyncio raising ``AttributeError`` instead of the payload codec's exception when the payload codec fails to encode the call: the error was logged with ``log.failure()``, which only the Twisted logger has. It is now logged with ``log.error()`` and the traceback, on both Twisted and asyncio, and the codec's exception reaches the caller (#1988) **Dependencies** diff --git a/src/autobahn/wamp/protocol.py b/src/autobahn/wamp/protocol.py index 37843eedf..58ffc716c 100644 --- a/src/autobahn/wamp/protocol.py +++ b/src/autobahn/wamp/protocol.py @@ -1947,7 +1947,13 @@ def call(self, procedure: str, *args, **kwargs) -> Any | CallResult: True, procedure, args, kwargs ) except: - self.log.failure() + # log.failure() exists on Twisted only, so log the traceback + # explicitly - this works on both Twisted and asyncio (#1988) + self.log.error( + "payload codec failed to encode call to {procedure}:\n{tb}", + procedure=procedure, + tb=txaio.failure_format_traceback(txaio.create_failure()), + ) raise if encoded_payload: