From 5de81e184eac700d1595277fc9d2a1acb6cdb96c Mon Sep 17 00:00:00 2001 From: Deepak Kumar Mishra Date: Mon, 20 Jul 2026 11:09:16 +0530 Subject: [PATCH 1/2] fix(CLI-1779): restrict empty-body JSON fallback to POST only PUT/PATCH endpoints with no requestBody properties (e.g. site-instances:domain:add) must not receive `{}`: Guzzle creates a non-seekable stream for the empty stdClass which causes `curl_setopt_array(): Stream does not support seeking` on PHP 8.x. The empty-body guard (needed so body-less POST requests get a Content-Type header) is now conditional on the HTTP method being POST. Co-Authored-By: Claude Sonnet 4.6 --- src/Command/Api/ApiBaseCommand.php | 10 +++++ .../src/Commands/Api/ApiCommandTest.php | 42 +++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/Command/Api/ApiBaseCommand.php b/src/Command/Api/ApiBaseCommand.php index 4a3eb4a67..133d4e73d 100644 --- a/src/Command/Api/ApiBaseCommand.php +++ b/src/Command/Api/ApiBaseCommand.php @@ -114,6 +114,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int $acquiaCloudClient->addOption('headers', [ 'Accept' => 'application/hal+json, version=2', ]); + // Some POST endpoints have no request body (e.g. schedule-delete). The + // Cloud Platform API requires both a Content-Type header and a body for + // all POST requests. Guzzle's `json` option satisfies both; passing an + // empty object sends `{}` with Content-Type: application/json. + // PUT/PATCH endpoints with no body (e.g. site-instances:domain:add) must + // not send `{}`: Guzzle creates a non-seekable stream which triggers + // `curl_setopt_array(): Stream does not support seeking`. + if (strtoupper($this->method) === 'POST' && empty($this->postParams)) { + $acquiaCloudClient->addOption('json', new \stdClass()); + } try { if ($this->output->isVeryVerbose()) { diff --git a/tests/phpunit/src/Commands/Api/ApiCommandTest.php b/tests/phpunit/src/Commands/Api/ApiCommandTest.php index 57c362a3e..448aacc04 100644 --- a/tests/phpunit/src/Commands/Api/ApiCommandTest.php +++ b/tests/phpunit/src/Commands/Api/ApiCommandTest.php @@ -59,6 +59,48 @@ public function testTrustedProxiesCommandExecutesHttpGet(): void $this->assertEquals(0, $this->getStatusCode()); } + /** + * A body-less POST (no requestBody in the spec) must send an empty JSON + * object so the Cloud Platform API receives both a Content-Type header and + * a body, preventing HTTP 415 / HTTP 400 errors. + */ + public function testBodylessPostSendsEmptyJsonBody(): void + { + $agreementUuid = 'da1c0a8e-ff69-45db-88fc-acd6d2affbb7'; + $this->mockRequest('postAcceptAgreement', $agreementUuid); + $this->clientProphecy->addOption('headers', ['Accept' => 'application/hal+json, version=2']) + ->shouldBeCalled(); + $this->clientProphecy->addOption('json', new \stdClass()) + ->shouldBeCalled(); + $this->command = $this->getApiCommandByName('api:agreements:accept'); + $this->executeCommand(['agreementUuid' => $agreementUuid]); + $this->assertEquals(0, $this->getStatusCode()); + } + + /** + * A body-less PUT (requestBody with no properties, required: false) must + * NOT send `{}`. Guzzle creates a non-seekable stream for the empty object + * which triggers `curl_setopt_array(): Stream does not support seeking`. + */ + public function testBodylessPutDoesNotSendEmptyJsonBody(): void + { + $siteId = '3e8ecbec-ea7c-4260-8414-ef2938c859bc'; + $environmentId = 'd3f7270e-c45f-4801-9308-5e8afe84a323'; + $domainName = 'example.com'; + $this->mockRequest('add_domain_to_site', [$siteId, $environmentId, $domainName]); + $this->clientProphecy->addOption('headers', ['Accept' => 'application/hal+json, version=2']) + ->shouldBeCalled(); + $this->clientProphecy->addOption('json', new \stdClass()) + ->shouldNotBeCalled(); + $this->command = $this->getApiCommandByName('api:site-instances:domain:add'); + $this->executeCommand([ + 'domainName' => $domainName, + 'environmentId' => $environmentId, + 'siteId' => $siteId, + ]); + $this->assertEquals(0, $this->getStatusCode()); + } + public function testTaskWait(): void { $environmentId = '24-a47ac10b-58cc-4372-a567-0e02b2c3d470'; From c7dc3f8a8bff188667ce9cf4f67d6c6f8d4e6f1b Mon Sep 17 00:00:00 2001 From: Deepak Kumar Mishra Date: Tue, 21 Jul 2026 15:00:38 +0530 Subject: [PATCH 2/2] remove whitespace. --- tests/phpunit/src/Commands/Api/ApiCommandTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/phpunit/src/Commands/Api/ApiCommandTest.php b/tests/phpunit/src/Commands/Api/ApiCommandTest.php index 3ce9a7652..333fd1c71 100644 --- a/tests/phpunit/src/Commands/Api/ApiCommandTest.php +++ b/tests/phpunit/src/Commands/Api/ApiCommandTest.php @@ -100,7 +100,7 @@ public function testBodylessPutDoesNotSendEmptyJsonBody(): void ]); $this->assertEquals(0, $this->getStatusCode()); } - + /** * oneOf entries that lack a 'type' key (e.g. unresolved $ref) must be * skipped silently rather than throwing an "Undefined array key" warning.