| name | frappe-errors-api |
| description | Use when debugging or handling API errors in Frappe/ERPNext v14/v15/v16. Prevents silent failures and wrong HTTP status codes in REST endpoints. Covers 401 Unauthorized (wrong token format, expired OAuth), 403 Forbidden (missing @whitelist, allow_guest needed), 404 Not Found (wrong endpoint URL), 417 Expectation Failed (validation via frappe.throw), 500 Internal Server Error, CORS issues, CSRF token missing/invalid, rate limit exceeded (429), file upload failures, JSON parse errors in request/response, webhook delivery failures, and timeout on long operations. Keywords: API error, 401, 403, 404, 417, 429, 500, CSRF, CORS, REST, whitelist, webhook, rate limit, file upload, authentication token.
|
| license | MIT |
| compatibility | Claude Code, Claude.ai Projects, Claude API. Frappe v14-v16. |
| metadata | {"author":"OpenAEC-Foundation","version":"2.0"} |
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 | AuthenticationError | Bad/expired token, wrong format | Check Authorization: token key:secret or Bearer access_token |
| 403 | PermissionError | Missing @whitelist, no role, no allow_guest | Add decorator or grant permission |
| 404 | DoesNotExistError | Wrong URL, doc not found, typo in endpoint path | Verify /api/resource/:doctype/:name or /api/method/dotted.path |
| 409 | DuplicateEntryError | Unique constraint violated | Check existing records before insert |
| 417 | ValidationError | frappe.throw() called | Fix validation logic or input data |
| 429 | RateLimitExceededError | Too many requests | Respect Retry-After header; throttle requests |
| 500 | Exception (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 | Authorization: token api_key:api_secret | Server-to-server, scripts |
| OAuth Bearer | Authorization: Bearer access_token | OAuth 2.0 flows |
| Session Cookie | Cookie from /api/method/login | Browser-based apps |
headers = {"Authorization": f"Bearer {api_key}:{api_secret}"}
headers = {"Authorization": f"token {api_key}:{api_secret}"}
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
def my_function(name):
return frappe.get_doc("Item", name)
@frappe.whitelist()
def my_function(name):
return frappe.get_doc("Item", name)
@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 allow_guest=True without input validation — these endpoints are exposed to the internet.
Not Found Errors (404)
Common URL Mistakes
| Wrong URL | Correct URL | Issue |
|---|
/api/resource/SalesOrder/SO-001 | /api/resource/Sales Order/SO-001 | Space in DocType name |
/api/method/myapp.my_function | /api/method/myapp.api.my_function | Missing module path |
/api/resource/sales_order | /api/resource/Sales Order | Wrong case / underscore |
/api/v2/document/Item/ITEM-001 [v14] | /api/resource/Item/ITEM-001 | v2 API only in v15+ |
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).
frappe.throw(_("Amount must be positive"))
frappe.throw(_("Amount must be positive"), exc=frappe.ValidationError)
frappe.throw(_("Access denied"), exc=frappe.PermissionError)
frappe.throw(_("Not found"), exc=frappe.DoesNotExistError)
ALWAYS use the specific exception class so clients can handle error types correctly:
frappe.throw(_("Customer not found"))
frappe.throw(_("Customer not found"), exc=frappe.DoesNotExistError)
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
X-Frappe-CSRF-Token header for session-based (cookie) auth.
- Token-based auth (
Authorization: token ...) does NOT require CSRF token.
- OAuth Bearer auth does NOT require CSRF token.
- The CSRF token is available in
frappe.csrf_token in JavaScript or embedded as window.CSRF_TOKEN.
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
{
"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
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))
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
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
},
timeout=60
)
Common upload failures:
Content-Type must be multipart/form-data (set automatically by files= param)
- NEVER set
Content-Type: application/json for file uploads
- Check
max_file_size in site_config.json (default 10MB)
- [v15+]
allowed_file_extensions restricts file types
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):
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
@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"}
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."""
if not docname:
frappe.throw(_("Document name required"), exc=frappe.ValidationError)
if not frappe.db.exists("My DocType", docname):
frappe.throw(_("Document not found"), exc=frappe.DoesNotExistError)
frappe.has_permission("My DocType", "write", docname, throw=True)
try:
doc = frappe.get_doc("My DocType", docname)
result = doc.run_method(action)
return {"status": "success", "data": result}
except frappe.ValidationError:
raise
except frappe.PermissionError:
raise
except Exception:
frappe.log_error(frappe.get_traceback(), f"API Error: {docname}")
frappe.throw(_("Operation failed. Please try again."))
Client-Side Error Handling
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) {
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(());
}
}
});
Critical Rules
ALWAYS
- Use specific exception classes in
frappe.throw() — enables correct HTTP status codes
- 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 —
frappe.log_error() then frappe.throw()
- Handle error callback in every
frappe.call() — silent failures confuse users
- 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 —
token key:secret vs Bearer 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
settings.get_password("field") from a DocType
Reference Files
| File | Contents |
|---|
references/patterns.md | Complete whitelisted method, webhook, external API patterns |
references/examples.md | Full working API module, client integration, external API client |
references/anti-patterns.md | 15 common API error handling mistakes |
See Also
frappe-core-api — API implementation patterns
frappe-errors-permissions — Permission error handling (403 deep dive)
frappe-syntax-whitelisted — Whitelisted method syntax
frappe-errors-serverscripts — Server Script error handling