Frappe_Claude_Skill_Package frappe-errors-api
install
source · Clone the upstream repo
git clone https://github.com/OpenAEC-Foundation/Frappe_Claude_Skill_Package
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/OpenAEC-Foundation/Frappe_Claude_Skill_Package "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/source/errors/frappe-errors-api" ~/.claude/skills/openaec-foundation-frappe-claude-skill-package-frappe-errors-api && rm -rf "$T"
manifest:
skills/source/errors/frappe-errors-api/SKILL.mdsource content
API Error Handling
For API implementation patterns see
frappe-core-api. For permission errors see frappe-errors-permissions.
HTTP Status Code Map: Error -> Cause -> Fix
| Code | Frappe Exception | When It Happens | Fix |
|---|---|---|---|
| 200 | — | Success | — |
| 401 | | Bad/expired token, wrong format | Check or |
| 403 | | Missing , no role, no | Add decorator or grant permission |
| 404 | | Wrong URL, doc not found, typo in endpoint path | Verify or |
| 409 | | Unique constraint violated | Check existing records before insert |
| 417 | | called | Fix validation logic or input data |
| 429 | | Too many requests | Respect header; throttle requests |
| 500 | (unhandled) | Unhandled server error | Check Error Log; wrap in try/except |
| 503 | — | Server overloaded / maintenance | Retry with exponential backoff |
Authentication Errors (401)
Wrong Token Format
Error: HTTP 401 Unauthorized Cause: Using "Bearer api_key:api_secret" instead of "token api_key:api_secret"
Frappe uses TWO authentication formats — NEVER mix them:
| Method | Header Format | When to Use |
|---|---|---|
| API Key/Secret | | Server-to-server, scripts |
| OAuth Bearer | | OAuth 2.0 flows |
| Session Cookie | Cookie from | Browser-based apps |
# 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}"}
Expired OAuth Token
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
Forbidden Errors (403)
Missing @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"}
Missing allow_guest for Public Endpoints
Error: HTTP 403 for unauthenticated requests Cause: @frappe.whitelist() without allow_guest=True Fix: Add allow_guest=True — but ALWAYS validate inputs
NEVER use
without input validation — these endpoints are exposed to the internet.allow_guest=True
Not Found Errors (404)
Common URL Mistakes
| Wrong URL | Correct URL | Issue |
|---|---|---|
| | Space in DocType name |
| | Missing module path |
| | Wrong case / underscore |
[v14] | | v2 API only in v15+ |
# ALWAYS URL-encode DocType names with spaces import urllib.parse url = f"/api/resource/{urllib.parse.quote('Sales Order')}/{name}"
Validation Errors (417)
Every
frappe.throw() call returns HTTP 417 by default (unless a specific exception class is provided).
# 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)
ALWAYS use the specific exception class so clients can handle error types correctly:
# 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 Token Errors
Error: HTTP 403 "CSRF token missing or invalid" Cause: POST/PUT/DELETE request without X-Frappe-CSRF-Token header
Rules:
- ALWAYS include
header for session-based (cookie) auth.X-Frappe-CSRF-Token - Token-based auth (
) does NOT require CSRF token.Authorization: token ... - OAuth Bearer auth does NOT require CSRF token.
- The CSRF token is available in
in JavaScript or embedded asfrappe.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 Errors
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" }
For multiple origins [v15+]:
{ "allow_cors": ["https://app1.example.com", "https://app2.example.com"] }
Rate Limit Errors (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}}
ALWAYS handle 429 in external API calls:
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()
File Upload Errors
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 )
Common upload failures:
must beContent-Type
(set automatically bymultipart/form-data
param)files=- NEVER set
for file uploadsContent-Type: application/json - Check
in site_config.json (default 10MB)max_file_size - [v15+]
restricts file typesallowed_file_extensions
JSON Parse Errors
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 Delivery Failures
Error: Webhook not firing or returning errors Cause: Target URL unreachable, wrong format, or timeout
Debug checklist:
- Check Error Log for webhook delivery errors
- Verify target URL is reachable from server
- Check webhook condition — is it filtering out the event?
- [v15+] Check Webhook Request Log for delivery status
# 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"}
Timeout on Long Operations
Error: HTTP 504 Gateway Timeout or connection reset Cause: Operation takes longer than proxy/server timeout (typically 60s)
Fix: Use background jobs for long operations:
@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"}
Server-Side Error Pattern (Standard)
@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."))
Client-Side Error Handling
// 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.")); } } });
Critical Rules
ALWAYS
- Use specific exception classes in
— enables correct HTTP status codesfrappe.throw() - Set timeout on all external requests —
requests.get(url, timeout=30) - Validate ALL inputs before processing — whitelisted methods are callable by any logged-in user
- Log errors before throwing —
thenfrappe.log_error()frappe.throw() - Handle error callback in every
— silent failures confuse usersfrappe.call() - Use background jobs for operations exceeding 30 seconds
- Return 200 quickly from incoming webhooks then process asynchronously
NEVER
- Expose internal errors to users — log traceback, show friendly message
- Mix token formats —
vstoken key:secretBearer oauth_token - Retry 4xx errors (except 429) — they indicate client bugs, not transient failures
- Skip CSRF token for session-based POST requests — results in 403
- Set Content-Type: application/json for file uploads — must be multipart/form-data
- Catch exceptions without logging — makes production debugging impossible
- Hardcode API credentials — use
from a DocTypesettings.get_password("field")
Reference Files
| File | Contents |
|---|---|
| Complete whitelisted method, webhook, external API patterns |
| Full working API module, client integration, external API client |
| 15 common API error handling mistakes |
See Also
— API implementation patternsfrappe-core-api
— Permission error handling (403 deep dive)frappe-errors-permissions
— Whitelisted method syntaxfrappe-syntax-whitelisted
— Server Script error handlingfrappe-errors-serverscripts