php-framework/docs/api/errors.md

526 lines
11 KiB
Markdown
Raw Normal View History

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