Files
michaelschiemer/src/Framework/LiveComponents/ERROR-RECOVERY.md
Michael Schiemer fc3d7e6357 feat(Production): Complete production deployment infrastructure
- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
2025-10-25 19:18:37 +02:00

18 KiB

LiveComponents Error Recovery Strategy

Error Categories

1. Component Errors

  • Unknown component name
  • Invalid component ID format
  • Component instantiation failure
  • Missing dependencies

2. Action Errors

  • Unknown action method
  • Action method not callable
  • Invalid action parameters
  • Action execution exception

3. State Errors

  • Invalid state format
  • State deserialization failure
  • State validation error
  • State version conflict

4. Rendering Errors

  • Template not found
  • Template syntax error
  • Rendering exception
  • Cache write failure

5. Upload Errors

  • File validation failure
  • Upload size limit exceeded
  • Invalid file type
  • Storage write failure

6. Lifecycle Errors

  • onMount() exception
  • onUpdate() exception
  • onDestroy() exception

Error Handling Strategy

Server-Side Error Handling

1. Component Resolution Errors

Current Behavior:

public function resolve(ComponentId $componentId, ?ComponentData $state = null): LiveComponentContract
{
    if (!$className) {
        throw new \InvalidArgumentException("Unknown component: {$componentName}");
    }
}

Improved Error Recovery:

use App\Framework\Exception\FrameworkException;
use App\Framework\Exception\ErrorCode;

public function resolve(ComponentId $componentId, ?ComponentData $state = null): LiveComponentContract
{
    [$componentName, $instanceId] = $this->parseComponentId($componentId->toString());

    $className = $this->nameToClassMap[$componentName] ?? null;

    if (!$className) {
        // Find similar component names (Levenshtein distance)
        $suggestions = $this->findSimilarComponents($componentName);

        throw FrameworkException::create(
            ErrorCode::COMPONENT_NOT_FOUND,
            "Unknown component: '{$componentName}'"
        )->withData([
            'component_name' => $componentName,
            'instance_id' => $instanceId,
            'suggestions' => $suggestions,
            'available_components' => array_keys($this->nameToClassMap)
        ]);
    }

    try {
        return $this->container->invoker->make($className, [
            'id' => $componentId,
            'initialData' => $state
        ]);
    } catch (\Throwable $e) {
        throw FrameworkException::create(
            ErrorCode::COMPONENT_INSTANTIATION_FAILED,
            "Failed to instantiate component '{$componentName}'"
        )->withData([
            'component_class' => $className,
            'error' => $e->getMessage()
        ])->withPrevious($e);
    }
}

private function findSimilarComponents(string $componentName): array
{
    $suggestions = [];

    foreach (array_keys($this->nameToClassMap) as $availableName) {
        $distance = levenshtein($componentName, $availableName);

        if ($distance <= 3) {  // Max 3 character difference
            $suggestions[] = $availableName;
        }
    }

    return $suggestions;
}

2. Action Execution Errors

Enhanced LiveComponentHandler:

public function handle(
    LiveComponentContract $component,
    string $actionName,
    ActionParameters $params
): ComponentUpdate {
    // Validate action exists
    if (!method_exists($component, $actionName)) {
        $availableActions = $this->getAvailableActions($component);

        throw FrameworkException::create(
            ErrorCode::ACTION_NOT_FOUND,
            "Unknown action '{$actionName}' on component " . get_class($component)
        )->withData([
            'action_name' => $actionName,
            'available_actions' => $availableActions,
            'component_class' => get_class($component)
        ]);
    }

    // Validate action is public and non-static
    $reflection = new \ReflectionMethod($component, $actionName);

    if (!$reflection->isPublic() || $reflection->isStatic()) {
        throw FrameworkException::create(
            ErrorCode::ACTION_NOT_CALLABLE,
            "Action '{$actionName}' is not a public instance method"
        )->withData([
            'action_name' => $actionName,
            'is_public' => $reflection->isPublic(),
            'is_static' => $reflection->isStatic()
        ]);
    }

    // Execute with error recovery
    try {
        $result = $this->container->invoker->invoke($component, $actionName, $params->toArray());

        // Validate result type
        if (!$result instanceof ComponentData) {
            throw FrameworkException::create(
                ErrorCode::ACTION_INVALID_RETURN,
                "Action '{$actionName}' must return ComponentData"
            )->withData([
                'action_name' => $actionName,
                'returned_type' => get_debug_type($result)
            ]);
        }

        return ComponentUpdate::fromActionResult($component, $result);

    } catch (FrameworkException $e) {
        // Re-throw framework exceptions
        throw $e;
    } catch (\Throwable $e) {
        // Wrap other exceptions
        throw FrameworkException::create(
            ErrorCode::ACTION_EXECUTION_FAILED,
            "Action '{$actionName}' execution failed"
        )->withData([
            'action_name' => $actionName,
            'component_class' => get_class($component),
            'error_message' => $e->getMessage(),
            'error_type' => get_class($e)
        ])->withPrevious($e);
    }
}

private function getAvailableActions(LiveComponentContract $component): array
{
    $reflection = new \ReflectionClass($component);
    $actions = [];

    foreach ($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
        if ($method->isStatic() || $method->isConstructor()) {
            continue;
        }

        // Skip getter/setter/lifecycle methods
        if (in_array($method->getName(), ['getId', 'getData', 'getRenderData', 'onMount', 'onUpdate', 'onDestroy'])) {
            continue;
        }

        // Check return type
        $returnType = $method->getReturnType();
        if ($returnType instanceof \ReflectionNamedType &&
            $returnType->getName() === ComponentData::class) {
            $actions[] = $method->getName();
        }
    }

    return $actions;
}

3. State Deserialization Errors

Safe State Handling:

#[Route('/live-component/{id}', method: Method::POST)]
public function handleAction(string $id, HttpRequest $request): JsonResult
{
    try {
        $action = ComponentAction::fromRequest($request, $id);
    } catch (\Throwable $e) {
        return new JsonResult([
            'success' => false,
            'error' => 'Invalid action format',
            'details' => $e->getMessage()
        ], Status::BAD_REQUEST);
    }

    // Safe state deserialization
    try {
        $stateArray = $request->parsedBody?->get('state', []);

        if (!is_array($stateArray)) {
            throw new \InvalidArgumentException('State must be an array');
        }

        $state = ComponentData::fromArray($stateArray);
    } catch (\Throwable $e) {
        return new JsonResult([
            'success' => false,
            'error' => 'Invalid state format',
            'details' => $e->getMessage(),
            'recovery' => 'Client should reload component from server'
        ], Status::BAD_REQUEST);
    }

    // Component resolution with error recovery
    try {
        $component = $this->componentRegistry->resolve($action->componentId, $state);
    } catch (FrameworkException $e) {
        return new JsonResult([
            'success' => false,
            'error' => $e->getMessage(),
            'data' => $e->getData(),
            'recovery' => 'Check component name and reload page'
        ], Status::NOT_FOUND);
    }

    // Action handling with error recovery
    try {
        $update = $this->handler->handle($component, $action->method, $action->params);
    } catch (FrameworkException $e) {
        return new JsonResult([
            'success' => false,
            'error' => $e->getMessage(),
            'data' => $e->getData(),
            'recovery' => 'Try again or reload component'
        ], Status::UNPROCESSABLE_ENTITY);
    } catch (\Throwable $e) {
        // Unexpected errors
        error_log("LiveComponent action error: " . $e->getMessage());

        return new JsonResult([
            'success' => false,
            'error' => 'Internal server error',
            'recovery' => 'Please reload the page',
            'error_id' => uniqid('lc_error_')  // For support
        ], Status::INTERNAL_SERVER_ERROR);
    }

    // Rendering with error recovery
    try {
        $updatedComponent = $this->componentRegistry->resolve(
            $action->componentId,
            ComponentData::fromArray($update->state->data)
        );

        $html = $this->componentRegistry->render($updatedComponent);

        return new JsonResult([
            'success' => true,
            'html' => $html,
            'state' => $update->state->toArray(),
            'events' => $update->events
        ]);
    } catch (\Throwable $e) {
        error_log("LiveComponent render error: " . $e->getMessage());

        return new JsonResult([
            'success' => false,
            'error' => 'Component rendering failed',
            'state' => $update->state->toArray(),  // Return state so client can retry
            'recovery' => 'Try again',
            'error_id' => uniqid('lc_render_')
        ], Status::INTERNAL_SERVER_ERROR);
    }
}

4. Lifecycle Hook Errors

Safe Lifecycle Hook Execution:

// In LiveComponentHandler
public function callMountHook(LiveComponentContract $component): void
{
    if (!$component instanceof LifecycleAware) {
        return;
    }

    try {
        $component->onMount();
    } catch (\Throwable $e) {
        // Log but don't fail - lifecycle hooks are side effects only
        error_log(sprintf(
            "Lifecycle hook onMount() failed for %s: %s",
            get_class($component),
            $e->getMessage()
        ));

        // Optional: Dispatch error event for monitoring
        $this->eventDispatcher?->dispatch(
            new ComponentLifecycleErrorEvent(
                componentId: $component->getId(),
                hook: 'onMount',
                error: $e
            )
        );
    }
}

private function callUpdateHook(LiveComponentContract $component): void
{
    if (!$component instanceof LifecycleAware) {
        return;
    }

    try {
        $component->onUpdate();
    } catch (\Throwable $e) {
        error_log(sprintf(
            "Lifecycle hook onUpdate() failed for %s: %s",
            get_class($component),
            $e->getMessage()
        ));
    }
}

Client-Side Error Recovery

JavaScript Error Handling

Enhanced LiveComponents Client:

class LiveComponentManager {
    async handleAction(componentId, actionName, params = {}) {
        const component = this.components.get(componentId);
        if (!component) {
            console.error(`Component not found: ${componentId}`);
            return;
        }

        // Show loading state
        component.element.classList.add('lc-loading');

        try {
            const response = await this.sendAction(componentId, actionName, params);

            if (!response.success) {
                this.handleError(component, response);
                return;
            }

            // Update component
            this.updateComponent(component, response);

        } catch (error) {
            this.handleNetworkError(component, error);
        } finally {
            component.element.classList.remove('lc-loading');
        }
    }

    handleError(component, response) {
        const { error, recovery, data } = response;

        console.error(`Component error: ${error}`, data);

        // Show user-friendly error message
        this.showErrorMessage(component.element, error);

        // Implement recovery strategy
        switch (recovery) {
            case 'Try again':
                this.showRetryButton(component);
                break;

            case 'Client should reload component from server':
                this.reloadComponent(component);
                break;

            case 'Check component name and reload page':
                this.showReloadPageMessage(component);
                break;

            default:
                // Unknown recovery - show generic error
                this.showGenericError(component);
        }

        // Dispatch error event for custom handling
        component.element.dispatchEvent(new CustomEvent('lc:error', {
            detail: { error, recovery, data },
            bubbles: true
        }));
    }

    handleNetworkError(component, error) {
        console.error('Network error:', error);

        // Retry logic with exponential backoff
        if (component.retryCount < 3) {
            const delay = Math.pow(2, component.retryCount) * 1000;
            component.retryCount++;

            setTimeout(() => {
                console.log(`Retrying action (attempt ${component.retryCount})...`);
                // Retry last action
                this.handleAction(
                    component.id,
                    component.lastAction,
                    component.lastParams
                );
            }, delay);
        } else {
            this.showErrorMessage(
                component.element,
                'Network error. Please check your connection and reload the page.'
            );
        }
    }

    reloadComponent(component) {
        // Reload component from server with current state
        fetch(`/live-component/${component.id}/reload`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ state: component.state })
        })
        .then(response => response.json())
        .then(data => {
            if (data.success) {
                this.updateComponent(component, data);
            } else {
                this.showReloadPageMessage(component);
            }
        })
        .catch(() => {
            this.showReloadPageMessage(component);
        });
    }

    showRetryButton(component) {
        const retryHtml = `
            <div class="lc-error-retry">
                <p>Action failed. Would you like to try again?</p>
                <button onclick="this.closest('.lc-error-retry').remove();
                                 window.liveComponents.retry('${component.id}')">
                    Retry
                </button>
            </div>
        `;

        component.element.insertAdjacentHTML('beforeend', retryHtml);
    }

    retry(componentId) {
        const component = this.components.get(componentId);
        if (component && component.lastAction) {
            this.handleAction(
                component.id,
                component.lastAction,
                component.lastParams
            );
        }
    }
}

Error Codes

Component Errors (COMP_*)

  • COMP_NOT_FOUND - Component name not registered
  • COMP_INSTANTIATION_FAILED - Failed to create component instance
  • COMP_INVALID_ID - Invalid component ID format

Action Errors (ACTION_*)

  • ACTION_NOT_FOUND - Action method does not exist
  • ACTION_NOT_CALLABLE - Action is not public or is static
  • ACTION_INVALID_PARAMS - Invalid action parameters
  • ACTION_EXECUTION_FAILED - Action threw exception
  • ACTION_INVALID_RETURN - Action did not return ComponentData

State Errors (STATE_*)

  • STATE_INVALID_FORMAT - State is not valid JSON/array
  • STATE_DESERIALIZATION_FAILED - Failed to parse state
  • STATE_VALIDATION_FAILED - State validation error
  • STATE_VERSION_CONFLICT - Optimistic locking conflict

Rendering Errors (RENDER_*)

  • RENDER_TEMPLATE_NOT_FOUND - Template file does not exist
  • RENDER_TEMPLATE_SYNTAX - Template has syntax error
  • RENDER_EXCEPTION - Rendering threw exception
  • RENDER_CACHE_WRITE_FAILED - Failed to write to cache

Testing Error Recovery

Unit Tests

it('suggests similar component names on not found error', function () {
    $registry = new ComponentRegistry(...);

    try {
        $registry->resolve(ComponentId::fromString('conter:test'), null);
        expect(false)->toBeTrue(); // Should have thrown
    } catch (FrameworkException $e) {
        expect($e->getData()['suggestions'])->toContain('counter');
    }
});

it('lists available actions on unknown action error', function () {
    $handler = new LiveComponentHandler(...);
    $component = new CounterComponent(...);

    try {
        $handler->handle($component, 'unknownAction', ActionParameters::empty());
        expect(false)->toBeTrue();
    } catch (FrameworkException $e) {
        expect($e->getData()['available_actions'])->toContain('increment');
        expect($e->getData()['available_actions'])->toContain('decrement');
    }
});

Integration Tests

it('recovers from state deserialization error', function () {
    $response = $this->post('/live-component/counter:test', [
        'action' => 'increment',
        'state' => 'invalid json'  // Invalid state
    ]);

    expect($response->status)->toBe(Status::BAD_REQUEST);
    expect($response->json('error'))->toBe('Invalid state format');
    expect($response->json('recovery'))->toContain('reload');
});

it('returns state on rendering error', function () {
    // Component that fails rendering but action succeeded
    $response = $this->post('/live-component/broken:test', [
        'action' => 'increment',
        'state' => ['count' => 5]
    ]);

    expect($response->json('success'))->toBeFalse();
    expect($response->json('state'))->toBeArray();
    expect($response->json('recovery'))->toBe('Try again');
});

Monitoring

Error Metrics

  • livecomponent.error.rate - Errors per second by type
  • livecomponent.error.component - Errors by component name
  • livecomponent.error.action - Errors by action name
  • livecomponent.error.recovery_success_rate - Recovery success rate

Alerts

  • Error rate > 5% - Warning
  • Error rate > 10% - Critical
  • Specific component error rate > 20% - Component issue
  • Network error rate > 10% - Infrastructure issue

Summary

Implemented:

  • Component not found suggestions
  • Available actions listing
  • Safe state deserialization
  • Lifecycle hook error handling
  • Client-side retry logic

⚠️ To Implement:

  • Error code enum (FrameworkException integration)
  • Client-side error recovery UI
  • Comprehensive error testing
  • Error monitoring dashboard

📋 Phase 1 Requirements:

  • Implement all error codes
  • Add client-side error boundaries
  • Comprehensive error tests
  • Error monitoring integration