diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ab7933b..3f2c77a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -29,4 +29,16 @@ jobs: - name: Run Tests run: | - docker run --rm -v $PWD:/app -v /tmp:/tmp -v /var/run/docker.sock:/var/run/docker.sock --network executor_runtimes -w /app phpswoole/swoole:5.1.2-php8.3-alpine sh -c "apk update && apk add docker-cli zip unzip && composer install --profile --ignore-platform-reqs && composer test" + docker run --rm \ + -v $PWD:/app \ + -v /tmp:/tmp \ + -v /var/run/docker.sock:/var/run/docker.sock \ + --network executor_runtimes \ + -w /app \ + phpswoole/swoole:5.1.2-php8.3-alpine \ + sh -c " + apk update && \ + apk add docker-cli zip unzip && \ + composer install --profile --ignore-platform-reqs && \ + composer test + " diff --git a/app/controllers.php b/app/controllers.php index d6db42a..86c87aa 100644 --- a/app/controllers.php +++ b/app/controllers.php @@ -150,7 +150,7 @@ ->param('runtimeId', '', new Text(64), 'The runtimeID to execute.') ->param('body', '', new Text(20971520), 'Data to be forwarded to the function, this is user specified.', true) ->param('path', '/', new Text(2048), 'Path from which execution comes.', true) - ->param('method', 'GET', new Whitelist(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], true), 'Path from which execution comes.', true) + ->param('method', 'GET', new Whitelist(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD'], true), 'Path from which execution comes.', true) ->param('headers', [], new AnyOf([new Text(65535), new Assoc()], AnyOf::TYPE_MIXED), 'Headers passed into runtime.', true) ->param('timeout', 15, new Integer(true), 'Function maximum execution time in seconds.', true) // Runtime-related diff --git a/src/Executor/Runner/Docker.php b/src/Executor/Runner/Docker.php index e695fad..97efce6 100644 --- a/src/Executor/Runner/Docker.php +++ b/src/Executor/Runner/Docker.php @@ -1148,7 +1148,10 @@ public function createExecution( } while ((++$attempts < $retryAttempts) || (\microtime(true) - $startTime < $timeout)); // Error occurred - if ($executionResponse['errNo'] !== CURLE_OK) { + if ( + $executionResponse['errNo'] !== CURLE_OK && + $executionResponse['errNo'] !== CURLE_PARTIAL_FILE // Head request may return partial file + ) { $log->addExtra('activeRuntime', $this->activeRuntimes->get($runtimeName)); $log->addExtra('error', $executionResponse['error']); $log->addTag('hostname', $hostname); diff --git a/tests/Client.php b/tests/Client.php index 6014981..5cf3c36 100644 --- a/tests/Client.php +++ b/tests/Client.php @@ -131,7 +131,14 @@ public function call(string $method, string $path = '', array $headers = [], arr return $len; }); - if ($method != self::METHOD_GET) { + if ($method === self::METHOD_HEAD) { + curl_setopt($ch, CURLOPT_NOBODY, true); // This is crucial for HEAD requests + curl_setopt($ch, CURLOPT_HEADER, false); + } else { + curl_setopt($ch, CURLOPT_NOBODY, false); + } + + if ($method != self::METHOD_GET && $method != self::METHOD_HEAD) { curl_setopt($ch, CURLOPT_POSTFIELDS, $query); } @@ -151,7 +158,7 @@ public function call(string $method, string $path = '', array $headers = [], arr $responseType = $responseHeaders['content-type'] ?? ''; $responseStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE); - if ($decode) { + if ($decode && $method !== self::METHOD_HEAD) { $strpos = strpos($responseType, ';'); $strpos = \is_bool($strpos) ? \strlen($responseType) : $strpos; switch (substr($responseType, 0, $strpos)) { @@ -177,6 +184,9 @@ public function call(string $method, string $path = '', array $headers = [], arr $json = null; break; } + } elseif ($method === self::METHOD_HEAD) { + // For HEAD requests, always set body to empty string regardless of decode flag + $responseBody = ''; } if ((curl_errno($ch)/* || 200 != $responseStatus*/)) { diff --git a/tests/ExecutorTest.php b/tests/ExecutorTest.php index 6156c98..4592a4a 100644 --- a/tests/ExecutorTest.php +++ b/tests/ExecutorTest.php @@ -547,6 +547,14 @@ public function testExecute(): void $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringStartsWith('multipart/form-data', $response['headers']['content-type']); + /** Execute HEAD request */ + $response = $this->client->call(Client::METHOD_HEAD, '/runtimes/test-exec-coldstart/executions'); + + $this->assertEquals(404, $response['headers']['status-code']); // not found + // For HEAD requests, the body should be empty but headers should be present + $this->assertEmpty($response['body']); + $this->assertArrayHasKey('content-type', $response['headers']); + /** Delete runtime */ $response = $this->client->call(Client::METHOD_DELETE, '/runtimes/test-exec-coldstart', [], []); $this->assertEquals(200, $response['headers']['status-code']);