Skip to content

[Bug] timed_with_status decorator silently swallows exceptions and returns None #1523

Closed

Description

@Ptah-CT

Summary

timed_with_status in src/memos/utils.py catches every exception, but when no fallback callable is configured the wrapper falls through to an implicit return None. Decorated functions therefore return None on any failure instead of raising, which masks the real error.

Where

src/memos/utils.py, lines 43-56 (current main @ cddc252).

try:
    result = fn(*args, **kwargs)
    success_flag = True
    return result
except Exception as e:
    exc_type = type(e)
    stack_info = "".join(traceback.format_stack()[:-1])
    exc_message = f"{stack_info}{traceback.format_exc()}"
    success_flag = False

    if fallback is not None and callable(fallback):
        result = fallback(e, *args, **kwargs)
        return result
    # ← no `raise` here; wrapper falls through to implicit return None
finally:
    ...

Impact

OpenAILLM.generate is decorated with @timed_with_status(...). When the upstream LLM returns 4xx/5xx (e.g. MiniMax 400 chat content is empty (2013) for a system-only message), the BadRequestError is caught, logged as status: FAILED, and then swallowed. generate() returns None to its caller.

Downstream that None flows into clean_json_response(response) (src/memos/mem_os/utils/format_utils.py:1403) and crashes with:

AttributeError: 'NoneType' object has no attribute 'replace'

Two consequences:

  1. The user sees a confusing AttributeError instead of the real 400 from the LLM. Diagnosis is hard because nothing in the traceback names the LLM call.
  2. It is a silent fail by API contract. Callers cannot tell whether generate() succeeded with empty output or failed with an exception, because the same None represents both.

Reproduction

  1. Set MOS_CHAT_MODEL=MiniMax-M2.7, OPENAI_API_BASE=https://api.minimax.io/v1, valid OPENAI_API_KEY.
  2. Start memos server, ensure default cube exists.
  3. POST /product/suggestions with a mem_cube_id whose recent memories are empty (so the suggestion prompt has only a system message).
  4. Observe HTTP 500 'NoneType' object has no attribute 'replace' in the response, and [TIMER_WITH_STATUS] OpenAI LLM took 5051 ms, status: FAILED, error_type: BadRequestError, error_message: ... immediately above it in the log.

Proposed fix

Add an explicit raise after the fallback branch:

                if fallback is not None and callable(fallback):
                    result = fallback(e, *args, **kwargs)
                    return result
                raise
            finally:
                ...

This preserves existing fallback semantics and makes the no-fallback path fail-fast.

I will open a PR with this change against main.

Related

The same swallow likely contributes to other reports where downstream code receives unexpected None/empty values from LLM helpers (e.g. #1324 memory_search always returns no results with reasoning-enabled models — different root cause, but the same pattern of LLM-call failure being invisible to the caller).

Activity

  1. MaiHHConnect commented on Apr 23, 2026

    @MaiHHConnect
  2. added
    area:coreMOS 编排层 / 框架底座 / 跨模块问题
    ai:taskDispatched to AI coding agent | 已派发给 AI 编码任务
    on May 26, 2026
  3. Memtensor-AI commented on May 28, 2026

    @Memtensor-AI
    Collaborator

    ✅ AutoDev task 702c33102691b25f completed.

    Summary: 修复 #1523:timed_with_status 在未配置 fallback 时会吞掉异常返回 None,导致下游 clean_json_response 抛出误导性的 AttributeError。在 fallback 分支后追加显式 raise,让无 fallback 路径 fail-fast,同时保留原有 fallback 语义与 status: FAILED 日志。已更新 test_failure_logging_no_fallback 使其断言异常会冒泡,并新增 test_no_fallback_does_not_return_none 防止再回归。已通过独立脚本验证三条路径(无 fallback 抛出 / 有 fallback 返回 / 成功返回原值)行为均符合预期。

    Base branch: v2.0.16
    Branch: autodev/MemOS-1523
    Commit: 249ac53
    PR: #1826
    Assigned to: @CarltonXiang, @syzsunshine219
    Reviewers: @CarltonXiang, @syzsunshine219

  4. added
    types:bugSomething isn't working | 功能异常
    ai:codingAI agent is coding | AI 正在编码
    on Jun 1, 2026
  5. self-assigned this
    on Jun 4, 2026
  6. Memtensor-AI commented on Jun 4, 2026

    @Memtensor-AI
    Collaborator

    🤖 AutoDev has picked up this issue and started working on it.

    Task ID: d84eb4283983e6d4
    Working branch: bugfix/autodev-1523
    Target branch: dev-20260604-v2.0.19
    Workflow: opsp (analysis → coding → testing → PR)

    I will post the PR link here once done. If I need more information, I will ask in the comments.

  7. added a commit that references this issue on Jun 4, 2026
    8d0c04b
  8. Memtensor-AI commented on Jun 4, 2026

    @Memtensor-AI
    Collaborator

    ✅ AutoDev task d84eb4283983e6d4 completed.

    Summary: Successfully fixed issue #1523: timed_with_status decorator now correctly re-raises exceptions when no fallback is configured, preventing silent failures.

    What Changed

    Root Cause: The timed_with_status decorator in src/memos/utils.py was catching all exceptions but silently swallowing them when no fallback was provided. This caused decorated functions like OpenAILLM.generate() to return None instead of propagating errors, masking real failures with confusing downstream errors.

    Fix Applied: Added a single raise statement at line 182, immediately after the fallback branch. This ensures exceptions are re-raised when no fallback is configured, while preserving existing fallback behavior.

    Code Change:

    if fallback is not None and callable(fallback):
        result = fallback(e, *args, **kwargs)
        return result
    raise  # ← Added this line
    

    Testing

    Created comprehensive regression test tests/test_utils_timing_exception_reraise.py with 4 test cases:

    • Exception re-raising when no fallback configured
    • Original exception type preservation
    • Fallback behavior still works when configured
    • No implicit None return on failure

    Verified with standalone test script - all tests passed.

    Impact

    Before: OpenAILLM.generate() returning None on LLM errors caused AttributeError: 'NoneType' object has no attribute 'replace' in downstream code, hiding the real BadRequestError.

    After: Original exceptions propagate correctly with meaningful tracebacks, enabling proper error handling and debugging.

    Compatibility

    • Functions with fallback parameter: No change
    • Functions without fallback: Now correctly raise exceptions (this is the intended fail-fast behavior)

    The fix affects 3 existing uses in the codebase:

    • openai.py (2 uses): Now correctly propagate LLM errors
    • universal_api.py (1 use): Now correctly propagate embedding errors
    • http_bge.py (1 use with fallback): Behavior unchanged

    Base branch: dev-20260604-v2.0.19
    Branch: bugfix/autodev-1523
    Commit: 8d0c04b
    PR: #1885
    Assigned to: @MatthewZhuang, @CarltonXiang, @syzsunshine219
    Reviewers: @MatthewZhuang, @CarltonXiang, @syzsunshine219

  9. added
    ai:testingAI agent is running tests | AI 正在运行测试
    and removed
    ai:codingAI agent is coding | AI 正在编码
    ai:testingAI agent is running tests | AI 正在运行测试
    ai:taskDispatched to AI coding agent | 已派发给 AI 编码任务
    on Jun 4, 2026
  10. added a commit that references this issue on Jul 2, 2026
    ca75756
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:coreMOS 编排层 / 框架底座 / 跨模块问题types:bugSomething isn't working | 功能异常

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions