From ed16808930e7d4854379a7fadb19b4489bd708c0 Mon Sep 17 00:00:00 2001 From: Khushboo Verma Date: Wed, 20 Aug 2025 16:35:40 +0530 Subject: [PATCH 1/4] Add executionId and client IP to execution headers --- app/controllers/general.php | 6 ++++-- .../Modules/Functions/Http/Executions/Create.php | 5 ++++- src/Appwrite/Platform/Workers/Functions.php | 9 +++++++-- .../e2e/Services/Functions/FunctionsCustomClientTest.php | 5 +++++ .../e2e/Services/Functions/FunctionsCustomServerTest.php | 6 ++++++ tests/resources/functions/basic/index.js | 2 ++ 6 files changed, 28 insertions(+), 5 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index 40f861ad8c..dc284a3ebe 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -355,11 +355,15 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw } } + $executionId = ID::unique(); + $headers = \array_merge([], $requestHeaders); + $headers['x-appwrite-execution-id'] = $executionId ?? ''; $headers['x-appwrite-user-id'] = ''; $headers['x-appwrite-country-code'] = ''; $headers['x-appwrite-continent-code'] = ''; $headers['x-appwrite-continent-eu'] = 'false'; + $headers['x-appwrite-client-ip'] = $request->getIP(); $jwtExpiry = $resource->getAttribute('timeout', 900) + 60; // 1min extra to account for possible cold-starts $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $jwtExpiry, 0); @@ -391,8 +395,6 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw } } - $executionId = ID::unique(); - $execution = new Document([ '$id' => $executionId, '$permissions' => [], diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php index ef31d5a79c..c7703231f1 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php @@ -221,6 +221,8 @@ class Create extends Base 'scopes' => $function->getAttribute('scopes', []) ]); + $executionId = ID::unique(); + $headers['x-appwrite-execution-id'] = $executionId ?? ''; $headers['x-appwrite-key'] = API_KEY_DYNAMIC . '_' . $apiKey; $headers['x-appwrite-trigger'] = 'http'; $headers['x-appwrite-user-id'] = $user->getId() ?? ''; @@ -228,6 +230,7 @@ class Create extends Base $headers['x-appwrite-country-code'] = ''; $headers['x-appwrite-continent-code'] = ''; $headers['x-appwrite-continent-eu'] = 'false'; + $headers['x-appwrite-client-ip'] = $request->getIP(); $ip = $headers['x-real-ip'] ?? ''; if (!empty($ip)) { @@ -249,7 +252,7 @@ class Create extends Base } } - $executionId = ID::unique(); + $status = $async ? 'waiting' : 'processing'; diff --git a/src/Appwrite/Platform/Workers/Functions.php b/src/Appwrite/Platform/Workers/Functions.php index 2e25248d9a..e754c3c7b6 100644 --- a/src/Appwrite/Platform/Workers/Functions.php +++ b/src/Appwrite/Platform/Workers/Functions.php @@ -264,10 +264,13 @@ class Functions extends Action string $jwt = null, string $event = null, ): void { + $executionId = ID::unique(); + $headers['x-appwrite-execution-id'] = $executionId ?? ''; $headers['x-appwrite-trigger'] = $trigger; $headers['x-appwrite-event'] = $event ?? ''; $headers['x-appwrite-user-id'] = $user->getId() ?? ''; $headers['x-appwrite-user-jwt'] = $jwt ?? ''; + $headers['x-appwrite-client-ip'] = ''; $headersFiltered = []; foreach ($headers as $key => $value) { @@ -276,7 +279,6 @@ class Functions extends Action } } - $executionId = ID::unique(); $execution = new Document([ '$id' => $executionId, '$permissions' => $user->isEmpty() ? [] : [Permission::read(Role::user($user->getId()))], @@ -397,6 +399,7 @@ class Functions extends Action 'scopes' => $function->getAttribute('scopes', []) ]); + $headers['x-appwrite-execution-id'] = $executionId ?? ''; $headers['x-appwrite-key'] = API_KEY_DYNAMIC . '_' . $apiKey; $headers['x-appwrite-trigger'] = $trigger; $headers['x-appwrite-event'] = $event ?? ''; @@ -405,10 +408,13 @@ class Functions extends Action $headers['x-appwrite-country-code'] = ''; $headers['x-appwrite-continent-code'] = ''; $headers['x-appwrite-continent-eu'] = 'false'; + $headers['x-appwrite-client-ip'] = ''; /** Create execution or update execution status */ $execution = $dbForProject->getDocument('executions', $executionId ?? ''); if ($execution->isEmpty()) { + $executionId = ID::unique(); + $headers['x-appwrite-execution-id'] = $executionId; $headersFiltered = []; foreach ($headers as $key => $value) { if (\in_array(\strtolower($key), FUNCTION_ALLOWLIST_HEADERS_REQUEST)) { @@ -416,7 +422,6 @@ class Functions extends Action } } - $executionId = ID::unique(); $execution = new Document([ '$id' => $executionId, '$permissions' => $user->isEmpty() ? [] : [Permission::read(Role::user($user->getId()))], diff --git a/tests/e2e/Services/Functions/FunctionsCustomClientTest.php b/tests/e2e/Services/Functions/FunctionsCustomClientTest.php index a60c9e59cf..724cf87486 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomClientTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomClientTest.php @@ -159,6 +159,11 @@ class FunctionsCustomClientTest extends Scope $this->assertNotEmpty($output['APPWRITE_FUNCTION_JWT']); $this->assertEquals($this->getProject()['$id'], $output['APPWRITE_FUNCTION_PROJECT_ID']); + $executionId = $execution['body']['$id'] ?? ''; + $this->assertNotEmpty($output['APPWRITE_FUNCTION_EXECUTION_ID']); + $this->assertEquals($executionId, $output['APPWRITE_FUNCTION_EXECUTION_ID']); + $this->assertNotEmpty($output['APPWRITE_FUNCTION_CLIENT_IP']); + $execution = $this->createExecution($functionId, [ 'body' => 'foobar', 'async' => true diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index ff99033fdf..4e59c881eb 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -1136,6 +1136,12 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals(1, $output['APPWRITE_FUNCTION_CPUS']); $this->assertEquals(1024, $output['APPWRITE_FUNCTION_MEMORY']); + // Test execution ID and client IP + $executionId = $execution['body']['$id'] ?? ''; + $this->assertNotEmpty($output['APPWRITE_FUNCTION_EXECUTION_ID']); + $this->assertEquals($executionId, $output['APPWRITE_FUNCTION_EXECUTION_ID']); + $this->assertNotEmpty($output['APPWRITE_FUNCTION_CLIENT_IP']); + // Change the specs to 1vcpu 512mb $function = $this->client->call(Client::METHOD_PUT, '/functions/' . $data['functionId'], array_merge([ 'content-type' => 'application/json', diff --git a/tests/resources/functions/basic/index.js b/tests/resources/functions/basic/index.js index 1eb9d38c58..47f7100b5b 100644 --- a/tests/resources/functions/basic/index.js +++ b/tests/resources/functions/basic/index.js @@ -41,6 +41,8 @@ module.exports = async(context) => { 'APPWRITE_FUNCTION_PROJECT_ID' : process.env.APPWRITE_FUNCTION_PROJECT_ID, 'APPWRITE_FUNCTION_MEMORY' : process.env.APPWRITE_FUNCTION_MEMORY, 'APPWRITE_FUNCTION_CPUS' : process.env.APPWRITE_FUNCTION_CPUS, + 'APPWRITE_FUNCTION_EXECUTION_ID': context.req.headers['x-appwrite-execution-id'] ?? '', + 'APPWRITE_FUNCTION_CLIENT_IP': context.req.headers['x-appwrite-client-ip'] ?? '', 'CUSTOM_VARIABLE' : process.env.CUSTOM_VARIABLE }, +statusCode); } \ No newline at end of file From 145850520191ab0ea5aae7c8cda1901e5b6f4201 Mon Sep 17 00:00:00 2001 From: Khushboo Verma Date: Tue, 26 Aug 2025 12:55:51 +0530 Subject: [PATCH 2/4] Address PR comments --- app/init/constants.php | 2 +- src/Appwrite/Platform/Workers/Functions.php | 2 -- tests/e2e/Services/Functions/FunctionsCustomServerTest.php | 3 +++ tests/e2e/Services/FunctionsSchedule/FunctionsScheduleTest.php | 3 +++ 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/init/constants.php b/app/init/constants.php index 689ab69fa4..f7a9d5341b 100644 --- a/app/init/constants.php +++ b/app/init/constants.php @@ -146,7 +146,7 @@ const MAX_OUTPUT_CHUNK_SIZE = 10 * 1024 * 1024; // 10MB const APP_FUNCTION_LOG_LENGTH_LIMIT = 1000000; const APP_FUNCTION_ERROR_LENGTH_LIMIT = 1000000; // Function headers -const FUNCTION_ALLOWLIST_HEADERS_REQUEST = ['content-type', 'agent', 'content-length', 'host']; +const FUNCTION_ALLOWLIST_HEADERS_REQUEST = ['content-type', 'agent', 'content-length', 'host', 'x-appwrite-client-ip']; const FUNCTION_ALLOWLIST_HEADERS_RESPONSE = ['content-type', 'content-length']; // Message types const MESSAGE_TYPE_EMAIL = 'email'; diff --git a/src/Appwrite/Platform/Workers/Functions.php b/src/Appwrite/Platform/Workers/Functions.php index ea05909a21..7413b9fb52 100644 --- a/src/Appwrite/Platform/Workers/Functions.php +++ b/src/Appwrite/Platform/Workers/Functions.php @@ -270,7 +270,6 @@ class Functions extends Action $headers['x-appwrite-event'] = $event ?? ''; $headers['x-appwrite-user-id'] = $user->getId() ?? ''; $headers['x-appwrite-user-jwt'] = $jwt ?? ''; - $headers['x-appwrite-client-ip'] = ''; $headersFiltered = []; foreach ($headers as $key => $value) { @@ -408,7 +407,6 @@ class Functions extends Action $headers['x-appwrite-country-code'] = ''; $headers['x-appwrite-continent-code'] = ''; $headers['x-appwrite-continent-eu'] = 'false'; - $headers['x-appwrite-client-ip'] = ''; /** Create execution or update execution status */ $execution = $dbForProject->getDocument('executions', $executionId ?? ''); diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index a695f86e3f..1d844b9cf0 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -1538,6 +1538,9 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals(204, $lastExecution['responseStatusCode']); $this->assertStringContainsString($userId, $lastExecution['logs']); $this->assertStringContainsString('Event User', $lastExecution['logs']); + $this->assertNotEmpty($lastExecution['$id']); + $headers = array_column($lastExecution['requestHeaders'] ?? [], 'value', 'name'); + $this->assertEmpty($headers['x-appwrite-client-ip'] ?? ''); }, 10000, 500); $this->cleanupFunction($functionId); diff --git a/tests/e2e/Services/FunctionsSchedule/FunctionsScheduleTest.php b/tests/e2e/Services/FunctionsSchedule/FunctionsScheduleTest.php index c3dd2c7fc8..2375057e17 100644 --- a/tests/e2e/Services/FunctionsSchedule/FunctionsScheduleTest.php +++ b/tests/e2e/Services/FunctionsSchedule/FunctionsScheduleTest.php @@ -62,6 +62,9 @@ class FunctionsScheduleTest extends Scope $this->assertNotEmpty($asyncExecution['logs']); $this->assertNotEmpty($asyncExecution['errors']); $this->assertGreaterThan(0, $asyncExecution['duration']); + $this->assertNotEmpty($asyncExecution['$id']); + $headers = array_column($asyncExecution['requestHeaders'] ?? [], 'value', 'name'); + $this->assertEmpty($headers['x-appwrite-client-ip'] ?? ''); }, 60000, 500); $this->cleanupFunction($functionId); From 753175d796eb7ef3771394853129e71ae5f3742c Mon Sep 17 00:00:00 2001 From: Khushboo Verma Date: Tue, 26 Aug 2025 13:59:46 +0530 Subject: [PATCH 3/4] Replace x-real-ip with ->getIP() --- app/controllers/general.php | 4 ++-- .../Platform/Modules/Functions/Http/Executions/Create.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index e9b9c8b852..f4b31bc737 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -363,7 +363,8 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw $headers['x-appwrite-country-code'] = ''; $headers['x-appwrite-continent-code'] = ''; $headers['x-appwrite-continent-eu'] = 'false'; - $headers['x-appwrite-client-ip'] = $request->getIP(); + $ip = $request->getIP(); + $headers['x-appwrite-client-ip'] = $ip; $jwtExpiry = $resource->getAttribute('timeout', 900) + 60; // 1min extra to account for possible cold-starts $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $jwtExpiry, 0); @@ -375,7 +376,6 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw $headers['x-appwrite-trigger'] = 'http'; $headers['x-appwrite-user-jwt'] = ''; - $ip = $headers['x-real-ip'] ?? ''; if (!empty($ip)) { $record = $geodb->get($ip); diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php index 27a3569750..48fc2997dd 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php @@ -230,9 +230,9 @@ class Create extends Base $headers['x-appwrite-country-code'] = ''; $headers['x-appwrite-continent-code'] = ''; $headers['x-appwrite-continent-eu'] = 'false'; - $headers['x-appwrite-client-ip'] = $request->getIP(); + $ip = $request->getIP(); + $headers['x-appwrite-client-ip'] = $ip; - $ip = $headers['x-real-ip'] ?? ''; if (!empty($ip)) { $record = $geodb->get($ip); From c087449725a8a001cf07a11929a9ec49ea65210a Mon Sep 17 00:00:00 2001 From: Khushboo Verma Date: Wed, 27 Aug 2025 12:39:38 +0530 Subject: [PATCH 4/4] Fix schedule execution test --- .../e2e/Services/FunctionsSchedule/FunctionsScheduleTest.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/e2e/Services/FunctionsSchedule/FunctionsScheduleTest.php b/tests/e2e/Services/FunctionsSchedule/FunctionsScheduleTest.php index 2375057e17..c9ec978cba 100644 --- a/tests/e2e/Services/FunctionsSchedule/FunctionsScheduleTest.php +++ b/tests/e2e/Services/FunctionsSchedule/FunctionsScheduleTest.php @@ -121,7 +121,9 @@ class FunctionsScheduleTest extends Scope $this->assertEquals('scheduled', $execution['body']['status']); $this->assertEquals('PATCH', $execution['body']['requestMethod']); $this->assertEquals('/custom-path', $execution['body']['requestPath']); - $this->assertCount(0, $execution['body']['requestHeaders']); + $this->assertCount(1, $execution['body']['requestHeaders']); + $this->assertEquals('x-appwrite-client-ip', $execution['body']['requestHeaders'][0]['name']); + $this->assertNotEmpty($execution['body']['requestHeaders'][0]['value']); \sleep(120);