Skip to main content
ClaudeWave
Install in Claude Code
Copy
git clone --depth 1 https://github.com/Impertio-Studio/Frappe_Claude_Skill_Package /tmp/frappe-errors-api && cp -r /tmp/frappe-errors-api/skills/source/errors/frappe-errors-api ~/.claude/skills/frappe-errors-api
Then start a new Claude Code session; the skill loads automatically.

SKILL.md

# 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 |

```python
# 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
```

```python
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
```

```python
# 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 `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+ |

```python
# 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).

```python
# 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:
```python
# 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 `X-Frappe-CSRF-Token` header for session-based (cookie) auth.
- Token-based auth (`Authorization: token ...`) does NOT require CSRF token.
- OAuth Bearer auth does