关于 frappe-errors-serverscripts
面向 Frappe 开发者的 Server Script 错误诊断指南,涵盖沙箱限制、脚本类型选择及常见运行问题的处理方法。
主要功能
- 01按错误信息定位原因与修复方式
- 02说明受限 Python 沙箱规则
- 03区分各脚本类型的上下文
- 04提供调度脚本与 API 防错要点
- 05涵盖 SQL 注入防范
适用场景
- 01排查 Server Script 导入或名称错误
- 02修正调度脚本未提交或超时问题
- 03诊断 API 响应为空的原因
- 04检查 SQL 查询中的用户输入风险
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ impertio-studio/ frappe_claude_skill_package --skill frappe-errors-serverscripts安装后在 Claude Code 中输入/frappe-errors-serverscripts确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/anti-patterns.mdexamples.mdpatterns.md详细介绍
服务端脚本错误 — 诊断与解决
交叉引用:frappe-syntax-serverscripts(语法)、frappe-impl-serverscripts(工作流)、frappe-errors-clientscripts(客户端)。
严重:默认禁用服务端脚本 [v15+]
从 Frappe v15 开始,服务端脚本默认禁用。你必须启用它们:
# In site_config.json
{ "server_script_enabled": 1 }
在 Frappe Cloud 上:服务端脚本仅适用于私有 bench,不适用于共享 bench。
错误诊断流程图
ERROR IN SERVER SCRIPT
│
├─► ImportError / NameError
│ ├─► "import json" → BLOCKED. Use frappe.parse_json()
│ ├─► "import datetime" → BLOCKED. Use frappe.utils
│ ├─► "import os/sys/subprocess" → BLOCKED. Security restriction
│ └─► "NameError: name 'dict' is not defined" → Some builtins restricted
│
├─► SyntaxError: not allowed
│ ├─► "try/except" → BLOCKED by RestrictedPython [v14-v15]
│ ├─► "raise ValueError" → BLOCKED. Use frappe.throw()
│ └─► "exec/eval" → BLOCKED. Security restriction
│
├─► Script runs but nothing happens
│ ├─► Wrong Script Type selected → Check Document Event vs API vs Scheduler
│ ├─► Wrong DocType selected → Verify exact DocType name
│ ├─► Wrong Event selected → Before Save ≠ After Save
│ └─► Script disabled → Check "Enabled" checkbox
│
├─► 403 Permission Denied
│ ├─► Scheduler script → Runs as Administrator, check role permissions
│ ├─► API script → Check Allow Guest setting
│ └─► doc_event → User lacks DocType permission
│
├─► Data not saved in Scheduler
│ └─► Missing frappe.db.commit() → REQUIRED in scheduler scripts
│
└─► API script returns empty/wrong response
└─► Not setting frappe.response["message"] → ALWAYS set response
错误消息 → 原因 → 修复方法表
| 错误消息 | 原因 | 修复方法 |
|---|---|---|
ImportError: import not allowed | 沙盒中的任何 import 语句 | 使用 frappe.utils、frappe.parse_json() 等 |
NameError: name 'dict' is not defined | RestrictedPython 禁用了部分 Python 内置函数 | 使用 frappe._dict() 或字面量 {} |
SyntaxError: try/except not allowed | RestrictedPython 禁用了异常处理 [v14-v15] | 改用条件检查(if/else) |
SyntaxError: raise not allowed | RestrictedPython 禁用了 raise | 使用 frappe.throw() |
Script not executing | 选择了错误的脚本类型或事件 | 确认类型匹配:Document Event、API 或 Scheduler |
doc is not defined | 在 API 或 Scheduler 脚本中使用了 doc(没有文档上下文) | doc 仅在 Document Event 脚本中可用 |
Scheduler 中出现 PermissionError | Scheduler 以 Administrator 身份运行,但脚本访问了受限资源 | 适当使用 ignore_permissions=True |
| Scheduler 中的更改未保存 | 缺少 frappe.db.commit() | Scheduler 脚本中必须调用 frappe.db.commit() |
API returns empty response | 忘记设置 frappe.response["message"] | 必须设置 frappe.response["message"] = result |
Timeout / killed | 无限循环或处理的记录过多 | 查询必须始终添加 limit,并始终使用批处理 |
ValidationError: qty is required | 在 Before Save 中调用了 doc.save()(递归) | 切勿在 Before Save 中调用 doc.save();只需设置值 |
SQL injection via string format | SQL 中的用户输入未转义 | 必须使用 frappe.db.escape() 或参数化查询 |
第一大错误:ImportError
每个初学者都会遇到这个问题。 服务端脚本沙盒会阻止所有导入,json 除外。
# ❌ BLOCKED — These ALL fail with ImportError
import json # Use frappe.parse_json() / frappe.as_json()
from datetime import datetime # Use frappe.utils.now(), frappe.utils.today()
import re # Not available in sandbox
import os # Security: blocked
import requests # Use frappe.make_get_request(), frappe.make_post_request()
# ✅ CORRECT — Sandbox equivalents
data = frappe.parse_json(doc.json_field) # Instead of json.loads()
today = frappe.utils.today() # Instead of datetime.date.today()
now = frappe.utils.now() # Instead of datetime.now()
diff = frappe.utils.date_diff(date1, date2) # Instead of timedelta
resp = frappe.make_get_request("https://api.com") # Instead of requests.get()
resp = frappe.make_post_request("https://api.com", data=payload)
可用沙盒 API(完整参考)
| 类别 | 可用方法 |
|---|---|
| 文档 | frappe.get_doc()、frappe.new_doc()、frappe.get_last_doc()、frappe.get_cached_doc()、frappe.get_mapped_doc()、frappe.rename_doc()、frappe.delete_doc() |
| 数据库 | frappe.db.get_list()、frappe.db.get_all()、frappe.db.get_value()、frappe.db.get_single_value()、frappe.db.set_value()、frappe.db.exists()、frappe.db.sql()、frappe.db.commit()、frappe.db.rollback()、frappe.db.escape() |
| 查询构建器 | frappe.qb(完整查询构建器) |
| HTTP | frappe.make_get_request()、frappe.make_post_request()、frappe.make_put_request() |
| 实用工具 | frappe.utils.*(所有实用工具函数)、frappe.parse_json()、frappe.as_json() |
| 用户/会话 | frappe.session.user、frappe.get_roles()、frappe.has_permission() |
| 消息 | frappe.throw()、frappe.msgprint()、frappe.log_error()、frappe.sendmail() |
| 模块 | json(唯一可导入的模块) |
脚本类型选择错误
务必确认选择了正确的脚本类型:
| 脚本类型 | 触发条件 | 有 doc? | 有 frappe.form_dict? | 自动提交? |
|---|---|---|---|---|
| Document Event | DocType 生命周期(Before Save、After Save 等) | 是 | 否 | 是 |
| API | 对 /api/method/{method_name} 的 HTTP 请求 | 否 | 是 | 是 |
| Scheduler Event | Cron 计划 | 否 | 否 | 否 — 必须调用 frappe.db.commit() |
| Permission Query | 对 DocType 的每次列表查询 | 否 | 否(有 user) | 不适用 |
常见错误:事件类型不正确
# ❌ WRONG — "After Save" cannot prevent save
# Script Type: Document Event, Event: After Save
if not doc.customer:
frappe.throw("Customer is required") # Document already saved!
# ✅ CORRECT — Use "Before Save" or "Before Validate"
# Script Type: Document Event, Event: Before Save
if not doc.customer:
frappe.throw("Customer is required") # Prevents save
沙箱应对方法
try/except 被禁用:改用条件检查
# ❌ BLOCKED in sandbox
try:
customer = frappe.get_doc("Customer", doc.customer)
except Exception:
frappe.throw("Customer not found")
# ✅ CORRECT — Check first, then access
if not frappe.db.exists("Customer", doc.customer):
frappe.throw(f"Customer '{doc.customer}' not found")
customer = frappe.get_doc("Customer", doc.customer)
raise 被禁用:改用 frappe.throw()
# ❌ BLOCKED
if amount < 0:
raise ValueError("Amount cannot be negative")
# ✅ CORRECT
if amount < 0:
frappe.throw("Amount cannot be negative")
API 脚本中 frappe.throw() 的异常类型
| 异常 | HTTP 代码 | 适用场景 |
|---|---|---|
frappe.ValidationError | 417 | 输入验证失败 |
frappe.PermissionError | 403 | 拒绝访问 |
frappe.DoesNotExistError | 404 | 未找到记录 |
frappe.AuthenticationError | 401 | 尚未登录 |
| (默认,无 exc) | 417 | 一般验证错误 |
# API Script — Correct exception types
if not customer:
frappe.throw("Customer param required", exc=frappe.ValidationError) # 417
if not frappe.db.exists("Customer", customer):
frappe.throw("Customer not found", exc=frappe.DoesNotExistError) # 404
if not frappe.has_permission("Customer", "read", customer):
frappe.throw("Access denied", exc=frappe.PermissionError) # 403
Scheduler 脚本:关键错误
# ❌ WRONG — No limit, no commit, no error logging
invoices = frappe.get_all("Sales Invoice", filters={"status": "Unpaid"})
for inv in invoices:
frappe.db.set_value("Sales Invoice", inv.name, "reminder_sent", 1)
# ✅ CORRECT — Limit, batch commit, error logging
BATCH_SIZE = 50
invoices = frappe.get_all(
"Sales Invoice",
filters={"status": "Unpaid", "docstatus": 1},
fields=["name", "customer"],
limit=500 # ALWAYS limit
)
errors = []
for i in range(0, len(invoices), BATCH_SIZE):
batch = invoices[i:i + BATCH_SIZE]
for inv in batch:
if not frappe.db.exists("Customer", inv.customer):
errors.append(f"{inv.name}: Customer not found")
continue
frappe.db.set_value("Sales Invoice", inv.name, "reminder_sent", 1)
frappe.db.commit() # REQUIRED
if errors:
frappe.log_error("\n".join(errors), "Reminder Errors")
frappe.db.commit()
防止 SQL 注入
# ❌ VULNERABLE — String interpolation with user input
territory = frappe.form_dict.get("territory")
conditions = f"`tabCustomer`.territory = '{territory}'" # SQL INJECTION!
# ✅ SAFE — Use frappe.db.escape()
territory = frappe.form_dict.get("territory")
conditions = f"`tabCustomer`.territory = {frappe.db.escape(territory)}"
# ✅ SAFEST — Use parameterized query or Query Builder
results = frappe.db.get_all("Customer", filters={"territory": territory})
必须遵守 / 绝对禁止的规则
必须遵守
- 使用
frappe.utils.*,而不是导入 Python 模块 — 只有json模块可以导入 - 使用
frappe.throw(),而不是raise—raise在沙箱中被禁用 - 使用条件检查,而不是
try/except— 异常处理被禁用 [v14-v15] - 在 Scheduler 脚本中调用
frappe.db.commit()— 更改不会自动提交 - 在 Scheduler 脚本的所有查询中添加
limit— 防止内存耗尽 - 在 API 脚本中设置
frappe.response["message"]— 否则响应为空 - 在 SQL 中使用
frappe.db.escape()处理用户输入 — 防止 SQL 注入 - 在 Scheduler 脚本中使用
frappe.log_error()记录错误 — 没有用户能看到错误 - 确认 Script Type 符合你的意图 — Document Event、API 或 Scheduler
绝对禁止
- 绝对不要使用
import语句(json除外)— RestrictedPython 会阻止 - 绝对不要使用
try/except或raise— 沙箱会阻止 [v14-v15] - 绝对不要在 Before Save 中调用
doc.save()— 会导致无限递归 - 绝对不要使用字符串格式化将用户输入拼入 SQL — 有 SQL 注入风险
- 绝对不要在 Scheduler 中处理不限量的记录 — 始终使用
limit - 绝对不要假设 API/Scheduler 脚本中存在
doc— 它仅在 Document Events 中可用 - 绝对不要忘记在 Scheduler 中调用
frappe.db.commit()— 所有更改都会丢失
参考文件
| 文件 | 内容 |
|---|---|
references/examples.md | 带诊断说明的真实错误场景 |
references/anti-patterns.md | 常见沙箱错误及修复方法 |
references/patterns.md | 按脚本类型划分的防御性错误处理模式 |
评论
支持 Markdown