Skip to content

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

CodeStatusDescription
200OKRequest succeeded
201CreatedResource created successfully
202AcceptedRequest accepted for processing
204No ContentRequest succeeded, no content to return

4xx Client Errors

CodeStatusDescription
400Bad RequestInvalid request format or parameters
401UnauthorizedMissing or invalid authentication
403ForbiddenAuthenticated but not authorized
404Not FoundResource doesn't exist
405Method Not AllowedHTTP method not supported for endpoint
409ConflictRequest conflicts with current state
422Unprocessable EntityValidation failed
429Too Many RequestsRate limit exceeded

5xx Server Errors

CodeStatusDescription
500Internal Server ErrorUnexpected server error
502Bad GatewayInvalid response from upstream server
503Service UnavailableServer temporarily unavailable
504Gateway TimeoutUpstream 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

CodeHTTP StatusDescription
UNAUTHENTICATED401No authentication provided
INVALID_API_KEY401API key is invalid or revoked
TOKEN_EXPIRED401Authentication token has expired
INVALID_CREDENTIALS401Username/password incorrect
INSUFFICIENT_PERMISSIONS403Missing required permissions/scopes

Resource Errors

CodeHTTP StatusDescription
RESOURCE_NOT_FOUND404Requested resource doesn't exist
DUPLICATE_RESOURCE409Resource with identifier already exists
RESOURCE_LOCKED409Resource is locked by another process
STATE_CONFLICT409Action conflicts with current state

Validation Errors

CodeHTTP StatusDescription
VALIDATION_FAILED422One or more fields failed validation
INVALID_TYPE400Parameter has wrong data type
MISSING_PARAMETER400Required parameter not provided
INVALID_FORMAT400Parameter format is invalid

Rate Limiting Errors

CodeHTTP StatusDescription
RATE_LIMIT_EXCEEDED429Too many requests in time window
QUOTA_EXCEEDED429Usage quota exceeded
CONCURRENT_LIMIT_EXCEEDED429Too many concurrent requests

Business Logic Errors

CodeHTTP StatusDescription
ENTITLEMENT_DENIED403Feature not included in plan
WORKSPACE_SUSPENDED403Workspace is suspended
NAMESPACE_ACCESS_DENIED403No access to namespace
PAYMENT_REQUIRED402Payment required to proceed

System Errors

CodeHTTP StatusDescription
INTERNAL_ERROR500Unexpected server error
SERVICE_UNAVAILABLE503Service temporarily unavailable
GATEWAY_TIMEOUT504Upstream service timeout
MAINTENANCE_MODE503System 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

Released under the EUPL-1.2 License.