phacility/phabricator · error · Exception

Search cluster configuration is not valid: each entry in the

Error message

Search cluster configuration is not valid: each entry in the list must be a dictionary describing a search service, but the value with index "%s" is not a dictionary.

What it means

`cluster.search` must be a list where every entry is a dictionary describing one search service. PhabricatorClusterSearchConfigType::validateValue iterates the list and throws when an entry is a scalar or a list — e.g. a bare engine name — instead of a map. Validation runs through validateStoredValue whenever the option is saved.

Source

Thrown at src/infrastructure/cluster/config/PhabricatorClusterSearchConfigType.php:19

<?php

final class PhabricatorClusterSearchConfigType
  extends PhabricatorJSONConfigType {

  const TYPEKEY = 'cluster.search';

  public function validateStoredValue(
    PhabricatorConfigOption $option,
    $value) {
    self::validateValue($value);
  }

  public static function validateValue($value) {
    $engines = PhabricatorSearchService::loadAllFulltextStorageEngines();

    foreach ($value as $index => $spec) {
      if (!is_array($spec)) {
        throw new Exception(
          pht(
            'Search cluster configuration is not valid: each entry in the '.
            'list must be a dictionary describing a search service, but '.
            'the value with index "%s" is not a dictionary.',
            $index));
      }

      try {
        PhutilTypeSpec::checkMap(
          $spec,
          array(
            'type'      => 'string',
            'hosts'     => 'optional list<map<string, wild>>',
            'roles'     => 'optional map<string, wild>',
            'port'      => 'optional int',
            'protocol'  => 'optional string',
            'path'      => 'optional string',
            'version'   => 'optional int',

View on GitHub (pinned to 5720a38cfe)

Solutions

  1. Wrap each service as a dictionary whose required first key is `type`, e.g. `array('type' => 'elasticsearch', 'hosts' => array(...))`.
  2. Remove any scalar or plain-list entries from `cluster.search`.
  3. Prefer the Config → Search Servers UI, which produces the correct structure.

Example fix

// before
'cluster.search' => array('elasticsearch'),
// after
'cluster.search' => array(
  array(
    'type' => 'elasticsearch',
    'hosts' => array(
      array('host' => 'es1.example.com', 'port' => 9200),
    ),
  ),
),
Defensive patterns

Strategy: validation

Validate before calling

// Before saving cluster.search, mirror the config-type check:
function assertSearchEntriesAreMaps(array $cluster_search) {
  foreach ($cluster_search as $index => $spec) {
    if (!is_array($spec) || (array_keys($spec) === range(0, count($spec) - 1))) {
      throw new Exception(
        'cluster.search['.$index.'] must be a dictionary, e.g. '.
        "array('type' => 'mysql')");
    }
  }
}

Type guard

function isSearchServiceSpec($spec) {
  return is_array($spec) && isset($spec['type']) && is_string($spec['type']);
}

Prevention

When it happens

Trigger: Setting `cluster.search` to `array('elasticsearch')` (string entry), `array('mysql', 'elasticsearch')` (scalars), or a doubly-nested list, instead of `array(array('type' => 'mysql'))`. Triggered when saving the option via the config application or `bin/config set`.

Common situations: Hand-writing the config from memory instead of docs; converting config from YAML/JSON and losing a nesting level; older examples where a simpler format was shown.

Related errors


AI-assisted analysis of phacility/phabricator@5720a38cfe (2026-08-21). Data as JSON: /api/errors/17290cf3ced478be. Report an issue: GitHub.