关于 frappe-impl-whitelisted
面向 Frappe 开发者的 whitelisted API 实现指南,涵盖端点设计、权限、安全和错误处理,并提供 v14、v15、v16 的版本差异。
主要功能
- 01端点类型与权限模型选择
- 02认证及访客 API 实现流程
- 03控制器方法与文件上传端点
- 04后台任务和错误处理模式
- 05客户端调用及外部集成示例
- 06v14–v16 功能差异说明
适用场景
- 01规划公开或登录用户 API
- 02为 DocType 表单实现控制器方法
- 03审核访客端点的输入与限流
- 04将 Server Script API 迁移为 whitelisted 方法
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ impertio-studio/ frappe_claude_skill_package --skill frappe-impl-whitelisted安装后在 Claude Code 中输入/frappe-impl-whitelisted确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/anti-patterns.mddecision-tree.mdexamples.mdworkflows.md详细介绍
Frappe 白名单方法实现工作流
用于构建 API 端点的分步工作流。装饰器语法请参阅 frappe-syntax-whitelisted。
版本:v14/v15/v16(已注明版本专属功能)
决策总览:端点类型是什么?
WHAT ARE YOU BUILDING?
│
├─► Public API (no login required)?
│ └─► allow_guest=True + STRICT input validation + rate limiting
│
├─► Authenticated API for logged-in users?
│ └─► Default @frappe.whitelist() + document permission checks
│
├─► Admin-only API?
│ └─► frappe.only_for("System Manager")
│
├─► Document-specific method (called from form)?
│ └─► Controller method + frm.call() from JS
│
├─► Standalone utility API?
│ └─► Separate api.py + frappe.call() from JS
│
├─► External webhook receiver?
│ └─► allow_guest=True + signature verification
│
└─► Background job trigger?
└─► Authenticated API that calls frappe.enqueue()
工作流 1:设计端点
步骤 1:选择位置
WHERE SHOULD THE CODE LIVE?
│
├─► Related to a DocType, called from its form?
│ └─► doctype/xxx/xxx.py (controller method)
│ Client: frm.call('method_name', args)
│
├─► Related to a DocType, standalone?
│ └─► doctype/xxx/xxx_api.py or myapp/api/module.py
│ Client: frappe.call('myapp.api.module.method')
│
├─► General app utility?
│ └─► myapp/api.py (small app) or myapp/api/module.py (large app)
│
└─► External integration?
└─► myapp/integrations/service_name.py
步骤 2:选择权限模型
WHO CAN CALL THIS API?
│
├─► Anyone (public) → allow_guest=True
│ ⚠️ MUST validate ALL input, sanitize for XSS, rate limit
│
├─► Any logged-in user → Default (no allow_guest)
│ Still check document permissions per record!
│
├─► Specific role(s) → frappe.only_for("Role")
│
└─► Document-level → frappe.has_permission(doctype, ptype, doc)
步骤 3:选择 HTTP 方法
WHAT DOES THE API DO?
│
├─► Read-only → methods=["GET"]
├─► Creates/modifies data → methods=["POST"]
└─► Both or default → omit methods parameter (all allowed)
工作流 2:实现经过身份验证的 API
分步指南
步骤 1:创建函数
# myapp/api.py
import frappe
from frappe import _
@frappe.whitelist()
def get_customer_balance(customer):
"""Get outstanding balance for a customer."""
# 1. Permission check
if not frappe.has_permission("Customer", "read", customer):
frappe.throw(_("Not permitted"), frappe.PermissionError)
# 2. Validate input
if not customer or not frappe.db.exists("Customer", customer):
frappe.throw(_("Customer not found"), frappe.DoesNotExistError)
# 3. Fetch and return
balance = frappe.db.sql("""
SELECT COALESCE(SUM(outstanding_amount), 0)
FROM `tabSales Invoice`
WHERE customer = %s AND docstatus = 1
""", customer)[0][0]
return {"customer": customer, "balance": balance}
步骤 2:从客户端脚本调用
frappe.call({
method: 'myapp.api.get_customer_balance',
args: { customer: 'CUST-00001' },
callback(r) {
if (r.message) console.log(r.message.balance);
}
});
步骤 3:使用 curl 测试
# Authenticate first
curl -X POST https://site.com/api/method/login \
-d 'usr=admin&pwd=password'
# Call the API
curl -X POST https://site.com/api/method/myapp.api.get_customer_balance \
-H "Content-Type: application/json" \
-d '{"customer": "CUST-00001"}' \
--cookie cookies.txt
# Or use token auth
curl -X POST https://site.com/api/method/myapp.api.get_customer_balance \
-H "Authorization: token api_key:api_secret" \
-H "Content-Type: application/json" \
-d '{"customer": "CUST-00001"}'
工作流 3:实现公开(访客)API
分步指南
步骤 1:创建并进行严格验证
@frappe.whitelist(allow_guest=True, methods=["POST"])
def submit_inquiry(name, email, phone=None, message=None):
"""Public contact form — strict validation required."""
# 1. Validate required fields
if not all([name, email]):
frappe.throw(_("Name and email are required"))
# 2. Validate email format
if not frappe.utils.validate_email_address(email):
frappe.throw(_("Invalid email address"))
# 3. Sanitize ALL input
name = frappe.utils.strip_html(name)[:100]
email = email.strip().lower()[:200]
phone = frappe.utils.strip_html(phone)[:20] if phone else None
message = frappe.utils.strip_html(message)[:2000] if message else None
# 4. Create record with ignore_permissions
lead = frappe.get_doc({
"doctype": "Lead",
"lead_name": name, "email_id": email,
"phone": phone, "notes": message, "source": "Website"
})
lead.insert(ignore_permissions=True)
return {"success": True, "message": _("Thank you")}
步骤 2:添加速率限制(v15+)
from frappe.rate_limiter import rate_limit
@frappe.whitelist(allow_guest=True, methods=["POST"])
@rate_limit(limit=5, seconds=60) # 5 calls per minute
def submit_inquiry(name, email, phone=None, message=None):
...
访客 API 的关键规则
- 始终验证并清理每个输入参数
- 始终为写入数据的端点使用
methods=["POST"] - 始终添加速率限制(v15+ 使用装饰器;v14 使用基于缓存的手动限流)
- 绝不暴露内部错误详情——使用
frappe.log_error()记录日志,并显示通用消息 - 绝不返回敏感数据(内部 ID、文件路径、堆栈跟踪)
- 绝不将原始用户输入传递给
frappe.get_doc()——应使用显式字段映射
工作流 4:实现控制器方法
分步指南
步骤 1:将方法添加到 DocType 控制器
# myapp/doctype/sales_order/sales_order.py
class SalesOrder(Document):
@frappe.whitelist()
def calculate_shipping(self, carrier):
"""Called from form via frm.call()."""
if not self.shipping_address:
frappe.throw(_("Shipping address required"))
rate = get_shipping_rate(self.shipping_address, carrier)
return {"carrier": carrier, "rate": rate}
步骤 2:从表单 JS 调用
frm.call('calculate_shipping', {
carrier: 'FedEx'
}).then(r => {
frm.set_value('shipping_amount', r.message.rate);
});
关键区别:通过 frm.call() 调用控制器方法时,Frappe 会自动检查文档权限。无需手动检查文档本身的权限。
工作流 5:实现错误处理
标准模式
@frappe.whitelist()
def process_payment(invoice, amount):
try:
# Validate
if not invoice:
frappe.throw(_("Invoice required"), frappe.ValidationError)
if not frappe.has_permission("Sales Invoice", "write", invoice):
frappe.throw(_("Not permitted"), frappe.PermissionError)
# Process
result = do_payment(invoice, float(amount))
return {"success": True, "data": result}
except (frappe.ValidationError, frappe.PermissionError):
raise # Let Frappe handle (417 / 403)
except frappe.DoesNotExistError:
frappe.throw(_("Not found"), frappe.DoesNotExistError) # 404
except Exception:
frappe.log_error(frappe.get_traceback(), "Payment Error")
frappe.local.response["http_status_code"] = 500
return {"success": False, "error": _("Internal error")}
HTTP 状态码参考
| 代码 | Frappe 异常 | 适用情况 |
|---|---|---|
| 200 | — | 成功 |
| 403 | PermissionError | 拒绝访问 |
| 404 | DoesNotExistError | 未找到 |
| 409 | DuplicateEntryError | 重复 |
| 417 | ValidationError | 验证失败 |
| 429 | — | 超出速率限制(v15+) |
| 500 | Exception | 服务器错误 |
工作流 6:文件上传端点
@frappe.whitelist()
def upload_attachment(doctype, docname):
"""Handle file upload attached to a document."""
if not frappe.has_permission(doctype, "write", docname):
frappe.throw(_("Not permitted"), frappe.PermissionError)
file = frappe.request.files.get('file')
if not file:
frappe.throw(_("No file provided"))
file_doc = frappe.get_doc({
"doctype": "File",
"file_name": file.filename,
"attached_to_doctype": doctype,
"attached_to_name": docname,
"content": file.read(),
"is_private": 1
})
file_doc.insert(ignore_permissions=True)
return {"file_url": file_doc.file_url}
工作流 7:后台任务端点
@frappe.whitelist()
def start_heavy_export(doctype, filters=None):
"""Trigger a background job — returns immediately."""
frappe.only_for("System Manager")
frappe.enqueue(
"myapp.tasks.export_data",
queue="long",
timeout=1500,
doctype=doctype,
filters=filters,
user=frappe.session.user
)
return {"status": "queued", "message": _("Export started")}
客户端集成模式
带选项的 frappe.call()
frappe.call({
method: 'myapp.api.my_method',
args: { param: 'value' },
freeze: true,
freeze_message: __('Processing...'),
callback(r) { console.log(r.message); },
error(r) { frappe.msgprint(__('Error')); }
});
Async/Await 模式
const r = await frappe.call({
method: 'myapp.api.get_data',
args: { id: 123 }
});
console.log(r.message);
来自外部系统的 REST API
# Token auth (recommended for integrations)
curl -H "Authorization: token api_key:api_secret" \
https://site.com/api/method/myapp.api.method
# Bearer auth (OAuth)
curl -H "Authorization: Bearer access_token" \
https://site.com/api/method/myapp.api.method
安全规则(始终/绝不)
- 始终在访问任何文档前检查权限
- 始终使用参数化查询——绝不在 SQL 中使用 f 字符串
- 始终验证并清理访客 API 的输入
- 始终在使用
ignore_permissions=True前通过frappe.only_for()检查角色 - 绝不暴露内部错误——记录详情并返回通用消息
- 绝不对会修改数据的端点使用
methods=["GET"] - 绝不信任访客 API 提供的
data字典——应使用显式参数名 - 始终在浏览器发出的 fetch() 调用中包含
X-Frappe-CSRF-Token标头
迁移:从 Server Script API 迁移到白名单方法
| 步骤 | 操作 |
|---|---|
| 1 | 将 Server Script 逻辑复制到 myapp/api/module.py |
| 2 | 使用相同的权限模型添加 @frappe.whitelist() 装饰器 |
| 3 | 将所有 frappe.call() 引用更新为新的点分路径 |
| 4 | 禁用或删除 Server Script |
| 5 | 运行 bench --site sitename migrate |
版本差异
| 功能 | v14 | v15 | v16 |
|---|---|---|---|
| @frappe.whitelist() | 是 | 是 | 是 |
| allow_guest | 是 | 是 | 是 |
| methods 参数 | 是 | 是 | 是 |
| 类型注解验证 | 否 | 是 | 是 |
| @rate_limit 装饰器 | 否 | 是 | 是 |
| API v2 端点 | 否 | 是 | 是 |
v15+ 类型验证
@frappe.whitelist()
def typed_api(customer: str, limit: int = 10) -> dict:
"""v15+ validates types from annotations automatically."""
return {"customer": customer, "limit": limit}
参考文件
| 文件 | 内容 |
|---|---|
| decision-tree.md | 完整的 API 类型与权限选择指南 |
| workflows.md | 分步实现模式(10 多种工作流) |
| examples.md | 可用于生产环境的代码示例 |
评论
支持 Markdown