mcpField
F
frappe-errors-api

frappe-errors-api

github.com/impertio-studio开发198113 次安装30 周前更新
代码仓库

关于 frappe-errors-api

面向开发者的 Frappe API 错误排查参考技能,将 HTTP 状态码与常见原因对应,并涵盖身份验证、权限、请求、Webhook 和客户端错误处理。

主要功能

  1. 01HTTP 状态码与异常对照
  2. 02身份验证和 CSRF 问题排查
  3. 03验证与权限错误处理指引
  4. 04速率限制和超时处理模式
  5. 05Webhook 与文件上传诊断

适用场景

  1. 01排查身份验证或权限失败
  2. 02处理 API 速率限制和长时间运行操作
  3. 03排查 Webhook 投递及文件上传问题

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/impertio-studio/frappe_claude_skill_package --skill frappe-errors-api

安装后在 Claude Code 中输入/frappe-errors-api确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
anti-patterns.md
examples.md
patterns.md

详细介绍

API 错误处理

有关 API 实现模式,请参阅 frappe-core-api。有关权限错误,请参阅 frappe-errors-permissions。


HTTP 状态码映射:错误 -> 原因 -> 修复方法

代码Frappe 异常发生时机修复方法
200—成功—
401AuthenticationError令牌错误或过期、格式错误检查 Authorization: token key:secret 或 Bearer access_token
403PermissionError缺少 @whitelist、无角色或未设置 allow_guest添加装饰器或授予权限
404DoesNotExistErrorURL 错误、文档不存在或端点路径拼写错误验证 /api/resource/:doctype/:name 或 /api/method/dotted.path
409DuplicateEntryError违反唯一约束插入前检查是否已有记录
417ValidationError调用了 frappe.throw()修复验证逻辑或输入数据
429RateLimitExceededError请求过多遵循 Retry-After 标头;限制请求频率
500Exception(未处理)未处理的服务器错误检查错误日志;使用 try/except 包装
503—服务器过载 / 维护中使用指数退避重试

身份验证错误(401)

令牌格式错误
Error:  HTTP 401 Unauthorized
Cause:  Using "Bearer api_key:api_secret" instead of "token api_key:api_secret"

Frappe 使用两种身份验证格式——切勿混用:

方法标头格式适用场景
API Key/SecretAuthorization: token api_key:api_secret服务器到服务器通信、脚本
OAuth BearerAuthorization: Bearer access_tokenOAuth 2.0 流程
Session Cookie来自 /api/method/login 的 Cookie基于浏览器的应用
# WRONG — Bearer with API key:secret
headers = {"Authorization": f"Bearer {api_key}:{api_secret}"}

# CORRECT — token keyword for API key:secret
headers = {"Authorization": f"token {api_key}:{api_secret}"}

# CORRECT — Bearer for OAuth access tokens only
headers = {"Authorization": f"Bearer {oauth_access_token}"}
OAuth 令牌过期
Error:  HTTP 401 after token was working
Cause:  OAuth access_token expired
Fix:    Use refresh_token to get new access_token
def get_fresh_token(settings):
    """ALWAYS implement token refresh for OAuth integrations."""
    if is_token_expired(settings.token_expiry):
        response = requests.post(f"{settings.base_url}/api/method/frappe.integrations.oauth2.get_token", data={
            "grant_type": "refresh_token",
            "refresh_token": settings.get_password("refresh_token"),
            "client_id": settings.client_id,
        })
        if response.status_code == 200:
            data = response.json()
            settings.access_token = data["access_token"]
            settings.token_expiry = frappe.utils.add_to_date(None, seconds=data["expires_in"])
            settings.save(ignore_permissions=True)
        else:
            frappe.throw(_("OAuth token refresh failed"), exc=frappe.AuthenticationError)
    return settings.access_token

禁止访问错误(403)

缺少 @frappe.whitelist()
Error:  HTTP 403 on /api/method/myapp.api.my_function
Cause:  Function exists but lacks @frappe.whitelist() decorator
Fix:    Add decorator — without it, NO external call is allowed
# WRONG — Callable internally but returns 403 via REST
def my_function(name):
    return frappe.get_doc("Item", name)

# CORRECT — Exposed to authenticated users
@frappe.whitelist()
def my_function(name):
    return frappe.get_doc("Item", name)

# CORRECT — Exposed to everyone including unauthenticated
@frappe.whitelist(allow_guest=True)
def public_function():
    return {"status": "ok"}
公开端点缺少 allow_guest
Error:  HTTP 403 for unauthenticated requests
Cause:  @frappe.whitelist() without allow_guest=True
Fix:    Add allow_guest=True — but ALWAYS validate inputs

切勿在未验证输入的情况下使用 allow_guest=True——这些端点会暴露在互联网上。


未找到错误(404)

常见 URL 错误
错误 URL正确 URL问题
/api/resource/SalesOrder/SO-001/api/resource/Sales Order/SO-001DocType 名称中有空格
/api/method/myapp.my_function/api/method/myapp.api.my_function缺少模块路径
/api/resource/sales_order/api/resource/Sales Order大小写错误 / 使用了下划线
/api/v2/document/Item/ITEM-001 [v14]/api/resource/Item/ITEM-001v2 API 仅适用于 v15+
# ALWAYS URL-encode DocType names with spaces
import urllib.parse
url = f"/api/resource/{urllib.parse.quote('Sales Order')}/{name}"

验证错误(417)

每次调用 frappe.throw() 默认都会返回 HTTP 417(除非指定了特定的异常类)。

# Returns 417 — generic validation error
frappe.throw(_("Amount must be positive"))

# Returns 417 — with explicit ValidationError type
frappe.throw(_("Amount must be positive"), exc=frappe.ValidationError)

# Returns 403 — PermissionError overrides to 403
frappe.throw(_("Access denied"), exc=frappe.PermissionError)

# Returns 404 — DoesNotExistError overrides to 404
frappe.throw(_("Not found"), exc=frappe.DoesNotExistError)

务必使用特定异常类,以便客户端能够正确处理错误类型:

# WRONG — all errors look the same to the client
frappe.throw(_("Customer not found"))  # 417, generic

# CORRECT — client can distinguish 404 from validation error
frappe.throw(_("Customer not found"), exc=frappe.DoesNotExistError)  # 404

CSRF 令牌错误

Error:  HTTP 403 "CSRF token missing or invalid"
Cause:  POST/PUT/DELETE request without X-Frappe-CSRF-Token header

规则:

  • 基于会话(Cookie)的身份验证务必包含 X-Frappe-CSRF-Token 标头。
  • 基于令牌的身份验证(Authorization: token ...)不需要 CSRF 令牌。
  • OAuth Bearer 身份验证不需要 CSRF 令牌。
  • 在 JavaScript 中可通过 frappe.csrf_token 获取 CSRF 令牌,也可通过 window.CSRF_TOKEN 嵌入页面。
// Browser-side: ALWAYS include CSRF for session-based requests
fetch("/api/method/myapp.api.update", {
    method: "POST",
    headers: {
        "Content-Type": "application/json",
        "X-Frappe-CSRF-Token": frappe.csrf_token
    },
    body: JSON.stringify({data: "value"})
});

CORS 错误

Error:  "Access-Control-Allow-Origin" header missing
Cause:  Cross-origin request not configured in site_config.json
// site_config.json — NEVER use "*" in production
{
    "allow_cors": "https://your-frontend.example.com"
}

配置多个来源时 [v15+]:

{
    "allow_cors": ["https://app1.example.com", "https://app2.example.com"]
}

速率限制错误(429)

Error:  HTTP 429 Too Many Requests
Cause:  Exceeded rate limit configured in site_config.json or hooks.py
# hooks.py — rate limiting on whitelisted methods [v14+]
rate_limit = {"myapp.api.heavy_endpoint": {"limit": 10, "seconds": 60}}

调用外部 API 时务必处理 429:

def call_with_rate_limit(url, data):
    response = requests.post(url, json=data, timeout=30)
    if response.status_code == 429:
        wait = int(response.headers.get("Retry-After", 60))
        time.sleep(min(wait, 120))  # Cap at 2 minutes
        response = requests.post(url, json=data, timeout=30)
    response.raise_for_status()
    return response.json()

文件上传错误

Error:  HTTP 500 on /api/method/upload_file
Cause:  Wrong content type, file too large, or missing file field
# CORRECT file upload via REST API
import requests

response = requests.post(
    f"{base_url}/api/method/upload_file",
    headers={"Authorization": f"token {api_key}:{api_secret}"},
    files={"file": ("document.pdf", open("document.pdf", "rb"), "application/pdf")},
    data={
        "doctype": "Sales Invoice",
        "docname": "SINV-001",
        "is_private": 1  # 1 = private, 0 = public
    },
    timeout=60  # ALWAYS set timeout for uploads
)

常见上传失败原因:

  • Content-Type 必须为 multipart/form-data(使用 files= 参数时会自动设置)
  • 文件上传时切勿设置 Content-Type: application/json
  • 检查 site_config.json 中的 max_file_size(默认 10MB)
  • [v15+] allowed_file_extensions 会限制文件类型

JSON 解析错误

Error:  "Failed to decode JSON" or unexpected behavior
Cause:  API arguments sent as JSON string instead of parsed object
@frappe.whitelist()
def update_items(items):
    # ALWAYS handle both string and parsed input
    if isinstance(items, str):
        try:
            items = frappe.parse_json(items)
        except Exception:
            frappe.throw(_("Invalid JSON format"), exc=frappe.ValidationError)

    if not isinstance(items, (list, dict)):
        frappe.throw(_("Expected list or dict"), exc=frappe.ValidationError)

Webhook 投递失败

Error:  Webhook not firing or returning errors
Cause:  Target URL unreachable, wrong format, or timeout

调试检查清单:

  1. 检查错误日志中的 Webhook 投递错误
  2. 验证服务器能否访问目标 URL
  3. 检查 Webhook 条件——它是否筛除了该事件?
  4. [v15+] 检查 Webhook Request Log 中的投递状态
# Custom webhook with error handling
@frappe.whitelist(allow_guest=True)
def incoming_webhook():
    """Handle incoming webhook with validation."""
    payload = frappe.request.data
    signature = frappe.request.headers.get("X-Webhook-Signature")

    if not verify_signature(payload, signature):
        frappe.local.response["http_status_code"] = 401
        return {"error": "Invalid signature"}

    try:
        data = frappe.parse_json(payload)
    except Exception:
        frappe.local.response["http_status_code"] = 400
        return {"error": "Invalid JSON payload"}

    # ALWAYS return 200 quickly to prevent sender retries
    frappe.enqueue(process_webhook_data, data=data, queue="short")
    return {"status": "accepted"}

长时间操作超时

Error:  HTTP 504 Gateway Timeout or connection reset
Cause:  Operation takes longer than proxy/server timeout (typically 60s)

修复方法:使用后台作业处理长时间操作:

@frappe.whitelist()
def start_long_operation(filters):
    """NEVER run long operations synchronously in API calls."""
    job_id = frappe.generate_hash(length=10)

    frappe.enqueue(
        "myapp.tasks.run_long_operation",
        queue="long",
        timeout=600,
        job_id=job_id,
        filters=filters
    )

    return {"status": "queued", "job_id": job_id}

@frappe.whitelist()
def check_job_status(job_id):
    """Poll for job completion."""
    from frappe.utils.background_jobs import get_info
    jobs = get_info()
    for job in jobs:
        if job.get("job_id") == job_id:
            return {"status": job.get("status", "unknown")}
    return {"status": "completed"}

服务端错误模式(标准)

@frappe.whitelist()
def safe_api_endpoint(docname, action):
    """ALWAYS follow: validate -> check permission -> execute -> handle errors."""

    # 1. Validate input
    if not docname:
        frappe.throw(_("Document name required"), exc=frappe.ValidationError)

    # 2. Check existence
    if not frappe.db.exists("My DocType", docname):
        frappe.throw(_("Document not found"), exc=frappe.DoesNotExistError)

    # 3. Check permission
    frappe.has_permission("My DocType", "write", docname, throw=True)

    # 4. Execute with error handling
    try:
        doc = frappe.get_doc("My DocType", docname)
        result = doc.run_method(action)
        return {"status": "success", "data": result}

    except frappe.ValidationError:
        raise  # Let Frappe handle — returns 417
    except frappe.PermissionError:
        raise  # Let Frappe handle — returns 403
    except Exception:
        frappe.log_error(frappe.get_traceback(), f"API Error: {docname}")
        frappe.throw(_("Operation failed. Please try again."))

客户端错误处理

// ALWAYS handle errors in frappe.call
frappe.call({
    method: "myapp.api.safe_api_endpoint",
    args: {docname: "DOC-001", action: "approve"},
    freeze: true,
    freeze_message: __("Processing..."),
    callback: function(r) {
        if (r.message && r.message.status === "success") {
            frappe.show_alert({message: __("Done"), indicator: "green"});
        }
    },
    error: function(r) {
        // ALWAYS check exc_type for specific handling
        if (r.exc_type === "PermissionError") {
            frappe.msgprint(__("You lack permission for this action."));
        } else if (r.exc_type === "DoesNotExistError") {
            frappe.msgprint(__("Record not found."));
        } else if (!r.status) {
            frappe.msgprint(__("Network error. Check your connection."));
        }
    }
});

关键规则

务必
  1. 在 frappe.throw() 中使用特定的异常类 — 以启用正确的 HTTP 状态码
  2. 为所有外部请求设置超时 — requests.get(url, timeout=30)
  3. 处理前验证所有输入 — 任何已登录用户都可以调用白名单方法
  4. 抛出错误前记录日志 — 先调用 frappe.log_error(),再调用 frappe.throw()
  5. 在每个 frappe.call() 中处理错误回调 — 静默失败会让用户困惑
  6. 对耗时超过 30 秒的操作使用后台作业
  7. 让传入的 Webhook 尽快返回 200,然后异步处理
切勿
  1. 向用户暴露内部错误 — 记录回溯信息,并显示友好提示
  2. 混用令牌格式 — token key:secret 与 Bearer oauth_token
  3. 重试 4xx 错误(429 除外) — 这类错误表示客户端存在问题,而非暂时性故障
  4. 对基于会话的 POST 请求省略 CSRF 令牌 — 会导致 403
  5. 为文件上传设置 Content-Type: application/json — 必须使用 multipart/form-data
  6. 捕获异常却不记录日志 — 这会导致无法调试生产环境问题
  7. 将 API 凭据硬编码 — 使用 DocType 中的 settings.get_password("field")

参考文件

文件内容
references/patterns.md完整的白名单方法、Webhook 和外部 API 模式
references/examples.md可完整运行的 API 模块、客户端集成和外部 API 客户端
references/anti-patterns.md15 个常见的 API 错误处理误区

另请参阅

  • frappe-core-api — API 实现模式
  • frappe-errors-permissions — 权限错误处理(深入解析 403)
  • frappe-syntax-whitelisted — 白名单方法语法
  • frappe-errors-serverscripts — Server Script 错误处理

评论

支持 Markdown