526 lines
11 KiB
Markdown
526 lines
11 KiB
Markdown
|
|
# API Errors
|
||
|
|
|
||
|
|
Core PHP Framework uses conventional HTTP response codes and provides detailed error information to help you debug issues.
|
||
|
|
|
||
|
|
## HTTP Status Codes
|
||
|
|
|
||
|
|
### 2xx Success
|
||
|
|
|
||
|
|
| Code | Status | Description |
|
||
|
|
|------|--------|-------------|
|
||
|
|
| 200 | OK | Request succeeded |
|
||
|
|
| 201 | Created | Resource created successfully |
|
||
|
|
| 202 | Accepted | Request accepted for processing |
|
||
|
|
| 204 | No Content | Request succeeded, no content to return |
|
||
|
|
|
||
|
|
### 4xx Client Errors
|
||
|
|
|
||
|
|
| Code | Status | Description |
|
||
|
|
|------|--------|-------------|
|
||
|
|
| 400 | Bad Request | Invalid request format or parameters |
|
||
|
|
| 401 | Unauthorized | Missing or invalid authentication |
|
||
|
|
| 403 | Forbidden | Authenticated but not authorized |
|
||
|
|
| 404 | Not Found | Resource doesn't exist |
|
||
|
|
| 405 | Method Not Allowed | HTTP method not supported for endpoint |
|
||
|
|
| 409 | Conflict | Request conflicts with current state |
|
||
|
|
| 422 | Unprocessable Entity | Validation failed |
|
||
|
|
| 429 | Too Many Requests | Rate limit exceeded |
|
||
|
|
|
||
|
|
### 5xx Server Errors
|
||
|
|
|
||
|
|
| Code | Status | Description |
|
||
|
|
|------|--------|-------------|
|
||
|
|
| 500 | Internal Server Error | Unexpected server error |
|
||
|
|
| 502 | Bad Gateway | Invalid response from upstream server |
|
||
|
|
| 503 | Service Unavailable | Server temporarily unavailable |
|
||
|
|
| 504 | Gateway Timeout | Upstream server timeout |
|
||
|
|
|
||
|
|
## Error Response Format
|
||
|
|
|
||
|
|
All errors return JSON with consistent structure:
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"message": "Human-readable error message",
|
||
|
|
"error_code": "MACHINE_READABLE_CODE",
|
||
|
|
"errors": {
|
||
|
|
"field": ["Detailed validation errors"]
|
||
|
|
},
|
||
|
|
"meta": {
|
||
|
|
"timestamp": "2026-01-26T12:00:00Z",
|
||
|
|
"request_id": "req_abc123"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Common Errors
|
||
|
|
|
||
|
|
### 400 Bad Request
|
||
|
|
|
||
|
|
**Missing Required Parameter:**
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"message": "Missing required parameter: title",
|
||
|
|
"error_code": "MISSING_PARAMETER",
|
||
|
|
"errors": {
|
||
|
|
"title": ["The title field is required."]
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Invalid Parameter Type:**
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"message": "Invalid parameter type",
|
||
|
|
"error_code": "INVALID_TYPE",
|
||
|
|
"errors": {
|
||
|
|
"published_at": ["The published at must be a valid date."]
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 401 Unauthorized
|
||
|
|
|
||
|
|
**Missing Authentication:**
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"message": "Unauthenticated.",
|
||
|
|
"error_code": "UNAUTHENTICATED"
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Invalid API Key:**
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"message": "Invalid API key",
|
||
|
|
"error_code": "INVALID_API_KEY"
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Expired Token:**
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"message": "Token has expired",
|
||
|
|
"error_code": "TOKEN_EXPIRED",
|
||
|
|
"meta": {
|
||
|
|
"expired_at": "2026-01-20T12:00:00Z"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 403 Forbidden
|
||
|
|
|
||
|
|
**Insufficient Permissions:**
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"message": "This action is unauthorized.",
|
||
|
|
"error_code": "INSUFFICIENT_PERMISSIONS",
|
||
|
|
"required_scope": "posts:write",
|
||
|
|
"provided_scopes": ["posts:read"]
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Workspace Suspended:**
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"message": "Workspace is suspended",
|
||
|
|
"error_code": "WORKSPACE_SUSPENDED",
|
||
|
|
"meta": {
|
||
|
|
"suspended_at": "2026-01-25T12:00:00Z",
|
||
|
|
"reason": "Payment overdue"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Namespace Access Denied:**
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"message": "You do not have access to this namespace",
|
||
|
|
"error_code": "NAMESPACE_ACCESS_DENIED"
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 404 Not Found
|
||
|
|
|
||
|
|
**Resource Not Found:**
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"message": "Post not found",
|
||
|
|
"error_code": "RESOURCE_NOT_FOUND",
|
||
|
|
"resource_type": "Post",
|
||
|
|
"resource_id": 999
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Endpoint Not Found:**
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"message": "Endpoint not found",
|
||
|
|
"error_code": "ENDPOINT_NOT_FOUND",
|
||
|
|
"requested_path": "/v1/nonexistent"
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 409 Conflict
|
||
|
|
|
||
|
|
**Duplicate Resource:**
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"message": "A post with this slug already exists",
|
||
|
|
"error_code": "DUPLICATE_RESOURCE",
|
||
|
|
"conflicting_field": "slug",
|
||
|
|
"existing_resource_id": 123
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**State Conflict:**
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"message": "Post is already published",
|
||
|
|
"error_code": "STATE_CONFLICT",
|
||
|
|
"current_state": "published",
|
||
|
|
"requested_action": "publish"
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 422 Unprocessable Entity
|
||
|
|
|
||
|
|
**Validation Failed:**
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"message": "The given data was invalid.",
|
||
|
|
"error_code": "VALIDATION_FAILED",
|
||
|
|
"errors": {
|
||
|
|
"title": [
|
||
|
|
"The title field is required."
|
||
|
|
],
|
||
|
|
"content": [
|
||
|
|
"The content must be at least 10 characters."
|
||
|
|
],
|
||
|
|
"category_id": [
|
||
|
|
"The selected category is invalid."
|
||
|
|
]
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 429 Too Many Requests
|
||
|
|
|
||
|
|
**Rate Limit Exceeded:**
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"message": "Too many requests",
|
||
|
|
"error_code": "RATE_LIMIT_EXCEEDED",
|
||
|
|
"limit": 10000,
|
||
|
|
"remaining": 0,
|
||
|
|
"reset_at": "2026-01-26T13:00:00Z",
|
||
|
|
"retry_after": 3600
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Usage Quota Exceeded:**
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"message": "Monthly usage quota exceeded",
|
||
|
|
"error_code": "QUOTA_EXCEEDED",
|
||
|
|
"quota_type": "monthly",
|
||
|
|
"limit": 50000,
|
||
|
|
"used": 50000,
|
||
|
|
"reset_at": "2026-02-01T00:00:00Z"
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 500 Internal Server Error
|
||
|
|
|
||
|
|
**Unexpected Error:**
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"message": "An unexpected error occurred",
|
||
|
|
"error_code": "INTERNAL_ERROR",
|
||
|
|
"meta": {
|
||
|
|
"request_id": "req_abc123",
|
||
|
|
"timestamp": "2026-01-26T12:00:00Z"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
::: tip
|
||
|
|
In production, internal error messages are sanitized. Include the `request_id` when reporting issues for debugging.
|
||
|
|
:::
|
||
|
|
|
||
|
|
## Error Codes
|
||
|
|
|
||
|
|
### Authentication Errors
|
||
|
|
|
||
|
|
| Code | HTTP Status | Description |
|
||
|
|
|------|-------------|-------------|
|
||
|
|
| `UNAUTHENTICATED` | 401 | No authentication provided |
|
||
|
|
| `INVALID_API_KEY` | 401 | API key is invalid or revoked |
|
||
|
|
| `TOKEN_EXPIRED` | 401 | Authentication token has expired |
|
||
|
|
| `INVALID_CREDENTIALS` | 401 | Username/password incorrect |
|
||
|
|
| `INSUFFICIENT_PERMISSIONS` | 403 | Missing required permissions/scopes |
|
||
|
|
|
||
|
|
### Resource Errors
|
||
|
|
|
||
|
|
| Code | HTTP Status | Description |
|
||
|
|
|------|-------------|-------------|
|
||
|
|
| `RESOURCE_NOT_FOUND` | 404 | Requested resource doesn't exist |
|
||
|
|
| `DUPLICATE_RESOURCE` | 409 | Resource with identifier already exists |
|
||
|
|
| `RESOURCE_LOCKED` | 409 | Resource is locked by another process |
|
||
|
|
| `STATE_CONFLICT` | 409 | Action conflicts with current state |
|
||
|
|
|
||
|
|
### Validation Errors
|
||
|
|
|
||
|
|
| Code | HTTP Status | Description |
|
||
|
|
|------|-------------|-------------|
|
||
|
|
| `VALIDATION_FAILED` | 422 | One or more fields failed validation |
|
||
|
|
| `INVALID_TYPE` | 400 | Parameter has wrong data type |
|
||
|
|
| `MISSING_PARAMETER` | 400 | Required parameter not provided |
|
||
|
|
| `INVALID_FORMAT` | 400 | Parameter format is invalid |
|
||
|
|
|
||
|
|
### Rate Limiting Errors
|
||
|
|
|
||
|
|
| Code | HTTP Status | Description |
|
||
|
|
|------|-------------|-------------|
|
||
|
|
| `RATE_LIMIT_EXCEEDED` | 429 | Too many requests in time window |
|
||
|
|
| `QUOTA_EXCEEDED` | 429 | Usage quota exceeded |
|
||
|
|
| `CONCURRENT_LIMIT_EXCEEDED` | 429 | Too many concurrent requests |
|
||
|
|
|
||
|
|
### Business Logic Errors
|
||
|
|
|
||
|
|
| Code | HTTP Status | Description |
|
||
|
|
|------|-------------|-------------|
|
||
|
|
| `ENTITLEMENT_DENIED` | 403 | Feature not included in plan |
|
||
|
|
| `WORKSPACE_SUSPENDED` | 403 | Workspace is suspended |
|
||
|
|
| `NAMESPACE_ACCESS_DENIED` | 403 | No access to namespace |
|
||
|
|
| `PAYMENT_REQUIRED` | 402 | Payment required to proceed |
|
||
|
|
|
||
|
|
### System Errors
|
||
|
|
|
||
|
|
| Code | HTTP Status | Description |
|
||
|
|
|------|-------------|-------------|
|
||
|
|
| `INTERNAL_ERROR` | 500 | Unexpected server error |
|
||
|
|
| `SERVICE_UNAVAILABLE` | 503 | Service temporarily unavailable |
|
||
|
|
| `GATEWAY_TIMEOUT` | 504 | Upstream service timeout |
|
||
|
|
| `MAINTENANCE_MODE` | 503 | System under maintenance |
|
||
|
|
|
||
|
|
## Handling Errors
|
||
|
|
|
||
|
|
### JavaScript Example
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
async function createPost(data) {
|
||
|
|
try {
|
||
|
|
const response = await fetch('/api/v1/posts', {
|
||
|
|
method: 'POST',
|
||
|
|
headers: {
|
||
|
|
'Authorization': `Bearer ${apiKey}`,
|
||
|
|
'Content-Type': 'application/json'
|
||
|
|
},
|
||
|
|
body: JSON.stringify(data)
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!response.ok) {
|
||
|
|
const error = await response.json();
|
||
|
|
|
||
|
|
switch (response.status) {
|
||
|
|
case 401:
|
||
|
|
// Re-authenticate
|
||
|
|
redirectToLogin();
|
||
|
|
break;
|
||
|
|
case 403:
|
||
|
|
// Show permission error
|
||
|
|
showError('You do not have permission to create posts');
|
||
|
|
break;
|
||
|
|
case 422:
|
||
|
|
// Show validation errors
|
||
|
|
showValidationErrors(error.errors);
|
||
|
|
break;
|
||
|
|
case 429:
|
||
|
|
// Show rate limit message
|
||
|
|
showError(`Rate limited. Retry after ${error.retry_after} seconds`);
|
||
|
|
break;
|
||
|
|
default:
|
||
|
|
// Generic error
|
||
|
|
showError(error.message);
|
||
|
|
}
|
||
|
|
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
return await response.json();
|
||
|
|
} catch (err) {
|
||
|
|
// Network error
|
||
|
|
showError('Network error. Please check your connection.');
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### PHP Example
|
||
|
|
|
||
|
|
```php
|
||
|
|
use GuzzleHttp\Client;
|
||
|
|
use GuzzleHttp\Exception\RequestException;
|
||
|
|
|
||
|
|
$client = new Client(['base_uri' => 'https://api.example.com']);
|
||
|
|
|
||
|
|
try {
|
||
|
|
$response = $client->post('/v1/posts', [
|
||
|
|
'headers' => [
|
||
|
|
'Authorization' => "Bearer {$apiKey}",
|
||
|
|
'Content-Type' => 'application/json',
|
||
|
|
],
|
||
|
|
'json' => $data,
|
||
|
|
]);
|
||
|
|
|
||
|
|
$post = json_decode($response->getBody(), true);
|
||
|
|
|
||
|
|
} catch (RequestException $e) {
|
||
|
|
$statusCode = $e->getResponse()->getStatusCode();
|
||
|
|
$error = json_decode($e->getResponse()->getBody(), true);
|
||
|
|
|
||
|
|
switch ($statusCode) {
|
||
|
|
case 401:
|
||
|
|
throw new AuthenticationException($error['message']);
|
||
|
|
case 403:
|
||
|
|
throw new AuthorizationException($error['message']);
|
||
|
|
case 422:
|
||
|
|
throw new ValidationException($error['errors']);
|
||
|
|
case 429:
|
||
|
|
throw new RateLimitException($error['retry_after']);
|
||
|
|
default:
|
||
|
|
throw new ApiException($error['message']);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Debugging
|
||
|
|
|
||
|
|
### Request ID
|
||
|
|
|
||
|
|
Every response includes a `request_id` for debugging:
|
||
|
|
|
||
|
|
```bash
|
||
|
|
curl -i https://api.example.com/v1/posts
|
||
|
|
```
|
||
|
|
|
||
|
|
Response headers:
|
||
|
|
```
|
||
|
|
X-Request-ID: req_abc123def456
|
||
|
|
```
|
||
|
|
|
||
|
|
Include this ID when reporting issues.
|
||
|
|
|
||
|
|
### Debug Mode
|
||
|
|
|
||
|
|
In development, enable debug mode for detailed errors:
|
||
|
|
|
||
|
|
```php
|
||
|
|
// .env
|
||
|
|
APP_DEBUG=true
|
||
|
|
```
|
||
|
|
|
||
|
|
Debug responses include:
|
||
|
|
- Full stack traces
|
||
|
|
- SQL queries
|
||
|
|
- Exception details
|
||
|
|
|
||
|
|
::: danger
|
||
|
|
Never enable debug mode in production! It exposes sensitive information.
|
||
|
|
:::
|
||
|
|
|
||
|
|
### Logging
|
||
|
|
|
||
|
|
All errors are logged with context:
|
||
|
|
|
||
|
|
```
|
||
|
|
[2026-01-26 12:00:00] production.ERROR: Post not found
|
||
|
|
{
|
||
|
|
"user_id": 123,
|
||
|
|
"workspace_id": 456,
|
||
|
|
"namespace_id": 789,
|
||
|
|
"post_id": 999,
|
||
|
|
"request_id": "req_abc123"
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Best Practices
|
||
|
|
|
||
|
|
### 1. Always Check Status Codes
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
// ✅ Good
|
||
|
|
if (!response.ok) {
|
||
|
|
handleError(response);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ❌ Bad - assumes success
|
||
|
|
const data = await response.json();
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2. Handle All Error Types
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
// ✅ Good - specific handling
|
||
|
|
switch (error.error_code) {
|
||
|
|
case 'RATE_LIMIT_EXCEEDED':
|
||
|
|
retryAfter(error.retry_after);
|
||
|
|
break;
|
||
|
|
case 'VALIDATION_FAILED':
|
||
|
|
showValidationErrors(error.errors);
|
||
|
|
break;
|
||
|
|
default:
|
||
|
|
showGenericError(error.message);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ❌ Bad - generic handling
|
||
|
|
alert(error.message);
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3. Implement Retry Logic
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
async function fetchWithRetry(url, options, retries = 3) {
|
||
|
|
for (let i = 0; i < retries; i++) {
|
||
|
|
try {
|
||
|
|
const response = await fetch(url, options);
|
||
|
|
|
||
|
|
if (response.status === 429) {
|
||
|
|
// Rate limited - wait and retry
|
||
|
|
const retryAfter = parseInt(response.headers.get('Retry-After'));
|
||
|
|
await sleep(retryAfter * 1000);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
return response;
|
||
|
|
} catch (err) {
|
||
|
|
if (i === retries - 1) throw err;
|
||
|
|
await sleep(1000 * Math.pow(2, i)); // Exponential backoff
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4. Log Error Context
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
// ✅ Good - log context
|
||
|
|
console.error('API Error:', {
|
||
|
|
endpoint: '/v1/posts',
|
||
|
|
method: 'POST',
|
||
|
|
status: response.status,
|
||
|
|
error_code: error.error_code,
|
||
|
|
request_id: error.meta.request_id
|
||
|
|
});
|
||
|
|
|
||
|
|
// ❌ Bad - no context
|
||
|
|
console.error(error.message);
|
||
|
|
```
|
||
|
|
|
||
|
|
## Learn More
|
||
|
|
|
||
|
|
- [API Authentication →](/api/authentication)
|
||
|
|
- [Rate Limiting →](/api/endpoints#rate-limiting)
|
||
|
|
- [API Endpoints →](/api/endpoints)
|