phacility/phabricator · error · Exception

Expected nonempty '%s' specification!

Error message

Expected nonempty '%s' specification!

What it means

For the Mercurial wire 'batch' command, Phabricator receives a 'cmds' argument (for example 'heads;known nodes=') and must inspect every sub-command to decide whether the batch is read-only. An empty or missing cmds string cannot be analyzed, so isReadOnlyBatchCommand() deliberately throws rather than assume the batch is safe: the comment says this indicates either a mangled command from the client or a programming error in Phabricator itself.

Source

Thrown at src/applications/diffusion/protocol/DiffusionMercurialWireProtocol.php:73

      'lookup' => true,
      'protocaps' => true,
      'stream_out' => true,
    );

    // Notably, the write commands are "pushkey" and "unbundle". The
    // "batch" command is theoretically read only, but we require explicit
    // analysis of the actual commands.

    return isset($read_only[$command]);
  }

  public static function isReadOnlyBatchCommand($cmds) {
    if (!strlen($cmds)) {
      // We expect a "batch" command to always have a "cmds" string, so err
      // on the side of caution and throw if we don't get any data here. This
      // either indicates a mangled command from the client or a programming
      // error in our code.
      throw new Exception(pht("Expected nonempty '%s' specification!", 'cmds'));
    }

    // For "batch" we get a "cmds" argument like:
    //
    //   heads ;known nodes=
    //
    // We need to examine the commands (here, "heads" and "known") to make sure
    // they're all read-only.

    // NOTE: Mercurial has some code to escape semicolons, but it does not
    // actually function for command separation. For example, these two batch
    // commands will produce completely different results (the former will run
    // the lookup; the latter will fail with a parser error):
    //
    //  lookup key=a:xb;lookup key=z* 0
    //  lookup key=a:;b;lookup key=z* 0
    //               ^
    //               |

View on GitHub (pinned to 5720a38cfe)

Solutions

  1. Reproduce with a stock, unmodified hg client; if stock works, capture the exact bytes your custom client sends and fix the 'cmds' payload
  2. Check for ssh stdin corruption between client and server (proxies, wrapper scripts, unusual environment)
  3. If a stock client triggers it, file a Phabricator bug with the full command trace
Defensive patterns

Strategy: validation

Validate before calling

// validate the batch payload before read-only classification
$cmds = idx($argv, 'cmds', '');
if (!is_string($cmds) || !strlen($cmds)) {
  // reject the mangled request up front with a protocol error
}

Type guard

function isValidBatchSpec($cmds) {
  return is_string($cmds) && strlen($cmds) > 0;
}

Try / catch

try {
  $read_only = DiffusionMercurialWireProtocol::isReadOnlyBatchCommand($cmds);
} catch (Exception $ex) {
  // convert to a protocol-level error for the client and keep the daemon alive
}

Prevention

When it happens

Trigger: A client sends 'batch' over the ssh protocol endpoint with a missing, empty, or truncated 'cmds' value, for example a hand-written protocol client, ssh stdin corruption, or a Phabricator parsing regression that drops the argument.

Common situations: Custom hg wrappers or CI scripts speaking the raw wire protocol; proxies or locale/terminal mangling corrupting ssh stdin; Phabricator protocol-parsing bugs after upgrades.

Related errors


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