Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
260
src/Framework/ErrorReporting/ErrorReportingMiddleware.php
Normal file
260
src/Framework/ErrorReporting/ErrorReportingMiddleware.php
Normal file
@@ -0,0 +1,260 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ErrorReporting;
|
||||
|
||||
use App\Framework\Http\HttpMiddleware;
|
||||
use App\Framework\Http\MiddlewareContext;
|
||||
use App\Framework\Http\Next;
|
||||
use App\Framework\Http\Request;
|
||||
use App\Framework\Http\RequestStateManager;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Middleware for automatic error reporting with request context enrichment
|
||||
*/
|
||||
final readonly class ErrorReportingMiddleware implements HttpMiddleware
|
||||
{
|
||||
private const int MAX_RECURSION_DEPTH = 5;
|
||||
private const int MAX_ARRAY_SIZE = 50;
|
||||
private const int MAX_STRING_LENGTH = 1000;
|
||||
|
||||
private const array PROXY_HEADERS = [
|
||||
'X-Forwarded-For',
|
||||
'X-Real-IP',
|
||||
'Client-IP',
|
||||
'X-Forwarded',
|
||||
'Forwarded-For',
|
||||
'Forwarded',
|
||||
];
|
||||
|
||||
private const array SENSITIVE_KEYS = [
|
||||
'password', 'passwd', 'pwd', 'secret', 'token', 'key', 'api_key',
|
||||
'authorization', 'auth', 'csrf', 'csrf_token', 'access_token',
|
||||
'refresh_token', 'private_key', 'credit_card', 'card_number',
|
||||
'cvv', 'cvc', 'ssn', 'social_security',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private ErrorReporter $reporter,
|
||||
private bool $enabled = true
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(MiddlewareContext $context, Next $next, RequestStateManager $stateManager): MiddlewareContext
|
||||
{
|
||||
if (! $this->enabled) {
|
||||
return $next($context);
|
||||
}
|
||||
|
||||
try {
|
||||
return $next($context);
|
||||
} catch (Throwable $e) {
|
||||
$this->reportError($e, $context);
|
||||
|
||||
throw $e; // Re-throw for proper error handling chain
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Report error with enriched request context
|
||||
*/
|
||||
private function reportError(Throwable $error, MiddlewareContext $context): void
|
||||
{
|
||||
try {
|
||||
$request = $context->request;
|
||||
$requestContext = $this->buildRequestContext($request);
|
||||
|
||||
$contextualReporter = $this->reporter->withRequestContext(
|
||||
method: $requestContext['method'],
|
||||
route: $requestContext['route'],
|
||||
requestId: $requestContext['requestId'],
|
||||
userAgent: $requestContext['userAgent'],
|
||||
ipAddress: $requestContext['ipAddress'],
|
||||
requestData: $requestContext['requestData']
|
||||
);
|
||||
|
||||
$contextualReporter->reportThrowable($error);
|
||||
} catch (Throwable) {
|
||||
// Silently fail to prevent error reporting from breaking the application
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build comprehensive request context for error reporting
|
||||
*/
|
||||
private function buildRequestContext(Request $request): array
|
||||
{
|
||||
return [
|
||||
'method' => $request->method->value,
|
||||
'route' => $request->path,
|
||||
'requestId' => $request->id->toString(),
|
||||
'userAgent' => $request->server->getUserAgent()->value,
|
||||
'ipAddress' => $this->extractClientIpAddress($request),
|
||||
'requestData' => $this->extractRequestData($request),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract client IP address from request headers and server environment
|
||||
*/
|
||||
private function extractClientIpAddress(Request $request): ?string
|
||||
{
|
||||
// Check proxy headers for real client IP
|
||||
foreach (self::PROXY_HEADERS as $header) {
|
||||
$ip = $this->extractIpFromHeader($request, $header);
|
||||
if ($ip !== null) {
|
||||
return $ip;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to direct connection IP
|
||||
return $request->server->get('REMOTE_ADDR');
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract and validate IP address from specific header
|
||||
*/
|
||||
private function extractIpFromHeader(Request $request, string $header): ?string
|
||||
{
|
||||
$headerValue = $request->headers->get($header);
|
||||
if (empty($headerValue)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Handle comma-separated list (X-Forwarded-For)
|
||||
if (str_contains($headerValue, ',')) {
|
||||
$headerValue = trim(explode(',', $headerValue)[0]);
|
||||
}
|
||||
|
||||
// Validate as public IP address
|
||||
return filter_var($headerValue, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)
|
||||
? $headerValue
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract and sanitize request data from multiple sources
|
||||
*/
|
||||
private function extractRequestData(Request $request): ?array
|
||||
{
|
||||
$data = [];
|
||||
|
||||
// Query parameters
|
||||
if (! empty($request->queryParams)) {
|
||||
$data['query'] = $this->sanitizeData($request->queryParams);
|
||||
}
|
||||
|
||||
// Request body (could be form data or JSON)
|
||||
if (! empty($request->body)) {
|
||||
$data['body'] = $this->sanitizeRequestBody($request);
|
||||
}
|
||||
|
||||
return ! empty($data) ? $data : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize request body based on content type
|
||||
*/
|
||||
private function sanitizeRequestBody(Request $request): mixed
|
||||
{
|
||||
$contentType = $request->headers->get('Content-Type') ?? '';
|
||||
|
||||
if (str_contains($contentType, 'application/json')) {
|
||||
return $this->sanitizeJsonBody($request->body);
|
||||
}
|
||||
|
||||
// For other content types, just sanitize as string
|
||||
return $this->sanitizeData($request->body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and sanitize JSON body
|
||||
*/
|
||||
private function sanitizeJsonBody(string $body): mixed
|
||||
{
|
||||
try {
|
||||
$jsonData = json_decode($body, true);
|
||||
|
||||
return json_last_error() === JSON_ERROR_NONE
|
||||
? $this->sanitizeData($jsonData)
|
||||
: '[invalid_json]';
|
||||
} catch (Throwable) {
|
||||
return '[json_parse_error]';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively sanitize data to remove sensitive information
|
||||
*/
|
||||
private function sanitizeData(mixed $data, int $depth = 0): mixed
|
||||
{
|
||||
if ($depth > self::MAX_RECURSION_DEPTH) {
|
||||
return '[max_depth_reached]';
|
||||
}
|
||||
|
||||
return match (true) {
|
||||
is_array($data) => $this->sanitizeArray($data, $depth),
|
||||
is_string($data) => $this->sanitizeString($data),
|
||||
default => $data
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize array data with size limits and sensitive key filtering
|
||||
*/
|
||||
private function sanitizeArray(array $data, int $depth): array
|
||||
{
|
||||
// Limit array size to prevent memory issues
|
||||
if (count($data) > self::MAX_ARRAY_SIZE) {
|
||||
$data = array_slice($data, 0, self::MAX_ARRAY_SIZE, true);
|
||||
$data['[truncated]'] = 'Array truncated - too many items';
|
||||
}
|
||||
|
||||
$sanitized = [];
|
||||
foreach ($data as $key => $value) {
|
||||
$sanitized[$key] = $this->isSensitiveKey((string)$key)
|
||||
? '[redacted]'
|
||||
: $this->sanitizeData($value, $depth + 1);
|
||||
}
|
||||
|
||||
return $sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize string data with length limits and pattern removal
|
||||
*/
|
||||
private function sanitizeString(string $data): string
|
||||
{
|
||||
// Limit string length
|
||||
if (strlen($data) > self::MAX_STRING_LENGTH) {
|
||||
$data = substr($data, 0, self::MAX_STRING_LENGTH) . '[truncated]';
|
||||
}
|
||||
|
||||
// Remove sensitive patterns
|
||||
$patterns = [
|
||||
'/\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/' => '[card_number]',
|
||||
'/\b\d{3}-?\d{2}-?\d{4}\b/' => '[ssn]',
|
||||
'/Bearer\s+[A-Za-z0-9\-._~+\/]+=*/' => 'Bearer [redacted]',
|
||||
];
|
||||
|
||||
return preg_replace(array_keys($patterns), array_values($patterns), $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a key contains sensitive information that should be redacted
|
||||
*/
|
||||
private function isSensitiveKey(string $key): bool
|
||||
{
|
||||
$lowerKey = strtolower($key);
|
||||
|
||||
foreach (self::SENSITIVE_KEYS as $sensitiveKey) {
|
||||
if (str_contains($lowerKey, $sensitiveKey)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user