methorz / http-problem-details
PSR-15 error handling middleware with RFC 7807 Problem Details support
Installs: 0
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/methorz/http-problem-details
Requires
- php: ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0
- psr/container: ^2.0
- psr/http-factory: ^1.0
- psr/http-message: ^1.0 || ^2.0
- psr/http-server-handler: ^1.0
- psr/http-server-middleware: ^1.0
- psr/log: ^3.0
Requires (Dev)
- nyholm/psr7: ^1.8
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^11.0 || ^12.0
- slevomat/coding-standard: ^8.25
- squizlabs/php_codesniffer: ^4.0
Suggests
- laminas/laminas-servicemanager: For Mezzio integration (use Integration\Mezzio\ConfigProvider)
This package is auto-updated.
Last update: 2025-12-01 09:38:26 UTC
README
Comprehensive error handling middleware for PSR-15 applications with RFC 7807 Problem Details support
Production-ready error handling with RFC 7807 Problem Details, environment-aware formatting, automatic logging, and developer-friendly stack traces. Zero configuration, works out-of-the-box.
โจ Features
- ๐ RFC 7807 Compliance - Standardized error responses (Problem Details)
- ๐ Environment-Aware - Stack traces in dev, sanitized messages in production
- ๐ Automatic Logging - PSR-3 logger integration with context
- ๐ฏ Custom Exception Mapping - Map exceptions to HTTP status codes
- ๐ Exception Chaining - Captures and formats previous exception details
- ๐ก Developer-Friendly - Detailed debugging info in development mode
- ๐ Production-Safe - No sensitive data leaks in production
- ๐จ Framework Agnostic - Works with any PSR-15 application
๐ฆ Installation
composer require methorz/http-problem-details
๐ Quick Start
Basic Usage
use MethorZ\ProblemDetails\Middleware\ErrorHandlerMiddleware; use Nyholm\Psr7\Factory\Psr17Factory; $middleware = new ErrorHandlerMiddleware( new Psr17Factory() ); // Add to middleware pipeline (first!) $app->pipe($middleware);
Production Response (500 Internal Server Error):
{
"type": "about:blank",
"title": "Internal Server Error",
"status": 500,
"detail": "An error occurred"
}
Development Response (with stack trace):
{
"type": "about:blank",
"title": "Internal Server Error",
"status": 500,
"detail": "Division by zero",
"trace": "#0 /path/to/file.php(42): calculate()...",
"file": "/path/to/file.php",
"line": 42,
"request_method": "POST",
"request_uri": "https://api.example.com/calculate",
"exception_class": "DivisionByZeroError"
}
๐ Detailed Usage
With Logger Integration
use MethorZ\ProblemDetails\Middleware\ErrorHandlerMiddleware; use Nyholm\Psr7\Factory\Psr17Factory; use Monolog\Logger; $logger = new Logger('app'); $middleware = new ErrorHandlerMiddleware( new Psr17Factory(), $logger // PSR-3 logger );
Logged Context:
[2024-11-26 10:30:00] app.ERROR: Exception caught: User not found {
"exception_class": "App\\Exception\\NotFoundException",
"exception_message": "User not found",
"exception_code": 0,
"exception_file": "/app/src/Service/UserService.php",
"exception_line": 42,
"request_method": "GET",
"request_uri": "https://api.example.com/users/123"
}
Development vs Production Mode
// Development: Include stack traces and debug info $devMiddleware = new ErrorHandlerMiddleware( $responseFactory, $logger, isDevelopment: true // โ Enable debug mode ); // Production: Sanitized error messages $prodMiddleware = new ErrorHandlerMiddleware( $responseFactory, $logger, isDevelopment: false // โ Production safe );
Custom Exception Status Mapping
use App\Exception\NotFoundException; use App\Exception\ValidationException; $middleware = new ErrorHandlerMiddleware( $responseFactory, $logger, exceptionStatusMap: [ NotFoundException::class => 404, ValidationException::class => 422, \InvalidArgumentException::class => 400, ] );
Before:
NotFoundExceptionโ 500 Internal Server Error โ
After:
NotFoundExceptionโ 404 Not Found โValidationExceptionโ 422 Unprocessable Entity โ
๐ฏ RFC 7807 Problem Details
Building Custom Problem Details
use MethorZ\ProblemDetails\Response\ProblemDetails; use Nyholm\Psr7\Response; $problem = ProblemDetails::create(404, 'Not Found') ->withType('https://api.example.com/problems/user-not-found') ->withDetail('User with ID 123 does not exist') ->withInstance('/api/users/123') ->withAdditional('user_id', 123); $response = $problem->toResponse(new Response());
Response:
{
"type": "https://api.example.com/problems/user-not-found",
"title": "Not Found",
"status": 404,
"detail": "User with ID 123 does not exist",
"instance": "/api/users/123",
"user_id": 123
}
Creating from Exception
$exception = new NotFoundException('User not found'); // Production mode $problem = ProblemDetails::fromException($exception, includeTrace: false); // Development mode $problem = ProblemDetails::fromException($exception, includeTrace: true);
๐ Environment-Aware Behavior
Development Mode (isDevelopment: true)
Response includes:
- โ Full exception message
- โ Stack trace
- โ File path and line number
- โ Exception class name
- โ Request method and URI
- โ Previous exception chain
Use when: Local development, staging, testing
Production Mode (isDevelopment: false)
Response includes:
- โ HTTP status code
- โ Generic title
- โ Exception message (if safe)
- โ NO stack traces
- โ NO file paths
- โ NO internal details
Use when: Production, public APIs
๐ HTTP Status Code Mapping
| Exception Type | Status Code | Log Level |
|---|---|---|
| Client errors (4xx) | 400-499 | warning |
| Server errors (5xx) | 500-599 | error |
Supported Status Codes:
400 Bad Request
401 Unauthorized
403 Forbidden
404 Not Found
405 Method Not Allowed
408 Request Timeout
409 Conflict
422 Unprocessable Entity
429 Too Many Requests
500 Internal Server Error
501 Not Implemented
502 Bad Gateway
503 Service Unavailable
504 Gateway Timeout
๐ Exception Chaining
Automatically captures and formats exception chains:
try { $db->connect(); // Throws PDOException } catch (PDOException $e) { throw new DatabaseException('Failed to connect', 0, $e); // Wraps PDOException }
Development Response (with previous_exception):
{
"status": 500,
"title": "Internal Server Error",
"detail": "Failed to connect",
"trace": "...",
"previous_exception": {
"class": "PDOException",
"message": "SQLSTATE[HY000] [2002] Connection refused",
"file": "/app/src/Database.php",
"line": 25
}
}
๐งช Testing
# Run tests composer test # Static analysis composer analyze # Code style composer cs-check composer cs-fix
Test Coverage: 21 tests, 59 assertions, 100% passing
๐ ๏ธ Use Cases
1. REST API Error Handling
// Global error handler (first middleware) $app->pipe(new ErrorHandlerMiddleware( $responseFactory, $logger, isDevelopment: $_ENV['APP_ENV'] === 'development' )); // All uncaught exceptions become RFC 7807 responses
2. Custom Application Exceptions
namespace App\Exception; class UserNotFoundException extends \RuntimeException { public function getStatusCode(): int { return 404; // โ Automatically used by middleware } }
3. Validation Error Responses
$middleware = new ErrorHandlerMiddleware( $responseFactory, $logger, exceptionStatusMap: [ ValidationException::class => 422, ] ); throw new ValidationException('Email is required'); // โ 422 Unprocessable Entity with Problem Details
4. Microservices Error Consistency
All services return the same RFC 7807 format:
{
"type": "about:blank",
"title": "Not Found",
"status": 404,
"detail": "Resource not found"
}
๐ง Configuration Examples
Mezzio / Laminas
// config/autoload/middleware.global.php use MethorZ\ProblemDetails\Middleware\ErrorHandlerMiddleware; return [ 'dependencies' => [ 'factories' => [ ErrorHandlerMiddleware::class => function ($container): ErrorHandlerMiddleware { return new ErrorHandlerMiddleware( $container->get(ResponseFactoryInterface::class), $container->get(LoggerInterface::class), isDevelopment: $_ENV['APP_ENV'] === 'development', exceptionStatusMap: [ NotFoundException::class => 404, ValidationException::class => 422, ], ); }, ], ], ]; // config/pipeline.php $app->pipe(ErrorHandlerMiddleware::class); // FIRST middleware!
Slim Framework
use MethorZ\ProblemDetails\Middleware\ErrorHandlerMiddleware; $app->add(new ErrorHandlerMiddleware( $responseFactory, $logger, isDevelopment: $_ENV['DEBUG'] === 'true' ));
๐ Resources
๐ Related Packages
This package is part of the MethorZ HTTP middleware ecosystem:
| Package | Description |
|---|---|
| methorz/http-dto | Automatic HTTP โ DTO conversion with validation |
| methorz/http-problem-details | RFC 7807 error handling (this package) |
| methorz/http-cache-middleware | HTTP caching with ETag support |
| methorz/http-request-logger | Structured logging with request tracking |
| methorz/openapi-generator | Automatic OpenAPI spec generation |
These packages work together seamlessly in PSR-15 applications.
๐ก Best Practices
DO
- โ Place error middleware FIRST in pipeline
- โ
Use
isDevelopmentbased on environment variable - โ Map domain exceptions to appropriate HTTP status codes
- โ Log exceptions with context for debugging
- โ Use PSR-3 logger for centralized log management
DON'T
- โ Don't expose stack traces in production (
isDevelopment: false) - โ Don't return 500 for client errors (use 4xx instead)
- โ Don't log sensitive data (passwords, tokens) in error context
- โ Don't catch errors before error middleware (let it handle them)
๐ Security Considerations
Sensitive Data
- โ Production mode hides file paths and stack traces
- โ Exception messages are still included (ensure they're safe!)
- โ Logger context can be filtered/redacted
- โ Don't include passwords, tokens, or PII in exception messages
Information Disclosure
// โ BAD: Leaks sensitive info throw new Exception("DB connection failed: password='secret123'"); // โ GOOD: Generic message throw new DatabaseException("Failed to connect to database");
๐ License
MIT License. See LICENSE for details.
๐ค Contributing
Contributions welcome! See CONTRIBUTING.md for guidelines.