Repository: litestar-org/litestar

Issue:
Bug: converting objects to a Bool in debug response
### Description

The function `litestar.middleware.exceptions._debug_response.get_symbol_name` [on line 51](https://github.com/litestar-org/litestar/blob/main/litestar/middleware/exceptions/_debug_response.py#L51):
```py
instance_or_cls = locals_dict.get("self") or locals_dict.get("cls")
```
The use of `or` operator calls the `__bool__` function of the objects mapped to the `self` or `cls` keys. This causes problems when the object cannot be converted to bool which is unfortunately the case when using sqlalchemy, because it raises a `TypeError` for some of its classes ([see here](https://github.com/sqlalchemy/sqlalchemy/blob/main/lib/sqlalchemy/sql/elements.py#L749)). The problem being that instead of the original DB error, the TypeError is propagated and the web exception renderer doesn't work for this case.



### URL to code causing the issue

_No response_

### MCVE

```python
from litestar import Litestar, get
from litestar.contrib.sqlalchemy.base import BigIntBase
from litestar.contrib.sqlalchemy.plugins import (
    AsyncSessionConfig,
    SQLAlchemyAsyncConfig,
    SQLAlchemyInitPlugin,
)
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Mapped, mapped_column


class Name(BigIntBase):
    name: Mapped[str] = mapped_column(unique=True)


@get("/")
async def add_row(db_session: AsyncSession) -> dict:
    async with db_session.begin():
        new_name = Name(name="taken")
        db_session.add(new_name)

    return {"status": "ok"}


sqlalchemy_config = SQLAlchemyAsyncConfig(
    connection_string="sqlite+aiosqlite:///test.sqlite3",
    session_config=AsyncSessionConfig(expire_on_commit=False),
)
sqlalchemy_plugin = SQLAlchemyInitPlugin(config=sqlalchemy_config)


async def on_startup() -> None:
    """Initializes the database."""
    async with sqlalchemy_config.get_engine().begin() as conn:
        await conn.run_sync(BigIntBase.metadata.create_all)


app = Litestar(
    route_handlers=[add_row], plugins=[sqlalchemy_plugin], on_startup=[on_startup]
)

# Run "get /" twice - first call creates the entry, second one causes the error.
```


### Steps to reproduce

```bash
1. Run the MCVE app (requires `aiosqlite` to be installed) with `--debug`
2. Navigate to "/" for the first time which creates the db row
3. Navigate to "/" for the second time to cause an IntegrityError
4. A `TypeError` is raised and propagated instead of the original DB error
5. The web debug exception renderer doesn't work
```


### Screenshots

_No response_

### Logs

```bash
[SQL: INSERT INTO name (name) VALUES (?)]
[parameters: ('taken',)]
(Background on this error at: https://sqlalche.me/e/20/gkpj)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/geeshta/prog/litestar/bugs/.venv/lib/python3.11/site-packages/litestar/middleware/exceptions/middleware.py", line 191, in __call__
    await self.app(scope, receive, send)
  File "/home/geeshta/prog/litestar/bugs/.venv/lib/python3.11/site-packages/litestar/middleware/exceptions/middleware.py", line 205, in __call__
    await self.handle_request_exception(
  File "/home/geeshta/prog/litestar/bugs/.venv/lib/python3.11/site-packages/litestar/middleware/exceptions/middleware.py", line 234, in handle_request_exception
    response = exception_handler(request, exc)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/geeshta/prog/litestar/bugs/.venv/lib/python3.11/site-packages/litestar/middleware/exceptions/middleware.py", line 270, in default_http_exception_handler
    return create_debug_response(request=request, exc=exc)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/geeshta/prog/litestar/bugs/.venv/lib/python3.11/site-packages/litestar/middleware/exceptions/_debug_response.py", line 184, in create_debug_response
    content: Any = create_html_response_content(exc=exc, request=request)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/geeshta/prog/litestar/bugs/.venv/lib/python3.11/site-packages/litestar/middleware/exceptions/_debug_response.py", line 137, in create_html_response_content
    exception_data: list[str] = [create_exception_html(exc, line_limit)]
                                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/geeshta/prog/litestar/bugs/.venv/lib/python3.11/site-packages/litestar/middleware/exceptions/_debug_response.py", line 122, in create_exception_html
    result = [create_frame_html(frame=frame, collapsed=idx > 0) for idx, frame in enumerate(reversed(frames))]
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/geeshta/prog/litestar/bugs/.venv/lib/python3.11/site-packages/litestar/middleware/exceptions/_debug_response.py", line 122, in <listcomp>
    result = [create_frame_html(frame=frame, collapsed=idx > 0) for idx, frame in enumerate(reversed(frames))]
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/geeshta/prog/litestar/bugs/.venv/lib/python3.11/site-packages/litestar/middleware/exceptions/_debug_response.py", line 104, in create_frame_html
    "symbol_name": escape(get_symbol_name(frame)),
                          ^^^^^^^^^^^^^^^^^^^^^^
  File "/home/geeshta/prog/litestar/bugs/.venv/lib/python3.11/site-packages/litestar/middleware/exceptions/_debug_response.py", line 51, in get_symbol_name
    instance_or_cls = locals_dict.get("self") or locals_dict.get("cls")
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/geeshta/prog/litestar/bugs/.venv/lib/python3.11/site-packages/sqlalchemy/sql/elements.py", line 749, in __bool__
    raise TypeError("Boolean value of this clause is not defined")
TypeError: Boolean value of this clause is not defined

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/geeshta/prog/litestar/bugs/.venv/lib/python3.11/site-packages/litestar/middleware/exceptions/middleware.py", line 191, in __call__
    await self.app(scope, receive, send)
  File "/home/geeshta/prog/litestar/bugs/.venv/lib/python3.11/site-packages/litestar/_asgi/asgi_router.py", line 84, in __call__
    await asgi_app(scope, receive, send)
  File "/home/geeshta/prog/litestar/bugs/.venv/lib/python3.11/site-packages/litestar/middleware/exceptions/middleware.py", line 205, in __call__
    await self.handle_request_exception(
  File "/home/geeshta/prog/litestar/bugs/.venv/lib/python3.11/site-packages/litestar/middleware/exceptions/middleware.py", line 234, in handle_request_exception
    response = exception_handler(request, exc)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/geeshta/prog/litestar/bugs/.venv/lib/python3.11/site-packages/litestar/middleware/exceptions/middleware.py", line 270, in default_http_exception_handler
    return create_debug_response(request=request, exc=exc)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/geeshta/prog/litestar/bugs/.venv/lib/python3.11/site-packages/litestar/middleware/exceptions/_debug_response.py", line 184, in create_debug_response
    content: Any = create_html_response_content(exc=exc, request=request)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/geeshta/prog/litestar/bugs/.venv/lib/python3.11/site-packages/litestar/middleware/exceptions/_debug_response.py", line 137, in create_html_response_content
    exception_data: list[str] = [create_exception_html(exc, line_limit)]
                                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/geeshta/prog/litestar/bugs/.venv/lib/python3.11/site-packages/litestar/middleware/exceptions/_debug_response.py", line 122, in create_exception_html
    result = [create_frame_html(frame=frame, collapsed=idx > 0) for idx, frame in enumerate(reversed(frames))]
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/geeshta/prog/litestar/bugs/.venv/lib/python3.11/site-packages/litestar/middleware/exceptions/_debug_response.py", line 122, in <listcomp>
    result = [create_frame_html(frame=frame, collapsed=idx > 0) for idx, frame in enumerate(reversed(frames))]
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/geeshta/prog/litestar/bugs/.venv/lib/python3.11/site-packages/litestar/middleware/exceptions/_debug_response.py", line 104, in create_frame_html
    "symbol_name": escape(get_symbol_name(frame)),
                          ^^^^^^^^^^^^^^^^^^^^^^
  File "/home/geeshta/prog/litestar/bugs/.venv/lib/python3.11/site-packages/litestar/middleware/exceptions/_debug_response.py", line 51, in get_symbol_name
    instance_or_cls = locals_dict.get("self") or locals_dict.get("cls")
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/geeshta/prog/litestar/bugs/.venv/lib/python3.11/site-packages/sqlalchemy/sql/elements.py", line 749, in __bool__
    raise TypeError("Boolean value of this clause is not defined")
TypeError: Boolean value of this clause is not defined

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/geeshta/prog/litestar/bugs/.venv/lib/python3.11/site-packages/uvicorn/protocols/http/httptools_impl.py", line 426, in run_asgi
    result = await app(  # type: ignore[func-returns-value]
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/geeshta/prog/litestar/bugs/.venv/lib/python3.11/site-packages/uvicorn/middleware/proxy_headers.py", line 84, in __call__
    return await self.app(scope, receive, send)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/geeshta/prog/litestar/bugs/.venv/lib/python3.11/site-packages/litestar/app.py", line 517, in __call__
    await self.asgi_handler(scope, receive, self._wrap_send(send=send, scope=scope))  # type: ignore[arg-type]
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/geeshta/prog/litestar/bugs/.venv/lib/python3.11/site-packages/litestar/middleware/exceptions/middleware.py", line 205, in __call__
    await self.handle_request_exception(
  File "/home/geeshta/prog/litestar/bugs/.venv/lib/python3.11/site-packages/litestar/middleware/exceptions/middleware.py", line 234, in handle_request_exception
    response = exception_handler(request, exc)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/geeshta/prog/litestar/bugs/.venv/lib/python3.11/site-packages/litestar/middleware/exceptions/middleware.py", line 270, in default_http_exception_handler
    return create_debug_response(request=request, exc=exc)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/geeshta/prog/litestar/bugs/.venv/lib/python3.11/site-packages/litestar/middleware/exceptions/_debug_response.py", line 184, in create_debug_response
    content: Any = create_html_response_content(exc=exc, request=request)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/geeshta/prog/litestar/bugs/.venv/lib/python3.11/site-packages/litestar/middleware/exceptions/_debug_response.py", line 137, in create_html_response_content
    exception_data: list[str] = [create_exception_html(exc, line_limit)]
                                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/geeshta/prog/litestar/bugs/.venv/lib/python3.11/site-packages/litestar/middleware/exceptions/_debug_response.py", line 122, in create_exception_html
    result = [create_frame_html(frame=frame, collapsed=idx > 0) for idx, frame in enumerate(reversed(frames))]
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/geeshta/prog/litestar/bugs/.venv/lib/python3.11/site-packages/litestar/middleware/exceptions/_debug_response.py", line 122, in <listcomp>
    result = [create_frame_html(frame=frame, collapsed=idx > 0) for idx, frame in enumerate(reversed(frames))]
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/geeshta/prog/litestar/bugs/.venv/lib/python3.11/site-packages/litestar/middleware/exceptions/_debug_response.py", line 104, in create_frame_html
    "symbol_name": escape(get_symbol_name(frame)),
                          ^^^^^^^^^^^^^^^^^^^^^^
  File "/home/geeshta/prog/litestar/bugs/.venv/lib/python3.11/site-packages/litestar/middleware/exceptions/_debug_response.py", line 51, in get_symbol_name
    instance_or_cls = locals_dict.get("self") or locals_dict.get("cls")
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/geeshta/prog/litestar/bugs/.venv/lib/python3.11/site-packages/sqlalchemy/sql/elements.py", line 749, in __bool__
    raise TypeError("Boolean value of this clause is not defined")
TypeError: Boolean value of this clause is not defined
INFO:     127.0.0.1:48748 - "GET / HTTP/1.1" 500 Internal Server Error
```


### Litestar Version

2.1.1

### Platform

- [X] Linux
- [ ] Mac
- [ ] Windows
- [ ] Other (Please specify in the description above)

<!-- POLAR PLEDGE BADGE START -->
---

## Funding
* If you would like to see an issue prioritized, make a pledge towards it!
* We receive the pledge once the issue is completed & verified

<a href="https://polar.sh/litestar-org/litestar/issues/2381">
<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://polar.sh/api/github/litestar-org/litestar/issues/2381/pledge.svg?darkmode=1">
  <img alt="Fund with Polar" src="https://polar.sh/api/github/litestar-org/litestar/issues/2381/pledge.svg">
</picture>
</a>
<!-- POLAR PLEDGE BADGE END -->



Discussion:

Resolving patch:
diff --git a/litestar/middleware/exceptions/_debug_response.py b/litestar/middleware/exceptions/_debug_response.py
--- a/litestar/middleware/exceptions/_debug_response.py
+++ b/litestar/middleware/exceptions/_debug_response.py
@@ -48,8 +48,9 @@ def get_symbol_name(frame: FrameInfo) -> str:
     locals_dict = frame.frame.f_locals
     # this piece assumes that the code uses standard names "self" and "cls"
     # in instance and class methods
-    instance_or_cls = locals_dict.get("self") or locals_dict.get("cls")
-    classname = f"{get_name(instance_or_cls)}." if instance_or_cls else ""
+    instance_or_cls = inst if (inst := locals_dict.get("self")) is not None else locals_dict.get("cls")
+
+    classname = f"{get_name(instance_or_cls)}." if instance_or_cls is not None else ""
 
     return f"{classname}{frame.function}"
 


Test patch:
diff --git a/tests/unit/test_middleware/test_exception_handler_middleware.py b/tests/unit/test_middleware/test_exception_handler_middleware.py
--- a/tests/unit/test_middleware/test_exception_handler_middleware.py
+++ b/tests/unit/test_middleware/test_exception_handler_middleware.py
@@ -1,3 +1,4 @@
+from inspect import getinnerframes
 from typing import TYPE_CHECKING, Any, Callable, Optional
 
 import pytest
@@ -10,6 +11,7 @@
 from litestar.exceptions import HTTPException, InternalServerException, ValidationException
 from litestar.logging.config import LoggingConfig, StructLoggingConfig
 from litestar.middleware.exceptions import ExceptionHandlerMiddleware
+from litestar.middleware.exceptions._debug_response import get_symbol_name
 from litestar.middleware.exceptions.middleware import get_exception_handler
 from litestar.status_codes import HTTP_400_BAD_REQUEST, HTTP_500_INTERNAL_SERVER_ERROR
 from litestar.testing import TestClient, create_test_client
@@ -340,3 +342,23 @@ def handler() -> None:
         assert caplog.records[0].message.startswith(
             "exception raised on http connection to route /test\n\nTraceback (most recent call last):\n"
         )
+
+
+def test_get_symbol_name_where_type_doesnt_support_bool() -> None:
+    class Test:
+        def __bool__(self) -> bool:
+            raise TypeError("This type doesn't support bool")
+
+        def method(self) -> None:
+            raise RuntimeError("Oh no!")
+
+    exc = None
+
+    try:
+        Test().method()
+    except Exception as e:
+        exc = e
+
+    if exc is not None and exc.__traceback__ is not None:
+        frame = getinnerframes(exc.__traceback__, 2)[-1]
+        assert get_symbol_name(frame) == "Test.method"