AgentSkillsCN

api-error-handling

面向PHP REST API的全面标准化错误响应系统,集成SweetAlert2。当构建需要一致错误格式、从数据库异常中提取特定错误消息、验证错误处理和无缝前端集成的REST API时使用。包括PDOException解析、业务规则提取和完整的SweetAlert2错误显示模式。

SKILL.md
--- frontmatter
name: api-error-handling
description: "Comprehensive, standardized error response system for PHP REST APIs with SweetAlert2 integration. Use when building REST APIs that need consistent error formatting, specific error message extraction from database exceptions, validation error handling, and seamless frontend integration. Includes PDOException parsing, business rule extraction, and complete SweetAlert2 error display patterns."

API Error Handling

Implement comprehensive, standardized error response system for PHP REST APIs with consistent JSON envelopes, specific error message extraction, and SweetAlert2 integration.

Core Principles:

  • Consistent JSON envelope across all endpoints
  • Specific error messages extracted from all exception types
  • Appropriate HTTP status codes for all error categories
  • Machine-readable error codes for programmatic handling
  • Human-readable messages for SweetAlert2 display
  • Secure error handling (no stack traces in production)
  • Comprehensive logging with request IDs

See subdirectories for:

  • references/ - Complete PHP classes (ApiResponse, ExceptionHandler, Exceptions)
  • examples/ - Full endpoint implementation, frontend client

Response Envelope Standard

Success:

json
{
  "success": true,
  "data": { /* payload */ },
  "message": "Optional success message",
  "meta": {
    "timestamp": "2026-01-24T10:30:00Z",
    "request_id": "req_abc123"
  }
}

Error:

json
{
  "success": false,
  "message": "Human-readable error for SweetAlert2",
  "error": {
    "code": "ERROR_CODE",
    "type": "validation_error",
    "details": { /* field-specific errors */ }
  },
  "meta": {
    "timestamp": "2026-01-24T10:30:00Z",
    "request_id": "req_abc123"
  }
}

HTTP Status Codes

StatusError TypeUse CaseError Code Examples
400Bad RequestMalformed JSON, missing paramsINVALID_JSON, MISSING_PARAMETER
401UnauthorizedMissing/invalid auth tokenTOKEN_MISSING, TOKEN_EXPIRED
403ForbiddenValid auth but no permissionPERMISSION_DENIED, ACCESS_FORBIDDEN
404Not FoundResource doesn't existRESOURCE_NOT_FOUND, INVOICE_NOT_FOUND
405Method Not AllowedWrong HTTP methodMETHOD_NOT_ALLOWED
409ConflictBusiness rule violationALREADY_EXISTS, OVERPAYMENT
422Unprocessable EntityValidation errorsVALIDATION_FAILED, INVALID_EMAIL
429Too Many RequestsRate limitingRATE_LIMIT_EXCEEDED
500Internal Server ErrorUnexpected errorsINTERNAL_ERROR, DATABASE_ERROR
503Service UnavailableMaintenance/overloadSERVICE_UNAVAILABLE, DEADLOCK

ApiResponse Helper (Quick Reference)

See references/ApiResponse.php for complete implementation

php
// Success responses
ApiResponse::success($data, $message, $status);
ApiResponse::created($data, $message);

// Error responses
ApiResponse::error($message, $code, $status, $type, $details);
ApiResponse::validationError($errors, $message);
ApiResponse::notFound($resource, $identifier);
ApiResponse::unauthorized($message);
ApiResponse::forbidden($permission);
ApiResponse::conflict($message, $code);
ApiResponse::methodNotAllowed($allowedMethods);
ApiResponse::rateLimited($retryAfter);
ApiResponse::serverError($message);
ApiResponse::serviceUnavailable($message);

Exception Handler (Quick Reference)

See references/ExceptionHandler.php for complete implementation

Key Features:

  • Extracts specific messages from PDOException
  • Parses SQLSTATE 45000 (user-defined exceptions from triggers)
  • Extracts constraint violation messages
  • Handles deadlocks gracefully
  • Logs all errors with request ID
  • Hides stack traces in production

PDOException Parsing:

php
// SQLSTATE 45000: Business rule from trigger
// "SQLSTATE[45000]: <<1>>: 1644 Overpayment not allowed"
// Extracted: "Overpayment not allowed"

// SQLSTATE 23000: Duplicate entry
// "Duplicate entry 'john@example.com' for key 'uk_email'"
// Extracted: "A record with this Email already exists: 'john@example.com'"

// SQLSTATE 23000: Foreign key violation
// "Cannot delete or update a parent row..."
// Extracted: "Referenced Customer does not exist or cannot be deleted"

Custom Exception Classes

See references/CustomExceptions.php for all classes

php
// Validation (422)
throw new ValidationException([
    'email' => 'Invalid email format',
    'phone' => 'Phone number required'
], 'Validation failed');

// Authentication (401)
throw new AuthenticationException('Token expired', 'expired');

// Authorization (403)
throw new AuthorizationException('MANAGE_USERS');

// Not Found (404)
throw new NotFoundException('Invoice', 'INV-123');

// Conflict (409)
throw new ConflictException('Already voided', 'ALREADY_VOIDED');

// Rate Limit (429)
throw new RateLimitException(60);

API Bootstrap Pattern

See references/bootstrap.php for complete file

Include at top of all API endpoints:

php
require_once __DIR__ . '/bootstrap.php';

// Helper functions available:
require_method(['POST', 'PUT']);
$data = read_json_body();
validate_required($data, ['customer_id', 'amount']);
$db = get_db();
$token = bearer_token();
require_auth();
require_permission('MANAGE_INVOICES');
handle_request(function() { /* endpoint logic */ });

Endpoint Implementation Pattern

See examples/InvoicesEndpoint.php for complete example

php
require_once __DIR__ . '/../bootstrap.php';
use App\Http\ApiResponse;
use App\Http\Exceptions\{NotFoundException, ValidationException};

require_auth();
handle_request(function() {
    $method = $_SERVER['REQUEST_METHOD'];
    match ($method) {
        'POST' => handlePost(),
        default => ApiResponse::methodNotAllowed('POST')
    };
});

function handlePost(): void {
    $data = read_json_body();
    validate_required($data, ['customer_id', 'items']);

    if (empty($data['items'])) {
        throw new ValidationException(['items' => 'Required']);
    }

    // Business logic...
    ApiResponse::created(['id' => $id], 'Created successfully');
}

Frontend Integration

See examples/ApiClient.js for complete implementation

javascript
const api = new ApiClient('./api');

// GET - Errors automatically shown via SweetAlert2
const response = await api.get('invoices.php', { status: 'pending' });
if (response) renderInvoices(response.data);

// POST - Validation errors highlight form fields
showLoading('Creating...');
const result = await api.post('invoices.php', formData);
hideLoading();
if (result) showSuccess('Created successfully');

// DELETE - With confirmation
const { value: reason } = await Swal.fire({
    title: 'Void?',
    input: 'textarea',
    showCancelButton: true
});
if (reason) {
    const res = await api.delete(`invoices.php?id=${id}`, { reason });
    if (res) showSuccess('Voided');
}

// Helpers: showSuccess/Error/Warning/Info, showConfirm, showLoading, hideLoading

Error Message Extraction

SQLSTATE 45000 (Trigger):

sql
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Overpayment not allowed';
-- Extracted: "Overpayment not allowed" → Code: OVERPAYMENT_NOT_ALLOWED

SQLSTATE 23000 (Duplicate):

code
"Duplicate entry 'john@example.com' for key 'uk_email'"
-- Extracted: "A record with this Email already exists: 'john@example.com'"

SQLSTATE 23000 (Foreign Key):

code
"Cannot delete or update a parent row..."
-- Extracted: "Referenced Customer does not exist or cannot be deleted"

Deadlock:

code
"Deadlock found when trying to get lock..."
-- Extracted: "Database conflict. Please try again." → HTTP 503

Validation Pattern

Backend:

php
$errors = [];
if (empty($data['email'])) $errors['email'] = 'Email required';
elseif (!filter_var($data['email'], FILTER_VALIDATE_EMAIL))
    $errors['email'] = 'Invalid email';

if ($errors) ApiResponse::validationError($errors);

Frontend automatically:

  • SweetAlert2 with formatted error list
  • Highlights fields with .is-invalid
  • Adds .invalid-feedback elements

Business Rule Pattern

php
$stmt = $db->prepare("SELECT status FROM invoices WHERE id = ? FOR UPDATE");
$stmt->execute([$id]);
$invoice = $stmt->fetch();

if (!$invoice) throw new NotFoundException('Invoice', $id);
if ($invoice['status'] === 'voided')
    throw new ConflictException('Already voided', 'ALREADY_VOIDED');
if ($invoice['status'] === 'paid')
    throw new ConflictException('Cannot void paid invoice', 'INVOICE_PAID');

Logging Pattern

All errors logged with context:

code
[req_abc123] PDOException: SQLSTATE[45000]: Overpayment not allowed in /api/payments.php:45
Context: {"user_id":123,"franchise_id":5,"url":"/api/payments.php","method":"POST"}

Implementation Checklist

Backend:

  • All endpoints use ApiResponse helper
  • All exceptions caught by ExceptionHandler
  • Specific error codes for each error type
  • PDOException messages properly extracted
  • Trigger errors parsed (SQLSTATE 45000)
  • Constraint violations give meaningful messages
  • Stack traces hidden in production
  • All errors logged with request ID
  • HTTP status codes match error types
  • Bootstrap file included in all endpoints

Frontend:

  • All API calls use centralized ApiClient
  • Errors displayed via SweetAlert2 only (no native alert/confirm)
  • Validation errors highlight form fields
  • Network errors handled gracefully
  • Loading states shown during requests
  • Error codes displayed in footer
  • Offline state detected and handled
  • Request IDs logged for debugging

Database:

  • Triggers use SQLSTATE 45000 for business rules
  • Clear, user-friendly error messages in triggers
  • Constraints have descriptive names (uk_email_franchise)
  • Foreign key errors give context

Quick Error Reference

php
// 400 - Bad Request
ApiResponse::error('Invalid request', 'BAD_REQUEST', 400);

// 401 - Unauthorized
ApiResponse::unauthorized('Session expired');

// 403 - Forbidden
ApiResponse::forbidden('MANAGE_USERS');

// 404 - Not Found
ApiResponse::notFound('Invoice', 'INV-123');

// 409 - Conflict
ApiResponse::conflict('Already voided', 'ALREADY_VOIDED');

// 422 - Validation
ApiResponse::validationError(['email' => 'Invalid format']);

// 500 - Server Error
ApiResponse::serverError('Unexpected error occurred');

Security Considerations

Production:

  • Never expose stack traces
  • Sanitize database error messages
  • Log full errors server-side only
  • Generic messages for unexpected errors

Development:

  • Set APP_DEBUG=true for detailed errors
  • Stack traces in logs
  • Actual error messages displayed

Sensitive Data:

  • Never include passwords in logs
  • Sanitize SQL queries in error messages
  • Remove file paths from production errors

Summary

Implementation:

  1. Include bootstrap.php at top of all endpoints
  2. Use ApiResponse helper for all responses
  3. Throw custom exceptions for specific errors
  4. Let ExceptionHandler convert to JSON
  5. Use ApiClient class on frontend
  6. Display all errors via SweetAlert2

Key Files:

  • references/ApiResponse.php - Response helper
  • references/ExceptionHandler.php - Exception converter
  • references/CustomExceptions.php - Exception classes
  • references/bootstrap.php - API setup
  • examples/InvoicesEndpoint.php - Complete endpoint
  • examples/ApiClient.js - Frontend client

Benefits:

  • Consistent error format across all endpoints
  • Specific messages from database exceptions
  • Beautiful error display with SweetAlert2
  • Easy debugging with request IDs
  • Secure (no sensitive data exposure)
  • Developer-friendly (clear patterns)

Remember: All error messages must be suitable for direct display in SweetAlert2. Write them for end users, not developers.