phacility/phabricator · error · PhutilProxyException

Failed to decode rule data.

Error message

Failed to decode rule data.

What it means

Thrown by HeraldRuleController::processRequest when saving a rule: the client POSTs a 'rule' parameter containing the whole rule serialized as JSON, and phutil_json_decode() throws PhutilJSONParserException because the blob is not parseable JSON. The controller wraps it in a PhutilProxyException with this generic message so the underlying parse error (position, reason) travels along with it.

Solutions

  1. Save the rule through the standard rule editor UI and let it serialize the payload; do not hand-edit the hidden 'rule' field.
  2. If scripting the endpoint, build strict JSON (double quotes, no trailing commas) and verify with jq before POSTing.
  3. Raise PHP post_max_size / upload_max_filesize and any proxy body limits if very large rules are being truncated mid-request.
  4. Check the wrapped PhutilJSONParserException message (visible in the exception trace/logs) for the exact parse offset, then fix that spot in the payload.

Example fix

// before — posting a PHP-ish blob
curl -d 'rule=conditions=>Array;actions=>Array' .../herald/edit/

// after — post strict JSON matching the client's schema
curl -d 'rule={"name":"Triage","conditions":[["title","contains","fix"]],"actions":[["addcc","alice"]],"logic":[]}' .../herald/edit/
Defensive patterns

Strategy: try-catch

Validate before calling

// Server-side pre-flight of the client payload
$raw = $request->getStr('rule');
try {
  $data = phutil_json_decode($raw);
} catch (PhutilJSONParserException $ex) {
  // return a 400 dialog with $ex->getMessage() (shows parse offset)
}

Type guard

function isStrictJson($raw) {
  if (!is_string($raw) || $raw === '') {
    return false;
  }
  try {
    phutil_json_decode($raw);
    return true;
  } catch (PhutilJSONParserException $ex) {
    return false;
  }
}

Try / catch

try {
  $data = phutil_json_decode($request->getStr('rule'));
} catch (PhutilJSONParserException $ex) {
  throw new PhutilProxyException(
    pht('Failed to decode rule data.'),
    $ex); // preserve the parse position for logs
}

Prevention

When it happens

Trigger: The 'rule' request parameter is truncated, empty, mojibake, or double-encoded when the rule edit form is submitted (Javelin client normally serializes it). Triggered by browser extensions rewriting form bodies, proxies trimming POST data, hand-crafted curl requests with malformed JSON, or client-side bugs that submit before serialization finishes.

Common situations: Scripting or fuzzing the /herald/edit/ endpoint without building the Javelin client's exact JSON payload; request-body size limits (post_max_size, proxy limits) silently truncating large rules with many conditions; ad-hoc integrations that send a PHP-style serialized array instead of JSON.

Understand the failure class

Background: JSON parse error: "Unexpected token" / "not valid JSON" / "failed to parse" — what JSON parsers are really complaining about — this error's family across 45 libraries.

Related errors


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

Appendix: source

Thrown at src/applications/herald/controller/HeraldRuleController.php:289

    // valid policy value.
    $repetition_options = $this->getRepetitionOptionMap($adapter);
    if (!isset($repetition_options[$repetition_policy])) {
      $repetition_policy = head_key($repetition_options);
    }

    $e_name = true;
    $errors = array();

    if (!strlen($new_name)) {
      $e_name = pht('Required');
      $errors[] = pht('Rule must have a name.');
    }

    $data = null;
    try {
      $data = phutil_json_decode($request->getStr('rule'));
    } catch (PhutilJSONParserException $ex) {
      throw new PhutilProxyException(
        pht('Failed to decode rule data.'),
        $ex);
    }

    if (!is_array($data) ||
        !$data['conditions'] ||
        !$data['actions']) {
      throw new Exception(pht('Failed to decode rule data.'));
    }

    $conditions = array();
    foreach ($data['conditions'] as $condition) {
      if ($condition === null) {
        // We manage this as a sparse array on the client, so may receive
        // NULL if conditions have been removed.
        continue;
      }

View on GitHub (pinned to 5720a38cfe)