php-framework/docs/api/errors.md

11 KiB

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:

{
  "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:

{
  "message": "Missing required parameter: title",
  "error_code": "MISSING_PARAMETER",
  "errors": {
    "title": ["The title field is required."]
  }
}

Invalid Parameter Type:

{
  "message": "Invalid parameter type",
  "error_code": "INVALID_TYPE",
  "errors": {
    "published_at": ["The published at must be a valid date."]
  }
}

401 Unauthorized

Missing Authentication:

{
  "message": "Unauthenticated.",
  "error_code": "UNAUTHENTICATED"
}

Invalid API Key:

{
  "message": "Invalid API key",
  "error_code": "INVALID_API_KEY"
}

Expired Token:

{
  "message": "Token has expired",
  "error_code": "TOKEN_EXPIRED",
  "meta": {
    "expired_at": "2026-01-20T12:00:00Z"
  }
}

403 Forbidden

Insufficient Permissions:

{
  "message": "This action is unauthorized.",
  "error_code": "INSUFFICIENT_PERMISSIONS",
  "required_scope": "posts:write",
  "provided_scopes": ["posts:read"]
}

Workspace Suspended:

{
  "message": "Workspace is suspended",
  "error_code": "WORKSPACE_SUSPENDED",
  "meta": {
    "suspended_at": "2026-01-25T12:00:00Z",
    "reason": "Payment overdue"
  }
}

Namespace Access Denied:

{
  "message": "You do not have access to this namespace",
  "error_code": "NAMESPACE_ACCESS_DENIED"
}

404 Not Found

Resource Not Found:

{
  "message": "Post not found",
  "error_code": "RESOURCE_NOT_FOUND",
  "resource_type": "Post",
  "resource_id": 999
}

Endpoint Not Found:

{
  "message": "Endpoint not found",
  "error_code": "ENDPOINT_NOT_FOUND",
  "requested_path": "/v1/nonexistent"
}

409 Conflict

Duplicate Resource:

{
  "message": "A post with this slug already exists",
  "error_code": "DUPLICATE_RESOURCE",
  "conflicting_field": "slug",
  "existing_resource_id": 123
}

State Conflict:

{
  "message": "Post is already published",
  "error_code": "STATE_CONFLICT",
  "current_state": "published",
  "requested_action": "publish"
}

422 Unprocessable Entity

Validation Failed:

{
  "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:

{
  "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:

{
  "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:

{
  "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

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

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:

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:

// .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

// ✅ Good
if (!response.ok) {
  handleError(response);
}

// ❌ Bad - assumes success
const data = await response.json();

2. Handle All Error Types

// ✅ 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

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

// ✅ 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