From 74b6b732c516e1a27cb7c807f04032cc791311f3 Mon Sep 17 00:00:00 2001 From: Cestercian <183791452+cestercian@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:14:41 +0530 Subject: [PATCH] fix: include OCS error response bodies in logged errors Signed-off-by: Cestercian <183791452+cestercian@users.noreply.github.com> --- ex_app/lib/logger.py | 48 +++++++++++++++++++++++++++++++++++++++++++- ex_app/lib/main.py | 13 ++++++------ 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/ex_app/lib/logger.py b/ex_app/lib/logger.py index bdf23c98..c9600963 100644 --- a/ex_app/lib/logger.py +++ b/ex_app/lib/logger.py @@ -1,7 +1,11 @@ # SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors # SPDX-License-Identifier: AGPL-3.0-or-later import asyncio +import inspect import logging +import traceback + +from nc_py_api import NextcloudException logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[logging.StreamHandler()]) logger = logging.getLogger('context_agent') @@ -14,4 +18,46 @@ async def log(nc, level, content): except asyncio.CancelledError: raise except Exception: - pass \ No newline at end of file + pass + +MAX_RESPONSE_TEXT_LENGTH = 2000 + + +async def _ocs_response_text(e: NextcloudException) -> str | None: + response = getattr(e, 'response', None) + if response is None: + return None + try: + text = response.text + # niquests' AsyncResponse.text is a coroutine, the sync Response's is a str + if inspect.isawaitable(text): + text = await text + except asyncio.CancelledError: + raise + except Exception: + return None + if not isinstance(text, str): + return None + text = text.strip() + if not text: + return None + if len(text) > MAX_RESPONSE_TEXT_LENGTH: + text = text[:MAX_RESPONSE_TEXT_LENGTH] + '... (truncated)' + return text + + +async def format_exception(e: BaseException) -> str: + """Format an exception with its traceback, like traceback.format_exception, + and append the body of any OCS error response in its chain, since + str(NextcloudException) only carries the status code and reason.""" + formatted = ''.join(traceback.format_exception(e)) + seen = set() + current: BaseException | None = e + while current is not None and id(current) not in seen: + seen.add(id(current)) + if isinstance(current, NextcloudException): + text = await _ocs_response_text(current) + if text is not None: + formatted += f'OCS response for [{current.status_code}]: {text}\n' + current = current.__cause__ or current.__context__ + return formatted diff --git a/ex_app/lib/main.py b/ex_app/lib/main.py index 6d2343c4..df7a29d2 100644 --- a/ex_app/lib/main.py +++ b/ex_app/lib/main.py @@ -2,7 +2,6 @@ # SPDX-License-Identifier: AGPL-3.0-or-later import concurrent.futures import os -import traceback from contextlib import asynccontextmanager from json import JSONDecodeError from threading import Event @@ -23,7 +22,7 @@ from ex_app.lib.agent import react from ex_app.lib.errors import UserFacingError -from ex_app.lib.logger import log +from ex_app.lib.logger import format_exception, log from ex_app.lib.mcp_server import UserAuthMiddleware, ToolListMiddleware from ex_app.lib.provider import provider, multimodal_provider from ex_app.lib.tools import get_categories @@ -207,7 +206,7 @@ async def background_thread_task(): await asyncio.sleep(2) continue except (NextcloudException, RequestException, JSONDecodeError) as e: - tb_str = ''.join(traceback.format_exception(e)) + tb_str = await format_exception(e) await log(nc, LogLvl.WARNING, "Error fetching the next task " + tb_str) await wait_for_task(5) continue @@ -262,17 +261,17 @@ async def stream_output(intermediate_output): ) except (NextcloudException, RequestException) as stream_err: stream_update_failed = True - tb_str = ''.join(traceback.format_exception(stream_err)) + tb_str = await format_exception(stream_err) await log(nc, LogLvl.WARNING, "Error streaming intermediate task result: " + tb_str) output = await react(task, nextcloud, stream_output=stream_output if stream_updates_enabled else None) except Exception as e: # noqa try: - tb_str = ''.join(traceback.format_exception(e)) + tb_str = await format_exception(e) await log(nc, LogLvl.ERROR, "Error: " + tb_str) await report_error(nc, task["id"], e) except (NextcloudException, RequestException) as net_err: - tb_str = ''.join(traceback.format_exception(net_err)) + tb_str = await format_exception(net_err) await log(nc, LogLvl.WARNING, "Network error in reporting the error: " + tb_str) finally: async with NUM_RUNNING_TASKS_LOCK: @@ -285,7 +284,7 @@ async def stream_output(intermediate_output): ) except Exception as e: try: - tb_str = ''.join(traceback.format_exception(e)) + tb_str = await format_exception(e) await log(nc, LogLvl.ERROR, "Error trying to report the task result: " + tb_str) except Exception: pass