快速结论:该问题通常出现在自托管或云端 Dify 的“重置密码/邮箱注册/所有者转移”验证码邮件链路:三个 token 生成函数使用可变默认参数 additional_data: dict[str, Any] = {},并在 TokenManager.generate_token 内的 Redis 往返让出执行权时被并发请求共享篡改,导致邮件中的验证码与 Redis 中存储的 token 数据不一致。优先排查 api/services/account_service.py 中 generate_reset_password_token、generate_email_register_token、generate_owner_transfer_token 是否仍在使用可变默认参数。
适用环境:Dify main 分支(Issue 中为提交 f9966c2);自托管 Docker 部署,Cloud 同样受影响;默认 worker 为 gevent(docker/docker-compose.yaml 中的 SERVER_WORKER_CLASS: geventwebsocket.gunicorn.workers.GeventWebSocketWorker,api/docker/entrypoint.sh 中的 --worker-class)。Issue 未给出操作系统、Python、CUDA、显卡或依赖的具体版本信息。
最快修复方案:暂无确认的一步修复方案。Issue 中提交者表示已有带修复与回归测试的 PR,但讨论中未展示已验证的合并结果;可优先尝试的修复方向是:为三个 token 生成函数改用 additional_data: dict[str, Any] | None = None,进入函数后新建字典(如 additional_data = dict(additional_data or {}))再写入 code,避免使用共享默认对象并避免修改调用方传入的字典。
注意事项:上述两行式修复属于 Issue 中提出的方向,尚不能视为已合入验证的结论;该并发竞态窗口较窄,不是每次请求必现,修复后需用并发请求验证。此外 Issue 提到 api/configs/remote_settings_sources/apollo/python_3x.py 的 http_request(headers={})、api/core/rag/extractor/notion_extractor.py 的 _get_notion_database_data(query_dict={}) 等位置存在同类可变默认参数,但目前仅读取,不代表同一故障已触发。
问题场景
用户在自托管 Docker 或 Cloud 版 Dify 中触发密码重置流程(account_service.py:707 的 send_reset_password_email),或触发邮箱注册邮件(account_service.py:740)、所有者转移邮件(account_service.py:840)时,三个调用方都没有传入 additional_data,于是共用同一个在函数定义时创建、进程内全局共享的默认字典。当 gevent worker 在 TokenManager.generate_token 中执行 Redis 查询/撤销操作并让出执行权时,同一进程内其他 greenlet 的请求会写入同一个字典,造成验证码错位。Issue 还指出第二个无条件副作用:当调用方显式传入字典时(如 api/controllers/console/workspace/members.py:527 传入 additional_data={}),函数会把 "code" 键写回调用方字典,而调用方并未请求该字段。
报错原文
Mutable default argument shared across requests in the token generators (password reset path)
相关关键代码形态(Issue 原文):
additional_data: dict[str, Any] = {}, # <- created once, at def time
...
additional_data["code"] = code # <- mutates that shared dict
原因分析
最可能的原因是 Python 可变默认参数语义:默认值 {} 仅在函数定义时创建一个对象,所有省略该参数的调用共享同一字典。在密码重置路径上,additional_data["code"] = code 的写入与 TokenManager.generate_token 中 token_data.update(additional_data) 的读取之间存在 Redis 往返(_get_current_token_for_account 与 revoke_token)。默认 gevent worker 下这些调用会让出执行权,使两个密码重置请求交错:后一个请求覆盖共享字典中的 code,导致 A 的 token 存入 B 的验证码,而 A 收到的邮件仍是 A 的本地变量验证码,最终重置失败,且 B 的验证码被绑定到 A 的 token 上。Issue 明确说明这是窗口较窄的竞态,而非每次请求必然发生。
环境排查
- 确认 Dify 版本:Issue 基于
main(f9966c2);对照其他版本时先确认对应代码是否仍含上述可变默认参数。 - 确认部署形态:自托管 Docker 或 Cloud;Issue 称两者均适用。
- 确认 worker 类型:查看
docker/docker-compose.yaml的SERVER_WORKER_CLASS与api/docker/entrypoint.sh的--worker-class,gevent 是竞态能被触发的前提。 - 检查
api/services/account_service.py中三个 token 生成函数(约 L909、L936 及generate_reset_password_token)的签名是否仍为additional_data: dict[str, Any] = {}。 - 检查调用方
account_service.py:707 / 740 / 840是否仍没有传入additional_data。 - 检查
api/controllers/console/workspace/members.py:527传入的additional_data={}是否被反向写入了"code"键。 - Issue 未提供操作系统、Python、CUDA、显卡及依赖版本,无需据此判断,也无法据此排除。
解决步骤
- 定位
api/services/account_service.py中generate_reset_password_token、generate_email_register_token(约 L909)、generate_owner_transfer_token(约 L936)三个函数。 - 将签名中的
additional_data: dict[str, Any] = {}改为additional_data: dict[str, Any] | None = None(可优先尝试的方向,Issue 未展示已合并代码)。 - 在函数体内写入
code前,先构造新字典,例如additional_data = dict(additional_data or {}),再从该新字典写入code,确保既不复用共享默认对象,也不修改调用方传入的字典。 - 对三处函数逐一实施相同改动,避免遗漏任一邮件链路。
- 由于 Issue 提到
generate_change_email_token已在 PR #36412 中改为类型化 token payload 对象、自定义工具认证凭据已在 PR #38616 中修复同类共享字典写入,可参考这两处的处理方式保持一致风格(仅作为参考,不要求在本次修复中一并改造)。 - 补充并发回归测试:同时发起两个密码重置请求,校验各自邮件中的验证码与其 token 在 Redis 中存储的
code一致;并断言调用方传入的additional_data在调用后不含被反向写入的"code"键。 - 如已有 PR,按 Dosu 建议与 issue #39815 协调,避免与同一根因的修复重复。
验证方法
修复后,对该函数反复并发调用两个密码重置请求,确认每个请求收到的邮件验证码与其 token 在 Redis 中对应的 code 完全一致,不再出现 A 收到 "111111" 但 Redis 存储 "222222" 的情况;同时确认 members.py 传入 additional_data={} 的调用返回后该字典中不出现额外写入的 "code" 键;再跑一遍补充的并发回归测试与相关邮件链路的功能测试,确认密码重置、邮箱注册、所有者转移三条路径均可正常完成。
参考来源
关联:langgenius/dify #39815(同一可变默认参数根因,含并发复现)、langgenius/dify PR #36412、langgenius/dify PR #38616。
AI 工具推荐
想把多个 AI 模型放在一个入口?
GamsGo AI 集成 ChatGPT、DeepSeek、Gemini、Claude、Midjourney、Veo 等常用模型,适合写作、绘图、视频和日常 AI 工作流。
推广链接:通过此链接购买,我可能获得佣金,不影响你的价格。
这个方案解决了吗?
可以继续搜索完整报错,或查看同一工具的其他排查指南。
![[bug]: Regional Guidance Layers should (probably) scale when changing resolution](https://www.chat-gpts.plus/wp-content/uploads/2026/11/6313-8337b0c7-768x403.jpg)
![[Bug]: anthropic.messages.acreate(stream=True) caches an empty reply from a non-Anthropic model and replays it on every repeat](https://www.chat-gpts.plus/wp-content/uploads/2026/11/49931-c0d02b9e-768x403.jpg)
![[Bug]: kv_cache_utils warns that prefix-cache reuse is disabled even when disable_eagle_block_drop keeps it working](https://www.chat-gpts.plus/wp-content/uploads/2026/11/55518-05700b36-768x403.jpg)