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/app/controllers/shared/api.php b/app/controllers/shared/api.php index c018803c82..c6499ff9b6 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,19 @@ 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 + $cacheLog = $authorization->skip(fn () => $dbForProject->getDocument('cache', $key)); + } } elseif (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_CACHE_UPDATE)) > $accessedAt) { $cacheLog->setAttribute('accessedAt', $now); $authorization->skip(fn () => $dbForProject->updateDocument('cache', $cacheLog->getId(), $cacheLog)); 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/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 ); }