From 159da8ba3106a077246b9ebe26dfcef480145d77 Mon Sep 17 00:00:00 2001 From: eldadfux Date: Wed, 25 Feb 2026 23:58:22 +0100 Subject: [PATCH 1/5] Fix 500 errors where we don't report duplication properly --- app/config/errors.php | 10 ++++ app/controllers/api/messaging.php | 27 +++++++--- src/Appwrite/Extend/Exception.php | 2 + .../Modules/Sites/Http/Sites/Create.php | 19 ++++--- .../Modules/Sites/Http/Sites/Update.php | 8 ++- tests/unit/Migration/MigrationTest.php | 51 +++++++++++++++++++ 6 files changed, 99 insertions(+), 18 deletions(-) create mode 100644 tests/unit/Migration/MigrationTest.php diff --git a/app/config/errors.php b/app/config/errors.php index a9ca0f79dd..bf0f4461f6 100644 --- a/app/config/errors.php +++ b/app/config/errors.php @@ -630,6 +630,11 @@ return [ 'description' => 'Site with the requested ID could not be found.', 'code' => 404, ], + Exception::SITE_ALREADY_EXISTS => [ + 'name' => Exception::SITE_ALREADY_EXISTS, + 'description' => 'Site with the requested ID already exists. Try again with a different ID or use ID.unique() to generate a unique ID.', + 'code' => 409, + ], Exception::SITE_TEMPLATE_NOT_FOUND => [ 'name' => Exception::SITE_TEMPLATE_NOT_FOUND, 'description' => 'Site Template with the requested ID could not be found.', @@ -1291,6 +1296,11 @@ return [ 'description' => 'Message with the requested ID could not be found.', 'code' => 404, ], + Exception::MESSAGE_ALREADY_EXISTS => [ + 'name' => Exception::MESSAGE_ALREADY_EXISTS, + 'description' => 'Message with the requested ID already exists. Try again with a different ID or use ID.unique() to generate a unique ID.', + 'code' => 409, + ], Exception::MESSAGE_MISSING_TARGET => [ 'name' => Exception::MESSAGE_MISSING_TARGET, 'description' => 'Message with the requested ID has no recipients (topics or users or targets).', diff --git a/app/controllers/api/messaging.php b/app/controllers/api/messaging.php index d0049c1397..a52ec70b12 100644 --- a/app/controllers/api/messaging.php +++ b/app/controllers/api/messaging.php @@ -3251,7 +3251,7 @@ Http::post('/v1/messaging/messages/email') } } - $message = $dbForProject->createDocument('messages', new Document([ + $message = new Document([ '$id' => $messageId, 'providerType' => MESSAGE_TYPE_EMAIL, 'topics' => $topics, @@ -3267,7 +3267,12 @@ Http::post('/v1/messaging/messages/email') 'attachments' => $attachments, ], 'status' => $status, - ])); + ]); + try { + $message = $dbForProject->createDocument('messages', $message); + } catch (DuplicateException) { + throw new Exception(Exception::MESSAGE_ALREADY_EXISTS); + } switch ($status) { case MessageStatus::PROCESSING: @@ -3400,7 +3405,7 @@ Http::post('/v1/messaging/messages/sms') } } - $message = $dbForProject->createDocument('messages', new Document([ + $message = new Document([ '$id' => $messageId, 'providerType' => MESSAGE_TYPE_SMS, 'topics' => $topics, @@ -3410,7 +3415,12 @@ Http::post('/v1/messaging/messages/sms') 'content' => $content, ], 'status' => $status, - ])); + ]); + try { + $message = $dbForProject->createDocument('messages', $message); + } catch (DuplicateException) { + throw new Exception(Exception::MESSAGE_ALREADY_EXISTS); + } switch ($status) { case MessageStatus::PROCESSING: @@ -3620,7 +3630,7 @@ Http::post('/v1/messaging/messages/push') $pushData['priority'] = $priority; } - $message = $dbForProject->createDocument('messages', new Document([ + $message = new Document([ '$id' => $messageId, 'providerType' => MESSAGE_TYPE_PUSH, 'topics' => $topics, @@ -3629,7 +3639,12 @@ Http::post('/v1/messaging/messages/push') 'scheduledAt' => $scheduledAt, 'data' => $pushData, 'status' => $status, - ])); + ]); + try { + $message = $dbForProject->createDocument('messages', $message); + } catch (DuplicateException) { + throw new Exception(Exception::MESSAGE_ALREADY_EXISTS); + } switch ($status) { case MessageStatus::PROCESSING: diff --git a/src/Appwrite/Extend/Exception.php b/src/Appwrite/Extend/Exception.php index e911faf77e..95a9c6ddac 100644 --- a/src/Appwrite/Extend/Exception.php +++ b/src/Appwrite/Extend/Exception.php @@ -166,6 +166,7 @@ class Exception extends \Exception /** Sites */ public const string SITE_NOT_FOUND = 'site_not_found'; + public const string SITE_ALREADY_EXISTS = 'site_already_exists'; public const string SITE_TEMPLATE_NOT_FOUND = 'site_template_not_found'; /** Functions */ @@ -365,6 +366,7 @@ class Exception extends \Exception /** Message */ public const string MESSAGE_NOT_FOUND = 'message_not_found'; + public const string MESSAGE_ALREADY_EXISTS = 'message_already_exists'; public const string MESSAGE_MISSING_TARGET = 'message_missing_target'; public const string MESSAGE_ALREADY_SENT = 'message_already_sent'; public const string MESSAGE_ALREADY_PROCESSING = 'message_already_processing'; diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php index 70781f41af..1da9196980 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php @@ -14,6 +14,7 @@ use Appwrite\Utopia\Response; use Utopia\Config\Config; use Utopia\Database\Database; use Utopia\Database\Document; +use Utopia\Database\Exception\Duplicate as DuplicateException; use Utopia\Database\Helpers\ID; use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; @@ -136,7 +137,7 @@ class Create extends Base throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'When connecting to VCS (Version Control System), you need to provide "installationId" and "providerBranch".'); } - $site = $dbForProject->createDocument('sites', new Document([ + $site = new Document([ '$id' => $siteId, 'enabled' => $enabled, 'live' => true, @@ -166,13 +167,17 @@ class Create extends Base 'runtimeSpecification' => $specification, 'buildRuntime' => $buildRuntime, 'adapter' => $adapter, - ])); + ]); + + try { + $site = $dbForProject->createDocument('sites', $site); + } catch (DuplicateException) { + throw new Exception(Exception::SITE_ALREADY_EXISTS); + } - // Git connect logic if (!empty($providerRepositoryId)) { $teamId = $project->getAttribute('teamId', ''); - - $repository = $dbForPlatform->createDocument('repositories', new Document([ + $repository = new Document([ '$id' => ID::unique(), '$permissions' => $this->getPermissions($teamId, $project->getId()), 'installationId' => $installation->getId(), @@ -184,8 +189,8 @@ class Create extends Base 'resourceInternalId' => $site->getSequence(), 'resourceType' => 'site', 'providerPullRequestIds' => [] - ])); - + ]); + $repository = $dbForPlatform->createDocument('repositories', $repository); $site->setAttribute('repositoryId', $repository->getId()); $site->setAttribute('repositoryInternalId', $repository->getSequence()); } diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php index 642d6bbe91..895e7ae3ef 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php @@ -190,11 +190,9 @@ class Update extends Base $repositoryInternalId = ''; } - // Git connect logic if (!$isConnected && !empty($providerRepositoryId)) { $teamId = $project->getAttribute('teamId', ''); - - $repository = $dbForPlatform->createDocument('repositories', new Document([ + $repository = new Document([ '$id' => ID::unique(), '$permissions' => $this->getPermissions($teamId, $project->getId()), 'installationId' => $installation->getId(), @@ -206,8 +204,8 @@ class Update extends Base 'resourceInternalId' => $site->getSequence(), 'resourceType' => 'site', 'providerPullRequestIds' => [] - ])); - + ]); + $repository = $dbForPlatform->createDocument('repositories', $repository); $repositoryId = $repository->getId(); $repositoryInternalId = $repository->getSequence(); } diff --git a/tests/unit/Migration/MigrationTest.php b/tests/unit/Migration/MigrationTest.php new file mode 100644 index 0000000000..bb6c49d2fc --- /dev/null +++ b/tests/unit/Migration/MigrationTest.php @@ -0,0 +1,51 @@ +method->invokeArgs($this->migration, [ + $this->method->invokeArgs($this->migration, [$document]) + ]); + } + + /** + * Check versions array integrity. + */ + public function testMigrationVersions(): void + { + require_once __DIR__ . '/../../../app/init.php'; + + foreach (Migration::$versions as $class) { + $this->assertTrue(class_exists('Appwrite\\Migration\\Version\\' . $class)); + } + + // Test if current version exists + // Only test official releases - skip if latest is release candidate + if (!(\str_contains(APP_VERSION_STABLE, 'RC'))) { + $this->assertArrayHasKey(APP_VERSION_STABLE, Migration::$versions); + } + } +} From b7e4f78c569cc26cad47a70ee2a23b9405ac509b Mon Sep 17 00:00:00 2001 From: eldadfux Date: Thu, 26 Feb 2026 00:00:10 +0100 Subject: [PATCH 2/5] fix --- tests/unit/Migration/MigrationTest.php | 51 -------------------------- 1 file changed, 51 deletions(-) delete mode 100644 tests/unit/Migration/MigrationTest.php diff --git a/tests/unit/Migration/MigrationTest.php b/tests/unit/Migration/MigrationTest.php deleted file mode 100644 index bb6c49d2fc..0000000000 --- a/tests/unit/Migration/MigrationTest.php +++ /dev/null @@ -1,51 +0,0 @@ -method->invokeArgs($this->migration, [ - $this->method->invokeArgs($this->migration, [$document]) - ]); - } - - /** - * Check versions array integrity. - */ - public function testMigrationVersions(): void - { - require_once __DIR__ . '/../../../app/init.php'; - - foreach (Migration::$versions as $class) { - $this->assertTrue(class_exists('Appwrite\\Migration\\Version\\' . $class)); - } - - // Test if current version exists - // Only test official releases - skip if latest is release candidate - if (!(\str_contains(APP_VERSION_STABLE, 'RC'))) { - $this->assertArrayHasKey(APP_VERSION_STABLE, Migration::$versions); - } - } -} From 9b2143a2a57514663b4c911af127d699945f5a57 Mon Sep 17 00:00:00 2001 From: eldadfux Date: Thu, 26 Feb 2026 07:44:35 +0100 Subject: [PATCH 3/5] Fixed cache duplication --- app/controllers/shared/api.php | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index c018803c82..c85d9aea73 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -27,6 +27,7 @@ use Utopia\Config\Config; use Utopia\Database\Database; use Utopia\Database\DateTime; use Utopia\Database\Document; +use Utopia\Database\Exception\Duplicate as DuplicateException; use Utopia\Database\Helpers\Role; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Authorization\Input; @@ -929,14 +930,18 @@ Http::shutdown() $accessedAt = $cacheLog->getAttribute('accessedAt', 0); $now = DateTime::now(); if ($cacheLog->isEmpty()) { - $authorization->skip(fn () => $dbForProject->createDocument('cache', new Document([ - '$id' => $key, - 'resource' => $resource, - 'resourceType' => $resourceType, - 'mimeType' => $response->getContentType(), - 'accessedAt' => $now, - 'signature' => $signature, - ]))); + try { + $authorization->skip(fn () => $dbForProject->createDocument('cache', new Document([ + '$id' => $key, + 'resource' => $resource, + 'resourceType' => $resourceType, + 'mimeType' => $response->getContentType(), + 'accessedAt' => $now, + 'signature' => $signature, + ]))); + } catch (DuplicateException) { + // Race condition: another concurrent request already created the cache document + } } elseif (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_CACHE_UPDATE)) > $accessedAt) { $cacheLog->setAttribute('accessedAt', $now); $authorization->skip(fn () => $dbForProject->updateDocument('cache', $cacheLog->getId(), $cacheLog)); From 79d219bf509913d48afc21a82a8241d5a0bcef29 Mon Sep 17 00:00:00 2001 From: eldadfux Date: Thu, 26 Feb 2026 07:50:51 +0100 Subject: [PATCH 4/5] fix cache duplication --- app/controllers/shared/api.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index c85d9aea73..c6499ff9b6 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -941,6 +941,7 @@ Http::shutdown() ]))); } catch (DuplicateException) { // Race condition: another concurrent request already created the cache document + $cacheLog = $authorization->skip(fn () => $dbForProject->getDocument('cache', $key)); } } elseif (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_CACHE_UPDATE)) > $accessedAt) { $cacheLog->setAttribute('accessedAt', $now); From d7c8b9d43a320c7bbc55acec47e24582864e5c78 Mon Sep 17 00:00:00 2001 From: eldadfux Date: Thu, 26 Feb 2026 10:24:46 +0100 Subject: [PATCH 5/5] Better error message when a function fail instead of general_unknown --- src/Appwrite/Platform/Workers/Functions.php | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Appwrite/Platform/Workers/Functions.php b/src/Appwrite/Platform/Workers/Functions.php index 9f1f328fd6..0932aea335 100644 --- a/src/Appwrite/Platform/Workers/Functions.php +++ b/src/Appwrite/Platform/Workers/Functions.php @@ -11,7 +11,6 @@ use Appwrite\Event\StatsUsage; use Appwrite\Event\Webhook; use Appwrite\Extend\Exception as AppwriteException; use Appwrite\Utopia\Response\Model\Execution; -use Exception; use Executor\Executor; use Utopia\Config\Config; use Utopia\Console; @@ -73,7 +72,10 @@ class Functions extends Action $payload = $message->getPayload() ?? []; if (empty($payload)) { - throw new Exception('Missing payload'); + throw new AppwriteException( + AppwriteException::GENERAL_ARGUMENT_INVALID, + 'Functions worker: missing payload in schedule execution' + ); } $type = $payload['type'] ?? ''; @@ -392,7 +394,10 @@ class Functions extends Action $runtimes = Config::getParam($version === 'v2' ? 'runtimes-v2' : 'runtimes', []); if (!\array_key_exists($function->getAttribute('runtime'), $runtimes)) { - throw new Exception('Runtime "' . $function->getAttribute('runtime', '') . '" is not supported'); + throw new AppwriteException( + AppwriteException::FUNCTION_RUNTIME_UNSUPPORTED, + \sprintf('Runtime "%s" is not supported', $function->getAttribute('runtime', '')), + ); } $runtime = $runtimes[$function->getAttribute('runtime')]; @@ -640,7 +645,7 @@ class Functions extends Action if (!empty($error)) { throw new AppwriteException( AppwriteException::GENERAL_SERVER_ERROR, - $error ?: 'Function execution failed with no error message', + 'Function execution failed: ' . ($error ?: 'No error message provided'), $errorCode ); }