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

dev-main 2025-12-01 09:38 UTC

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

CI codecov PHPStan PHP Version License

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 isDevelopment based 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.

๐Ÿ”— Links