{"record":{"id":"0aad79818168a963","repo":"phacility/phabricator","slug":"expected-s-property-to-contain-a-dictionary","errorCode":null,"errorMessage":"Expected \"%s\" property to contain a dictionary.","messagePattern":"Expected \"(.+?)\" property to contain a dictionary\\.","errorType":"validation","errorClass":"Exception","httpStatus":null,"severity":"error","filePath":"src/applications/harbormaster/controller/HarbormasterBuildkiteHookController.php","lineNumber":24,"sourceCode":"  public function shouldRequireLogin() {\n    return false;\n  }\n\n  /**\n   * @phutil-external-symbol class PhabricatorStartup\n   */\n  public function handleRequest(AphrontRequest $request) {\n    $raw_body = PhabricatorStartup::getRawInput();\n    $body = phutil_json_decode($raw_body);\n\n    $event = idx($body, 'event');\n    if ($event != 'build.finished') {\n      return $this->newHookResponse(pht('OK: Ignored event.'));\n    }\n\n    $build = idx($body, 'build');\n    if (!is_array($build)) {\n      throw new Exception(\n        pht(\n          'Expected \"%s\" property to contain a dictionary.',\n          'build'));\n    }\n\n    $meta_data = idx($build, 'meta_data');\n    if (!is_array($meta_data)) {\n      throw new Exception(\n        pht(\n          'Expected \"%s\" property to contain a dictionary.',\n          'build.meta_data'));\n    }\n\n    $target_phid = idx($meta_data, 'buildTargetPHID');\n    if (!$target_phid) {\n      return $this->newHookResponse(pht('OK: No Harbormaster target PHID.'));\n    }\n","sourceCodeStart":6,"sourceCodeEnd":42,"githubUrl":"https://github.com/phacility/phabricator/blob/5720a38cfe95b00ca4be5016dd0d2f3195f4fa04/src/applications/harbormaster/controller/HarbormasterBuildkiteHookController.php#L6-L42","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Send the full, unmodified Buildkite 'build.finished' webhook payload, which always contains a 'build' object.","Verify with curl that your test body includes a JSON object under 'build' (e.g. \"build\": {\"state\":\"passed\", ...}).","Make sure only Buildkite is configured to deliver to this endpoint and nothing rewrites the body.","If you maintain an integration, pre-validate the payload shape (json object at build) before forwarding."],"exampleFix":"// before (sender)\ncurl -d '{\"event\":\"build.finished\",\"state\":\"passed\"}' https://phabricator/harbormaster/buildkite/\n\n// after (sender) - include the build dictionary\ncurl -d '{\"event\":\"build.finished\",\"build\":{\"state\":\"passed\",\"meta_data\":{\"buildTargetPHID\":\"PHID-HMBT-xxx\"}}}' https://phabricator/harbormaster/buildkite/","handlingStrategy":"try-catch","validationCode":"// Pre-validate a payload before delivering it to the hook (test harness):\n$body = phutil_json_decode($raw);\nif (idx($body, 'event') !== 'build.finished') { return; }\nif (!is_array(idx($body, 'build'))) {\n  // do not send / fix payload shape first\n}","typeGuard":"function isBuildkiteFinishedPayload(array $body) {\n  return isset($body['event'])\n    && $body['event'] === 'build.finished'\n    && is_array($body['build'] ?? null);\n}","tryCatchPattern":"try {\n  return id(new HarbormasterBuildkiteHookController())->handleRequest($request);\n} catch (Exception $e) {\n  // Return HTTP 400 to Buildkite instead of a raw 500 so the delivery is retryable\n  return new Aphront400Response();\n}","preventionTips":["Always forward Buildkite's payload verbatim; never synthesize minimal payloads in production integrations.","Point only Buildkite at this endpoint; route other providers to their own hooks.","Add payload-shape assertions in integration tests (event, build object, meta_data object) before shipping webhook plumbing."],"tags":["harbormaster","buildkite","webhook","payload-validation"],"backgroundTag":"webhook-payload-validation","analyzedSha":"5720a38cfe95b00ca4be5016dd0d2f3195f4fa04","analyzedAt":"2026-08-21T05:07:25.672Z","schemaVersion":2},"datasetVersion":"2026-08-21T11:28:35.574Z"}