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; } }