diff --git a/app/controllers/general.php b/app/controllers/general.php index 40ce66b574..50d56ff9fd 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -562,15 +562,18 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw cpus: $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT, memory: $spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT, logging: $resource->getAttribute('logging', true), - requestTimeout: 30 + requestTimeout: 30, + responseFormat: Executor::RESPONSE_FORMAT_ARRAY_HEADERS ); + $headerOverrides = []; + // Branded 404 override $isResponseBranded = false; if ($executionResponse['statusCode'] === 404 && $deployment->getAttribute('adapter', '') === 'static') { $layout = new View(__DIR__ . '/../views/general/404.phtml'); $executionResponse['body'] = $layout->render(); - $executionResponse['headers']['content-length'] = \strlen($executionResponse['body']); + $headerOverrides['content-length'] = \strlen($executionResponse['body']); $isResponseBranded = true; } @@ -580,15 +583,16 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw $transformation = new Transformation(); $transformation->addAdapter(new Preview()); $transformation->setInput($executionResponse['body']); - $transformation->setTraits($executionResponse['headers']); + + $simpleHeaders = []; + foreach ($executionResponse['headers'] as $key => $value) { + $simpleHeaders[$key] = \is_array($value) ? \implode(', ', $value) : $value; + } + + $transformation->setTraits($simpleHeaders); if ($isPreview && $transformation->transform()) { $executionResponse['body'] = $transformation->getOutput(); - - foreach ($executionResponse['headers'] as $key => $value) { - if (\strtolower($key) === 'content-length') { - $executionResponse['headers'][$key] = \strlen($executionResponse['body']); - } - } + $headerOverrides['content-length'] = \strlen($executionResponse['body']); } } } @@ -602,25 +606,33 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw ->setParam('code', $executionResponse['statusCode']); $executionResponse['body'] = $layout->render(); - foreach ($executionResponse['headers'] as $key => $value) { - if (\strtolower($key) === 'content-length') { - $executionResponse['headers'][$key] = \strlen($executionResponse['body']); - } elseif (\strtolower($key) === 'content-type') { - $executionResponse['headers'][$key] = 'text/html'; - } - } + + $headerOverrides['content-length'] = \strlen($executionResponse['body']); + $headerOverrides['content-type'] = 'text/html'; } if ($deployment->getAttribute('resourceType') === 'functions') { - $executionResponse['headers']['x-appwrite-execution-id'] = $execution->getId(); + $headerOverrides['x-appwrite-execution-id'] = $execution->getId(); } elseif ($deployment->getAttribute('resourceType') === 'sites') { - $executionResponse['headers']['x-appwrite-log-id'] = $execution->getId(); + $headerOverrides['x-appwrite-log-id'] = $execution->getId(); + } + + foreach ($headerOverrides as $key => $value) { + if (\array_key_exists($key, $executionResponse['headers'])) { + if (\is_array($executionResponse['headers'][$key])) { + $executionResponse['headers'][$key][] = $value; + } else { + $executionResponse['headers'][$key] = [$executionResponse['headers'][$key], $value]; + } + } else { + $executionResponse['headers'][$key] = $value; + } } $headersFiltered = []; foreach ($executionResponse['headers'] as $key => $value) { if (\in_array(\strtolower($key), FUNCTION_ALLOWLIST_HEADERS_RESPONSE)) { - $headersFiltered[] = ['name' => $key, 'value' => $value]; + $headersFiltered[] = ['name' => $key, 'value' => \is_array($value) ? \implode(', ', $value) : $value]; } } @@ -687,7 +699,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw $headers = []; foreach (($executionResponse['headers'] ?? []) as $key => $value) { - $headers[] = ['name' => $key, 'value' => $value]; + $headers[] = ['name' => $key, 'value' => \is_array($value) ? \implode(', ', $value) : $value]; } $execution->setAttribute('responseBody', $executionResponse['body'] ?? ''); @@ -696,16 +708,17 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw $body = $execution['responseBody'] ?? ''; $contentType = 'text/plain'; - foreach ($execution['responseHeaders'] as $header) { - if (\strtolower($header['name']) === 'content-type') { - $contentType = $header['value']; - } - - if (\strtolower($header['name']) === 'transfer-encoding') { + foreach ($executionResponse['headers'] as $name => $values) { + if (\strtolower($name) === 'content-type') { + $contentType = \is_array($values) ? $values[0] : $values; continue; } - $response->addHeader(\strtolower($header['name']), $header['value']); + if (\strtolower($name) === 'transfer-encoding') { + continue; + } + + $response->setHeader($name, $values); } $response diff --git a/docker-compose.yml b/docker-compose.yml index 87385aa086..da6362b4c4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -966,7 +966,7 @@ services: hostname: exc1 <<: *x-logging stop_signal: SIGINT - image: openruntimes/executor:0.8.1 + image: openruntimes/executor:0.11.0 restart: unless-stopped networks: - appwrite diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index 35c43ef07c..4a59ce77ad 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -411,6 +411,8 @@ class Response extends SwooleResponse */ protected static bool $showSensitive = false; + protected SwooleHTTPResponse $swoole; + /** * Response constructor. * @@ -418,6 +420,8 @@ class Response extends SwooleResponse */ public function __construct(SwooleHTTPResponse $response) { + $this->swoole = $response; + $this // General ->setModel(new None()) @@ -955,12 +959,18 @@ class Response extends SwooleResponse * Set Header * * @param string $key - * @param string $value + * @param string|array $value * @return void */ - public function setHeader(string $key, string $value): void + public function setHeader(string $key, mixed $value): void { - $this->sendHeader($key, $value); + if (is_array($value)) { + // Temporary solution to support proxying Set-cookie (2 cookies, 1 name) + // Ideally this would live in http library, supporting array of values in all adapters + $this->swoole->header($key, $value); + } else { + $this->sendHeader($key, $value); + } } /** diff --git a/src/Executor/Executor.php b/src/Executor/Executor.php index df4c5fd8f5..30c6132b76 100644 --- a/src/Executor/Executor.php +++ b/src/Executor/Executor.php @@ -9,6 +9,12 @@ use Utopia\System\System; class Executor { + // 0.8.6 is last version with object-based headers + public const RESPONSE_FORMAT_OBJECT_HEADERS = '0.10.0'; + + // 0.9.0 is first version with array-based headers + public const RESPONSE_FORMAT_ARRAY_HEADERS = '0.11.0'; + public const METHOD_GET = 'GET'; public const METHOD_POST = 'POST'; public const METHOD_PUT = 'PUT'; @@ -170,6 +176,7 @@ class Executor * @param string $entrypoint * @param string $runtimeEntrypoint * @param bool $logging + * @param string $responseFormat * * @return array */ @@ -190,7 +197,8 @@ class Executor int $memory, bool $logging, string $runtimeEntrypoint = '', - ?int $requestTimeout = null + ?int $requestTimeout = null, + string $responseFormat = self::RESPONSE_FORMAT_OBJECT_HEADERS ) { if (empty($headers['host'])) { $headers['host'] = System::getEnv('_APP_DOMAIN', ''); @@ -232,7 +240,7 @@ class Executor $requestTimeout = $timeout + 15; } - $response = $this->call($this->endpoint, self::METHOD_POST, $route, [ 'x-opr-runtime-id' => $runtimeId, 'content-type' => 'multipart/form-data', 'accept' => 'multipart/form-data' ], $params, true, $requestTimeout); + $response = $this->call($this->endpoint, self::METHOD_POST, $route, [ 'x-opr-runtime-id' => $runtimeId, 'content-type' => 'multipart/form-data', 'accept' => 'multipart/form-data', 'x-executor-response-format' => $responseFormat ], $params, true, $requestTimeout); $status = $response['headers']['status-code']; if ($status >= 400) { diff --git a/tests/e2e/Services/Sites/SitesCustomServerTest.php b/tests/e2e/Services/Sites/SitesCustomServerTest.php index d28e2fe8b4..c8301b9428 100644 --- a/tests/e2e/Services/Sites/SitesCustomServerTest.php +++ b/tests/e2e/Services/Sites/SitesCustomServerTest.php @@ -2775,11 +2775,13 @@ class SitesCustomServerTest extends Scope $proxyClient->setEndpoint('http://' . $domain); $response = $proxyClient->call(Client::METHOD_GET, '/cookies', [ - 'cookie' => 'custom-session-id=abcd123' + 'cookie' => 'custom-session-id=abcd123; custom-user-id=efgh456' ]); $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals("abcd123", $response['body']); + $this->assertEquals("abcd123;efgh456", $response['body']); + $this->assertEquals("value-one", $response['cookies']['my-cookie-one']); + $this->assertEquals("value-two", $response['cookies']['my-cookie-two']); $this->cleanupSite($siteId); } diff --git a/tests/resources/sites/astro/src/pages/cookies.js b/tests/resources/sites/astro/src/pages/cookies.js index 5f5efac833..65911c8aeb 100644 --- a/tests/resources/sites/astro/src/pages/cookies.js +++ b/tests/resources/sites/astro/src/pages/cookies.js @@ -1,4 +1,9 @@ export async function GET(context) { const sessionId = context.cookies.get("custom-session-id")?.value ?? 'Custom session ID missing'; - return new Response(sessionId); + const userId = context.cookies.get("custom-user-id")?.value ?? 'Custom user ID missing'; + + context.cookies.set('my-cookie-one', 'value-one'); + context.cookies.set('my-cookie-two', 'value-two'); + + return new Response(sessionId + ";" + userId); }