phacility/phabricator · error · Exception

Expected "%s" property to contain a dictionary.

Error message

Expected "%s" property to contain a dictionary.

What it means

The Buildkite webhook endpoint (reached at harbormaster/buildkite/) decodes the raw JSON body with phutil_json_decode, accepts only event == 'build.finished', and then requires the top-level 'build' property to be an array (PHP dictionary). If 'build' is absent (idx returns null) or a scalar (string, number), this plain Exception is thrown and Buildkite receives a 500 for the webhook delivery.

Source

Thrown at src/applications/harbormaster/controller/HarbormasterBuildkiteHookController.php:24

  public function shouldRequireLogin() {
    return false;
  }

  /**
   * @phutil-external-symbol class PhabricatorStartup
   */
  public function handleRequest(AphrontRequest $request) {
    $raw_body = PhabricatorStartup::getRawInput();
    $body = phutil_json_decode($raw_body);

    $event = idx($body, 'event');
    if ($event != 'build.finished') {
      return $this->newHookResponse(pht('OK: Ignored event.'));
    }

    $build = idx($body, 'build');
    if (!is_array($build)) {
      throw new Exception(
        pht(
          'Expected "%s" property to contain a dictionary.',
          'build'));
    }

    $meta_data = idx($build, 'meta_data');
    if (!is_array($meta_data)) {
      throw new Exception(
        pht(
          'Expected "%s" property to contain a dictionary.',
          'build.meta_data'));
    }

    $target_phid = idx($meta_data, 'buildTargetPHID');
    if (!$target_phid) {
      return $this->newHookResponse(pht('OK: No Harbormaster target PHID.'));
    }

View on GitHub (pinned to 5720a38cfe)

Solutions

  1. Send the full, unmodified Buildkite 'build.finished' webhook payload, which always contains a 'build' object.
  2. Verify with curl that your test body includes a JSON object under 'build' (e.g. "build": {"state":"passed", ...}).
  3. Make sure only Buildkite is configured to deliver to this endpoint and nothing rewrites the body.
  4. If you maintain an integration, pre-validate the payload shape (json object at build) before forwarding.

Example fix

// before (sender)
curl -d '{"event":"build.finished","state":"passed"}' https://phabricator/harbormaster/buildkite/

// after (sender) - include the build dictionary
curl -d '{"event":"build.finished","build":{"state":"passed","meta_data":{"buildTargetPHID":"PHID-HMBT-xxx"}}}' https://phabricator/harbormaster/buildkite/
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-validate a payload before delivering it to the hook (test harness):
$body = phutil_json_decode($raw);
if (idx($body, 'event') !== 'build.finished') { return; }
if (!is_array(idx($body, 'build'))) {
  // do not send / fix payload shape first
}

Type guard

function isBuildkiteFinishedPayload(array $body) {
  return isset($body['event'])
    && $body['event'] === 'build.finished'
    && is_array($body['build'] ?? null);
}

Try / catch

try {
  return id(new HarbormasterBuildkiteHookController())->handleRequest($request);
} catch (Exception $e) {
  // Return HTTP 400 to Buildkite instead of a raw 500 so the delivery is retryable
  return new Aphront400Response();
}

Prevention

When it happens

Trigger: POSTing a payload like {"event":"build.finished"} or {"event":"build.finished","build":"finished"} to the hook URL; pointing a non-Buildkite CI system or a hand-rolled integration at this endpoint; a proxy or test harness mangling the JSON body; a future Buildkite API change that renames or restructures the build object.

Common situations: Testing the webhook with a minimal curl payload during setup; build.finished events from pipelines whose payload was customized; sending GitHub/other provider webhooks to the wrong URL.

Related errors


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