Annotation hit-history pager never ends for limit=0: has_more is computed from a page size the query does not use

这个报错出现在自托管 Dify 未合并的 main 上,调用命中历史(hit-histories)接口传入 limit=0 (或 limit=-1 )时,接口按每页 1 条查询、却按未钳制的 limit 计算 has_more ,导致分页永不结束;优先排查该路由是否绑定了带 ge=1 约束的查询模型

快速结论:这个报错出现在自托管 Dify 未合并的 main 上,调用命中历史(hit-histories)接口传入 limit=0(或 limit=-1)时,接口按每页 1 条查询、却按未钳制的 limit 计算 has_more,导致分页永不结束;优先排查该路由是否绑定了带 ge=1 约束的查询模型。

适用环境:Dify Self Hosted (Source),main at a4d74f7c;其余版本、Python、CUDA、显卡等环境信息 Issue 中未确认。

最快修复方案:暂无确认的一步修复方案。Issue 中被验证的方向是:让 AnnotationHitHistoryListApi 像同文件的 AnnotationApi.get 一样,通过 @model_validate(AnnotationHitHistoryListQuery) 绑定请求参数,使 ge=1 约束在进入 handler 前拒绝非正的 page/limit。该改动需要 maintainer 审核合并,尚未落地。

注意事项:该修复只针对 hit-history 路由;Issue 指出 AnnotationApi.get(控制台)与 Service API 的 AnnotationListApi.get 也存在同样的 has_more 重算逻辑,但它们通过 Pydantic 模型的 ge=1 保护,目前不可达,修复时应一并考虑但不要误判为已修。客户端侧对 limit<=0 做校验只能规避、不能修复服务端行为。

问题场景

在自托管 Dify 源码部署中,直接请求标注命中历史接口:

GET /console/api/apps/{app_id}/annotations/{annotation_id}/hit-histories?limit=0

客户端按页遍历、以 has_more 为 false 作为停止条件时,会在数据已取完后继续拿到空页,且每页仍然返回 has_more: true,分页循环无法结束。limit=-1 行为相同。?page=0 也有类似缺口:查询按第 1 页执行,但响应回显 page: 0

报错原文

Annotation hit-history pager never ends for limit=0: has_more is computed from a page size the query does not use

has_more=page * effective_limit  client never stopped after 10 pages

原因分析

已确认的原因(Issue 中经代码定位验证):AnnotationHitHistoryListApi.get 直接用 request.argstype=int 读取 page/limit,无下界校验,只通过 effective_limit = min(limit, 100) 钳制上界;而真正执行查询的 libs/pagination.paginate_query 会在查询前把两者下限抬到 1(page = max(1, page)per_page = max(1, per_page))。于是 limit<=0 时查询按每页 1 条执行,响应里的 has_more 却用未钳制的 effective_limit 计算为 page * effective_limit < total,即 page * 0 < total 恒为真。

进一步定位:同文件已存在带 ge=1 约束的 AnnotationHitHistoryListQuery,但该路由只用 query_params_from_model(...) 喂给 OpenAPI 文档生成,并未用 @model_validate(...) 绑定请求解析;而 AnnotationApi.get 和 Service API 的 AnnotationListApi.get 都走了 @model_validate(...),所以能在 handler 执行前拒绝 limit<=0。这一“模型存在但未接线”的错配是直接原因。PaginatedResult.has_next 本身在上述复现中始终正确,问题出在 handler 自行用未钳制输入重算。

环境排查

  • 确认 Dify 部署方式:Self Hosted (Source)。
  • 确认代码版本是否为 main at a4d74f7c(Issue 中确认的问题版本)。
  • 确认请求是否命中 /console/api/apps/{app_id}/annotations/{annotation_id}/hit-histories 路由,且查询串中 limit 为 0 或负数(或 page 为 0)。
  • 确认该路由是否已用 @model_validate(AnnotationHitHistoryListQuery) 绑定参数;若仍只用于 OpenAPI 文档生成,则问题仍可复现。
  • Python、PyTorch、CUDA、显卡等版本与本问题无直接关联,Issue 中未提供,无需作为排查项。

解决步骤

  1. 先确认触发条件:用客户端或 curl 请求 hit-histories 接口并传 limit=0,观察响应中的 has_more 是否在数据取完后仍为 true。
  2. 定位漏洞点:检查 api/controllers/console/app/annotation.pyAnnotationHitHistoryListApi.get 是否直接读取 request.args,且 effective_limit = min(limit, 100) 没有下界。
  3. 按 Issue 验证过的修复方向改造:为该路由改用 @model_validate(AnnotationHitHistoryListQuery) 绑定请求,复用其中 limit: int = Field(default=20, ge=1)page 同理)的约束,使非正参数在进入 handler 前被拒绝,从根上避免 has_more 用未钳制值重算。
  4. 代码改动需提交 PR 并由 maintainer 审核合并;Issue 中该修复尚处于待合并状态,社区版用户可先本地应用后验证。
  5. 临时规避:在客户端侧不要发送 limit<=0page<=0,改用正常正整数值,可避免分页不停止。

验证方法

修复后重放同一请求:GET .../hit-histories?limit=0 应被参数校验直接拒绝(而非进入 handler),或至少返回的 limit/page 与查询实际使用的值一致;当最后一页取完后 has_more 必须变为 false,客户端按 has_more 遍历能正常终止。对照 Issue 中的七行数据场景,limit=0 不应再出现超过末页仍返回 has_more: true 的空页。同时回归 limit>100、恰好整页等 #41875 场景以及 AnnotationApi.getAnnotationListApi.get 等兄弟路由,确认未引入回归。

参考来源

langgenius/dify #42322

GamsGo AI

AI 工具推荐

想把多个 AI 模型放在一个入口?

GamsGo AI 集成 ChatGPT、DeepSeek、Gemini、Claude、Midjourney、Veo 等常用模型,适合写作、绘图、视频和日常 AI 工作流。

了解 GamsGo AI

推广链接:通过此链接购买,我可能获得佣金,不影响你的价格。

这个方案解决了吗?

celebrityanime
celebrityanime
文章: 23921

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注