Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .audit/oberstet_fix_1988.md
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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**

Expand Down
8 changes: 7 additions & 1 deletion src/autobahn/wamp/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
101 changes: 101 additions & 0 deletions src/autobahn/wamp/test/test_wamp_payload_codec_error.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
###############################################################################
#
# 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
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:
from autobahn.asyncio.wamp import ApplicationSession


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


def check_call_reraises_payload_codec_error(test: unittest.TestCase) -> None:
session = ApplicationSession()
session._transport = MockTransport()
session._payload_codec = FailingPayloadCodec()

with test.assertRaises(CodecError):
session.call("com.example.procedure", 1, 2)

# nothing went out on the wire
test.assertEqual(session._transport.messages, [])


# 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)
Loading