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. 02APIのレート制限や長時間処理に対処
  3. 03Webhook配信とファイルアップロードの問題を調査

インストール

コマンドをコピーしてターミナルで実行します。

$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一意制約に違反している挿入前に既存のレコードを確認します
417ValidationErrorfrappe.throw() が呼び出された検証ロジックまたは入力データを修正します
429RateLimitExceededErrorリクエストが多すぎるRetry-After ヘッダーに従い、リクエストを抑制します
500Exception(未処理)未処理のサーバーエラーError Log を確認し、try/except で囲みます
503—サーバーが過負荷またはメンテナンス中指数バックオフで再試行します

認証エラー(401)

トークン形式が正しくない
Error:  HTTP 401 Unauthorized
Cause:  Using "Bearer api_key:api_secret" instead of "token api_key:api_secret"

Frappe は認証形式を 2 種類使用します。絶対に混在させないでください。

方法ヘッダー形式使用する状況
API Key/SecretAuthorization: token api_key:api_secretサーバー間通信、スクリプト
OAuth BearerAuthorization: Bearer access_tokenOAuth 2.0 フロー
セッション 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 トークンは不要です。
  • CSRF トークンは JavaScript の frappe.csrf_token で利用できるほか、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. Error Log で 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.mdAPI のエラー処理でよくある 15 の間違い

関連項目

  • frappe-core-api — API の実装パターン
  • frappe-errors-permissions — 権限エラーの処理(403 の詳細解説)
  • frappe-syntax-whitelisted — ホワイトリスト登録済みメソッドの構文
  • frappe-errors-serverscripts — Server Script のエラー処理

コメント