Merge pull request #9147 from JoshiJoshiJoshi/feat-extend-function-headers

Feat: Add executionId and client IP to function headers
This commit is contained in:
Matej Bačo 2025-08-27 10:10:59 +02:00 committed by GitHub
commit 8a76979ba8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 38 additions and 9 deletions

View file

@ -356,11 +356,16 @@ 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';
$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);
@ -372,7 +377,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);
@ -392,8 +396,6 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
}
}
$executionId = ID::unique();
$execution = new Document([
'$id' => $executionId,
'$permissions' => [],

View file

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

View file

@ -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,8 +230,9 @@ class Create extends Base
$headers['x-appwrite-country-code'] = '';
$headers['x-appwrite-continent-code'] = '';
$headers['x-appwrite-continent-eu'] = 'false';
$ip = $request->getIP();
$headers['x-appwrite-client-ip'] = $ip;
$ip = $headers['x-real-ip'] ?? '';
if (!empty($ip)) {
$record = $geodb->get($ip);
@ -249,7 +252,7 @@ class Create extends Base
}
}
$executionId = ID::unique();
$status = $async ? 'waiting' : 'processing';

View file

@ -264,6 +264,8 @@ 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() ?? '';
@ -276,7 +278,6 @@ class Functions extends Action
}
}
$executionId = ID::unique();
$execution = new Document([
'$id' => $executionId,
'$permissions' => $user->isEmpty() ? [] : [Permission::read(Role::user($user->getId()))],
@ -397,6 +398,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 ?? '';
@ -409,6 +411,8 @@ class Functions extends Action
/** 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 +420,6 @@ class Functions extends Action
}
}
$executionId = ID::unique();
$execution = new Document([
'$id' => $executionId,
'$permissions' => $user->isEmpty() ? [] : [Permission::read(Role::user($user->getId()))],

View file

@ -172,6 +172,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

View file

@ -1173,6 +1173,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',
@ -1545,6 +1551,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'] ?? '');
}, 20_000, 500);
$this->cleanupFunction($functionId);

View file

@ -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);
@ -118,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);

View file

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