frappe-errors-serverscripts について
Frappe の Server Script を扱う開発者向けのトラブルシューティングガイドです。よくあるエラーの原因と対処法に加え、サンドボックスの制約やスクリプト種別ごとの動作を説明します。
主な機能
- 01エラー別の原因と対処法
- 02RestrictedPython の制約説明
- 03スクリプト種別ごとのコンテキスト比較
- 04スケジューラーと API の注意点
- 05SQL インジェクション対策
活用シーン
- 01Server Script の import や名前エラーの診断
- 02スケジューラーのコミットやタイムアウト問題の修正
- 03API の空レスポンスの原因調査
- 04SQL クエリにおけるユーザー入力の確認
インストール
コマンドをコピーしてターミナルで実行します。
$
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 では、サーバースクリプトはプライベートベンチでのみ利用でき、共有ベンチでは利用できません。
エラー診断フローチャート
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 | Script Type または Event の選択が間違っています | Document Event、API、Scheduler のいずれかに一致することを確認します |
doc is not defined | API または Scheduler スクリプトで doc を使用しています(ドキュメントコンテキストがありません) | doc は Document Event スクリプトでのみ利用できます |
Scheduler での PermissionError | Scheduler は Administrator として実行されますが、スクリプトが制限されたリソースにアクセスしています | 適切な場合は ignore_permissions=True を使用します |
Scheduler での Changes not saved | 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(インポート可能な唯一のモジュール) |
スクリプトタイプの選択ミス
正しい Script Type を選択したことを必ず確認してください。
| Script Type | トリガー | doc あり? | frappe.form_dict あり? | 自動コミット? |
|---|---|---|---|---|
| Document Event | DocType のライフサイクル(Before Save、After Save など) | YES | NO | YES |
| API | /api/method/{method_name} への HTTP リクエスト | NO | YES | YES |
| Scheduler Event | Cron スケジュール | NO | NO | NO — frappe.db.commit() の呼び出しが必須 |
| Permission Query | DocType に対するすべてのリストクエリ | NO | NO(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
スケジューラースクリプト:重大な間違い
# ❌ 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})
必ず守ること/絶対にしないこと
必ず守ること
- Python のインポートの代わりに
frappe.utils.*を使用する — インポートできるのはjsonモジュールのみです raiseの代わりにfrappe.throw()を使用する —raiseはサンドボックスでブロックされていますtry/exceptの代わりに条件チェックを使用する — 例外処理はブロックされています [v14-v15]- スケジューラースクリプトでは
frappe.db.commit()を呼び出す — 変更は自動コミットされません - スケジューラースクリプトのすべてのクエリに
limitを追加する — メモリの枯渇を防ぎます - API スクリプトで
frappe.response["message"]を設定する — そうしないとレスポンスは空になります - SQL でユーザー入力に
frappe.db.escape()を使用する — SQL インジェクションを防ぎます - スケジューラースクリプトのエラーを
frappe.log_error()でログに記録する — エラーを確認するユーザーがいません - スクリプトタイプが目的に合っていることを確認する — ドキュメントイベント、API、スケジューラー
絶対にしないこと
import文を絶対に使用しない(jsonを除く)— RestrictedPython によってブロックされていますtry/exceptまたはraiseを絶対に使用しない — サンドボックスでブロックされています [v14-v15]- Before Save で
doc.save()を絶対に呼び出さない — 無限再帰が発生します - ユーザー入力を含む SQL に文字列フォーマットを絶対に使用しない — SQL インジェクションのリスクがあります
- スケジューラーでレコードを無制限に処理しない — 必ず
limitを使用してください - API/スケジューラースクリプトで
docが存在すると想定しない — ドキュメントイベントでのみ使用できます - スケジューラーで
frappe.db.commit()を忘れない — すべての変更が失われます
参照ファイル
| ファイル | 内容 |
|---|---|
references/examples.md | 診断付きの実際のエラーシナリオ |
references/anti-patterns.md | 修正方法を含む、よくあるサンドボックスの間違い |
references/patterns.md | スクリプトタイプ別の防御的なエラー処理パターン |
コメント
Markdown 対応