Skip to content

Bug: Matcher.expire_time 传入带时区的 datetime 时,_check_matcher 每次检查抛 TypeError 且响应器永不销毁 #4236

Closed

Description

@SuellenQwQ

操作系统

Windows 11(与操作系统无关,任何平台均可复现)

Python 版本

3.12.7

NoneBot 版本

2.5.0(PyPI 最新版;master 分支 nonebot/message.py L387 代码一致,问题同样存在)

适配器

无(直接调用 nonebot.message._check_matcher,与适配器无关)

协议端

无

描述问题

Matcher.new(expire_time=...) 的类型注解允许传入 datetime,带时区的 aware datetime 也是自然用法(例如 datetime.now(timezone.utc) + timedelta(minutes=5),或从数据库/API 读出的带 tzinfo 的过期时刻)。但 _check_matcher 的过期判断:

if Matcher.expire_time and datetime.now() > Matcher.expire_time:   # message.py L387

用 naive 的 datetime.now() 与 aware 的 expire_time 比较,直接抛出:

TypeError: can't compare offset-naive and offset-aware datetimes

三个后果叠加:

  1. 该 TypeError 位于任何 try 保护之外(下面的 check_perm / check_rule 才有 try 包裹);
  2. 异常沿 check_and_run_matcher → handle_event 的任务组被捕获并记录,** matcher 不会被 destroy()** —— 永久留在 matcher 列表中;
  3. 临时响应器的 priority 为 0,handle_event 按 priority 从小到大检查,每个上报事件(不限群、不限类型)都会最先检查它并重复抛出同一 TypeError —— 日志持续刷屏,且该会话既不匹配也永不过期。

对照:同文件同一函数中,naive 的过期 matcher 会被正确 destroy();naive 的未过期 matcher 正常通过检查。即 naive 路径完全正常,仅 aware 输入崩溃。

复现步骤

完整最小脚本(无需适配器,pip install nonebot2[fastapi] 后即可运行):

import asyncio
from datetime import datetime, timedelta, timezone

import nonebot

nonebot.init()

from nonebot.matcher import Matcher, matchers
from nonebot.message import _check_matcher


def destroyed(m) -> bool:
    return not any(m in lst for lst in matchers.values())


async def main():
    # 对照组: naive expire_time —— 一切正常
    naive_m = Matcher.new("message", expire_time=datetime.now() + timedelta(hours=1),
                          temp=True, priority=1, block=True)
    await _check_matcher(Matcher=naive_m, bot=None, event=None, state={},
                         stack=None, dependency_cache=None)
    print("naive 未过期: 销毁了吗?", destroyed(naive_m), "(False = 正常保留)")

    # 实验组: aware expire_time —— 类型注解允许的输入
    aware_m = Matcher.new("message",
                          expire_time=datetime.now(timezone.utc) + timedelta(hours=1),
                          temp=True, priority=0, block=True)
    try:
        await _check_matcher(Matcher=aware_m, bot=None, event=None, state={},
                             stack=None, dependency_cache=None)
    except TypeError as e:
        print("aware: TypeError:", e)
        print("aware: matcher 已销毁?", destroyed(aware_m), "(True 才是预期)")


asyncio.run(main())

实际输出(本机实测):

[naive] _check_matcher 正常返回 | matcher 已销毁(过期即删): False (False = 正常保留)

[aware] TypeError: can't compare offset-naive and offset-aware datetimes
[aware] matcher 已销毁? False    <- 预期应销毁

再对照已过期的 naive matcher:destroy() 被正确调用(从 matcher 列表移除)——证明销毁逻辑本身正常,仅 aware 输入的_compare 抛错导致既不销毁也不匹配。

在真实进程中,handle_event 会在每个事件的任务组里捕获该 TypeError(表现为每个事件一条 "Error when checking Matcher." 错误日志),该临时 matcher(priority=0)永远占据检查首位。

期望的结果

aware datetime 应被正确处理——比较时统一时区(例如利用 expire_time 自身的 tzinfo):

if Matcher.expire_time and (
    datetime.now(tz=Matcher.expire_time.tzinfo) > Matcher.expire_time
):

tzinfo=None 时 datetime.now(tz=None) 即 naive now,与现有 naive 行为完全兼容;aware 时按其自身时区比较,两种输入都正确过期并销毁。

截图或日志

复现脚本实测输出(关键部分):

[naive] _check_matcher 正常返回 | matcher 已销毁(过期即删): False (False = 正常保留)

[aware] TypeError: can't compare offset-naive and offset-aware datetimes
[aware] matcher 已销毁? False    <- 预期应销毁

[expired-naive] matcher 已销毁(过期即删): True

(附:matcher 已销毁(过期即删): False 一行中,naive 未过期的 False 是"正常保留";aware 行的 False 是"应销毁未销毁"。)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions