Merge pull request #10427 from appwrite/feat-set-cookie-array-of-headers

Feat: Support array headers for set-cookie
This commit is contained in:
Matej Bačo 2025-09-03 18:43:33 +02:00 committed by GitHub
commit 1b6fd73314
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 74 additions and 36 deletions

View file

@ -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

View file

@ -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

View file

@ -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<string> $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);
}
}
/**

View file

@ -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) {

View file

@ -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);
}

View file

@ -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);
}