Skip to content

Add comprehensive error handling and edge case managementΒ #26

@e0ipso

Description

@e0ipso

Add comprehensive error handling and edge case management

Parent Issue: #22 - Add A/B Testing Proxy Block Plugin
Phase: Polish
Depends On: #23 (skeleton), #24 (configuration), #25 (rendering)
Recommended Assignee: AI (systematic error condition identification)

Objective

Implement comprehensive error handling throughout the AbTestProxyBlock to ensure graceful degradation and proper debugging information for all edge cases.

Error Scenarios to Handle

1. Plugin Instantiation Errors

Scenarios:

  • Target block plugin doesn't exist
  • Target block plugin exists but fails to instantiate
  • Plugin configuration is malformed
  • Plugin dependencies missing

Implementation:

protected function createTargetBlock(): ?BlockPluginInterface {
  $config = $this->getConfiguration();
  $plugin_id = $config['target_block_plugin'] ?? '';
  
  if (empty($plugin_id)) {
    $this->logError('Empty target block plugin ID', ['config' => $config]);
    return NULL;
  }
  
  if (\!$this->blockManager->hasDefinition($plugin_id)) {
    $this->logError('Target block plugin not found: @plugin', ['@plugin' => $plugin_id]);
    return NULL;
  }
  
  try {
    $block_config = $config['target_block_config'] ?? [];
    return $this->blockManager->createInstance($plugin_id, $block_config);
  }
  catch (PluginException $e) {
    $this->logError('Failed to instantiate target block @plugin: @message', [
      '@plugin' => $plugin_id,
      '@message' => $e->getMessage(),
    ]);
    return NULL;
  }
  catch (\Exception $e) {
    $this->logError('Unexpected error instantiating target block @plugin: @message', [
      '@plugin' => $plugin_id,
      '@message' => $e->getMessage(),
    ]);
    return NULL;
  }
}

2. Configuration Validation Errors

Scenarios:

  • Invalid render mode values
  • Malformed target block configuration
  • Missing required configuration keys
  • Configuration from different Drupal version

Implementation:

protected function validateConfiguration(): array {
  $config = $this->getConfiguration();
  $errors = [];
  
  // Validate render mode
  $valid_modes = ['block', 'empty'];
  if (\!in_array($config['render_mode'] ?? '', $valid_modes)) {
    $errors[] = 'Invalid render mode: ' . ($config['render_mode'] ?? 'NULL');
  }
  
  // Validate target block configuration when in block mode
  if (($config['render_mode'] ?? '') === 'block') {
    if (empty($config['target_block_plugin'])) {
      $errors[] = 'Target block plugin required when render mode is "block"';
    }
    
    if (isset($config['target_block_config']) && \!is_array($config['target_block_config'])) {
      $errors[] = 'Target block configuration must be an array';
    }
  }
  
  return $errors;
}

public function build(): array {
  $validation_errors = $this->validateConfiguration();
  if (\!empty($validation_errors)) {
    foreach ($validation_errors as $error) {
      $this->logError('Configuration validation error: @error', ['@error' => $error]);
    }
    return $this->buildConfigurationErrorState($validation_errors);
  }
  
  // Continue with normal build process...
}

3. Target Block Runtime Errors

Scenarios:

  • Target block build() method throws exception
  • Target block returns invalid render array
  • Target block access check fails
  • Target block has circular dependencies

Implementation:

protected function buildTargetBlock(BlockPluginInterface $target_block): array {
  try {
    $build = $target_block->build();
    
    if (\!is_array($build)) {
      $this->logError('Target block returned non-array build: @type', [
        '@type' => gettype($build),
      ]);
      return $this->buildRuntimeErrorState('Target block returned invalid build result');
    }
    
    return $build;
  }
  catch (\Exception $e) {
    $this->logError('Target block build() failed: @message', [
      '@message' => $e->getMessage(),
      '@plugin' => $target_block->getPluginId(),
    ]);
    return $this->buildRuntimeErrorState('Target block failed to render');
  }
}

4. Form Handling Errors

Scenarios:

  • Form state corruption during AJAX requests
  • Invalid form submissions
  • Missing form values
  • Form tampering/security issues

Implementation in blockValidate():

public function blockValidate($form, FormStateInterface $form_state) {
  $values = $form_state->getValues();
  
  try {
    // Validate render mode
    if (\!isset($values['render_mode']) || \!in_array($values['render_mode'], ['block', 'empty'])) {
      $form_state->setErrorByName('render_mode', $this->t('Invalid render mode selected.'));
      return;
    }
    
    // Validate block mode requirements
    if ($values['render_mode'] === 'block') {
      if (empty($values['target_block_plugin'])) {
        $form_state->setErrorByName('target_block_plugin', 
          $this->t('You must select a target block when render mode is "block".'));
        return;
      }
      
      // Security: Validate plugin ID format
      if (\!preg_match('/^[a-z0-9_]+$/', $values['target_block_plugin'])) {
        $form_state->setErrorByName('target_block_plugin',
          $this->t('Invalid block plugin ID format.'));
        return;
      }
      
      // Validate plugin exists
      if (\!$this->blockManager->hasDefinition($values['target_block_plugin'])) {
        $form_state->setErrorByName('target_block_plugin',
          $this->t('The selected block plugin does not exist.'));
        return;
      }
    }
  }
  catch (\Exception $e) {
    $this->logError('Form validation error: @message', ['@message' => $e->getMessage()]);
    $form_state->setError($form, $this->t('A validation error occurred. Please try again.'));
  }
}

5. Cache-Related Errors

Scenarios:

  • Target block returns invalid cache metadata
  • Cache context calculation fails
  • Cache tag generation errors
  • Memory issues with large cache metadata

Implementation:

protected function bubbleTargetBlockCacheMetadata(array &$build, BlockPluginInterface $target_block): void {
  try {
    $cache_metadata = CacheableMetadata::createFromRenderArray($build);
    
    // Safely get cache contexts
    try {
      $target_contexts = $target_block->getCacheContexts();
      if (is_array($target_contexts)) {
        $cache_metadata->addCacheContexts($target_contexts);
      }
    }
    catch (\Exception $e) {
      $this->logError('Failed to get cache contexts from target block: @message', ['@message' => $e->getMessage()]);
    }
    
    // Safely get cache tags
    try {
      $target_tags = $target_block->getCacheTags();
      if (is_array($target_tags)) {
        $cache_metadata->addCacheTags($target_tags);
      }
    }
    catch (\Exception $e) {
      $this->logError('Failed to get cache tags from target block: @message', ['@message' => $e->getMessage()]);
    }
    
    // Safely get cache max age
    try {
      $target_max_age = $target_block->getCacheMaxAge();
      if (is_int($target_max_age)) {
        $cache_metadata->setCacheMaxAge(
          Cache::mergeMaxAges($cache_metadata->getCacheMaxAge(), $target_max_age)
        );
      }
    }
    catch (\Exception $e) {
      $this->logError('Failed to get cache max age from target block: @message', ['@message' => $e->getMessage()]);
    }
    
    $cache_metadata->applyTo($build);
  }
  catch (\Exception $e) {
    $this->logError('Failed to apply cache metadata: @message', ['@message' => $e->getMessage()]);
    // Apply basic cache metadata as fallback
    $build['#cache'] = [
      'contexts' => ['url'],
      'tags' => [$this->getCacheTagsToInvalidate()],
      'max-age' => 0, // Disable caching on error
    ];
  }
}

Helper Methods for Error Handling

1. Centralized Logging

protected function logError(string $message, array $context = []): void {
  \Drupal::logger('ab_blocks')->error('[AbTestProxyBlock] ' . $message, $context + [
    'plugin_id' => $this->getPluginId(),
    'block_id' => $this->getConfiguration()['id'] ?? 'unknown',
  ]);
}

protected function logWarning(string $message, array $context = []): void {
  \Drupal::logger('ab_blocks')->warning('[AbTestProxyBlock] ' . $message, $context + [
    'plugin_id' => $this->getPluginId(),
    'block_id' => $this->getConfiguration()['id'] ?? 'unknown',
  ]);
}

2. Error State Builders

protected function buildConfigurationErrorState(array $errors): array {
  return [
    '#markup' => $this->t('Block configuration errors: @errors', [
      '@errors' => implode(', ', $errors),
    ]),
    '#cache' => ['max-age' => 0], // Don't cache error states
  ];
}

protected function buildRuntimeErrorState(string $message): array {
  return [
    '#markup' => $this->t('Block error: @message', ['@message' => $message]),
    '#cache' => ['max-age' => 0],
  ];
}

protected function buildAccessDeniedState(): array {
  return [
    '#markup' => '',
    '#cache' => [
      'contexts' => $this->getCacheContexts(),
      'tags' => $this->getCacheTags(),
      'max-age' => $this->getCacheMaxAge(),
    ],
  ];
}

3. Configuration Recovery

protected function recoverConfiguration(): array {
  $config = $this->getConfiguration();
  $defaults = $this->defaultConfiguration();
  
  // Merge with defaults to fix missing keys
  $recovered_config = $config + $defaults;
  
  // Validate and fix invalid values
  if (\!in_array($recovered_config['render_mode'], ['block', 'empty'])) {
    $recovered_config['render_mode'] = 'empty';
    $this->logWarning('Invalid render mode recovered to "empty"');
  }
  
  if (\!is_array($recovered_config['target_block_config'])) {
    $recovered_config['target_block_config'] = [];
    $this->logWarning('Invalid target block config recovered to empty array');
  }
  
  return $recovered_config;
}

Debug Mode Support

Add development-friendly debug information:

protected function addDebugInformation(array &$build): void {
  if (\Drupal::service('kernel')->getEnvironment() === 'local') {
    $config = $this->getConfiguration();
    $build['#prefix'] = '<\!-- AbTestProxyBlock Debug: ' . 
      'Mode=' . ($config['render_mode'] ?? 'unknown') . ', ' .
      'Target=' . ($config['target_block_plugin'] ?? 'none') . ' -->';
  }
}

Testing Requirements

Error Condition Tests

public function testMissingTargetBlockPlugin() {
  // Set non-existent plugin ID
  // Assert error state returned
  // Assert error logged
}

public function testInvalidRenderMode() {
  // Set invalid render mode
  // Assert configuration error
  // Assert fallback behavior
}

public function testTargetBlockException() {
  // Mock target block that throws exception
  // Assert runtime error state
  // Assert error logged
}

public function testMalformedConfiguration() {
  // Set invalid configuration structure
  // Assert recovery mechanisms work
  // Assert validation catches issues
}

Edge Case Tests

  • Circular dependency prevention
  • Memory limit handling with large configurations
  • Concurrent access scenarios
  • Database connection failures

Acceptance Criteria

  • All error scenarios handled gracefully without fatal errors
  • Comprehensive logging for debugging
  • User-friendly error messages (no technical details exposed)
  • Configuration validation prevents invalid states
  • Error states don't break page rendering
  • Fallback mechanisms work correctly
  • Debug information available in development
  • Performance doesn't degrade significantly with error handling

Context for AI Implementation

This is a systematic task focusing on:

  1. Comprehensive Error Identification: Think through all possible failure points
  2. Graceful Degradation: Never break the page, always return valid render array
  3. Debugging Support: Log useful information for developers
  4. Security: Validate all inputs, sanitize error messages for users
  5. Performance: Error handling shouldn't impact normal operation significantly

Implementation Strategy:

  • Add error handling around every external dependency call
  • Use try-catch blocks judiciously (not everywhere, but around risky operations)
  • Provide fallback render arrays for all error conditions
  • Log errors for debugging without exposing technical details to users
  • Test error conditions systematically

Key Patterns:

  • Always return valid render arrays
  • Use Drupal's logger service consistently
  • Apply proper cache metadata even in error states
  • Validate configurations early and often

Metadata

Metadata

Assignees

No one assigned

    Labels

    🏷️ type:productionProduction codebase (src/, modules/, core functionality)πŸ“‚ area:blocksLayout Builder block A/B testing featuresπŸ“‚ area:coreCore plugin system, interfaces, managers, and base functionalityπŸ”₯ priority:highImportant features, significant bugs affecting many users

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions