Skip to content

Commit 28daa52

Browse files
authored
fix(events): ensure callable metadata is replicated (#4045)
When passing sync functions to listeners which can raise an error put the Litestar into unrecoverable state. Fixes #4044
1 parent 171f98e commit 28daa52

File tree

2 files changed

+32
-10
lines changed

2 files changed

+32
-10
lines changed

litestar/events/listener.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
from __future__ import annotations
22

33
import logging
4+
from functools import update_wrapper
45
from typing import TYPE_CHECKING, Any
56

67
from litestar.exceptions import ImproperlyConfiguredException
78
from litestar.utils import ensure_async_callable
9+
from litestar.utils.predicates import is_async_callable
810

911
if TYPE_CHECKING:
1012
from litestar.types import AnyCallable, AsyncAnyCallable
@@ -42,7 +44,12 @@ def __call__(self, fn: AnyCallable) -> EventListener:
4244
if not callable(fn):
4345
raise ImproperlyConfiguredException("EventListener instance should be called as a decorator on a callable")
4446

45-
self.fn = self.wrap_in_error_handler(ensure_async_callable(fn))
47+
_fn = ensure_async_callable(fn)
48+
49+
if not is_async_callable(fn):
50+
update_wrapper(_fn, fn)
51+
52+
self.fn = self.wrap_in_error_handler(_fn)
4653

4754
return self
4855

tests/unit/test_events.py

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -111,34 +111,49 @@ async def test_raises_when_not_listener_are_registered_for_an_event_id(async_lis
111111
client.app.emit("x")
112112

113113

114-
async def test_event_listener_raises_exception(async_listener: EventListener, mock: MagicMock) -> None:
114+
async def test_event_listener_raises_exception(
115+
async_listener: EventListener, sync_listener: EventListener, mock: MagicMock
116+
) -> None:
115117
"""Test that an event listener that raises an exception does not prevent other listeners from being called.
116118
117119
https://github.com/litestar-org/litestar/issues/2809
120+
https://github.com/litestar-org/litestar/issues/4044
118121
"""
119122

120123
error_mock = MagicMock()
121124

122-
@listener("error_event")
123-
async def raising_listener(*args: Any, **kwargs: Any) -> None:
125+
@listener("async_error_event")
126+
async def async_raising_listener(*args: Any, **kwargs: Any) -> None:
127+
error_mock()
128+
raise ValueError("test")
129+
130+
@listener("sync_error_event")
131+
def sync_raising_listener(*args: Any, **kwargs: Any) -> None:
124132
error_mock()
125133
raise ValueError("test")
126134

127-
@get("/error")
135+
@get("/async-error")
128136
def route_handler_1(request: Request[Any, Any, Any]) -> None:
129-
request.app.emit("error_event")
137+
request.app.emit("async_error_event")
130138

131-
@get("/no-error")
139+
@get("/sync-error")
132140
def route_handler_2(request: Request[Any, Any, Any]) -> None:
141+
request.app.emit("sync_error_event")
142+
143+
@get("/no-error")
144+
def route_handler_3(request: Request[Any, Any, Any]) -> None:
133145
request.app.emit("test_event")
134146

135147
with create_test_client(
136-
route_handlers=[route_handler_1, route_handler_2], listeners=[async_listener, raising_listener]
148+
route_handlers=[route_handler_1, route_handler_2, route_handler_3],
149+
listeners=[async_listener, sync_listener, async_raising_listener, sync_raising_listener],
137150
) as client:
138-
first_response = client.get("/error")
139-
second_response = client.get("/no-error")
151+
first_response = client.get("/async-error")
152+
second_response = client.get("/sync-error")
153+
third_response = client.get("/no-error")
140154
assert first_response.status_code == HTTP_200_OK
141155
assert second_response.status_code == HTTP_200_OK
156+
assert third_response.status_code == HTTP_200_OK
142157

143158
error_mock.assert_called()
144159
mock.assert_called()

0 commit comments

Comments
 (0)