From e4b9b92cbf51e95ab74dc0f958a84d5e79622b76 Mon Sep 17 00:00:00 2001 From: Khushboo Verma <43381712+vermakhushboo@users.noreply.github.com> Date: Thu, 27 Mar 2025 13:27:51 +0530 Subject: [PATCH 01/21] Use Fira Code for powered by --- app/views/general/404.phtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/general/404.phtml b/app/views/general/404.phtml index 7ec1cfbf21..24ce8c67b3 100644 --- a/app/views/general/404.phtml +++ b/app/views/general/404.phtml @@ -88,7 +88,7 @@ } .brand p { - font-family: var(--font-family-monospace, "Aeonik Fono"); + font-family: var(--font-family-monospace, "Fira Code"); font-size: var(--font-size-XS, 12px); font-style: normal; font-weight: 400; From c684d0a2cd076a7bed8602aff94e125a5cd671db Mon Sep 17 00:00:00 2001 From: Khushboo Verma <43381712+vermakhushboo@users.noreply.github.com> Date: Wed, 2 Apr 2025 23:59:43 +0530 Subject: [PATCH 02/21] Enhance error pages --- app/controllers/general.php | 2 +- app/views/general/error.phtml | 402 ++++++++++++++++++++++++++-------- 2 files changed, 316 insertions(+), 88 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index 5ce0a03471..f4d973dc19 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -83,7 +83,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw } if (\str_ends_with($host, System::getEnv('_APP_DOMAIN_FUNCTIONS', '')) || \str_ends_with($host, System::getEnv('_APP_DOMAIN_SITES', ''))) { - throw new AppwriteException(AppwriteException::GENERAL_ACCESS_FORBIDDEN, 'This domain is not connected to any Appwrite resource yet. Please configure custom domain or function domain to allow this request.'); + throw new AppwriteException(AppwriteException::RULE_NOT_FOUND, 'This domain is not connected to any Appwrite resources. Visit domains tab under function/site settings to configure it.'); } if (System::getEnv('_APP_OPTIONS_ROUTER_PROTECTION', 'disabled') === 'enabled') { diff --git a/app/views/general/error.phtml b/app/views/general/error.phtml index a3715e0156..f36f1957e7 100644 --- a/app/views/general/error.phtml +++ b/app/views/general/error.phtml @@ -7,16 +7,67 @@ $message = $this->getParam('message', ''); $trace = $this->getParam('trace', []); $projectName = $this->getParam('projectName', ''); $projectURL = $this->getParam('projectURL', ''); -$title = $this->getParam('title', '') +$title = $this->getParam('title', 'Error'); + +$knownTypes = ['build_not_ready', 'build_failed', 'rule_not_found']; +$label = ''; +$labelClass = ''; +$buttons = []; + +switch ($type) { + case 'build_not_ready': + $label = 'Deployment is still building'; + $message = 'The page will update after the build completes.'; + $labelClass = 'warning'; + $buttons = [ + [ + 'text' => 'Reload', + 'url' => '/', + 'class' => 'bordered-button' + ], + [ + 'text' => 'View logs', + 'url' => $projectURL, + 'class' => 'button' + ], + ]; + break; + case 'build_failed': + $label = 'Deployment build failed'; + $message = 'An error occurred during the build process.'; + $labelClass = 'error'; + $buttons = [ + [ + 'text' => 'View logs', + 'url' => '/', + 'class' => 'bordered-button' + ], + ]; + break; + case 'rule_not_found': + $label = 'Nothing is here yet'; + $message = 'This page is empty, but you can make it yours.'; + $buttons = [ + [ + 'text' => 'Start with this domain', + 'url' => '/', + 'class' => 'bordered-button' + ], + ]; + break; + default: + $label = 'Error ' . $code; + $message = $message; + break; +} ?> - - - + + getParam('title', '') as="font" type="font/woff2" crossorigin /> - - - - - - - - - - <?php echo $this->print($title); ?> -
-
-
-

Error print($code); ?>

-

print($message); ?>

-
-

Type

-

print($type); ?>

-
- -

Error Trace

- -
-
- - - $value) : ?> - - - - - - - -
print($key, self::FILTER_ESCAPE); ?> - - -
print(var_export($value, true), self::FILTER_ESCAPE); ?>
- -
print($value, self::FILTER_ESCAPE); ?>
- -
-
-
-
+
+
+
print($labelClass); ?>>print($label); ?>
+

print($message); ?>

+
+ + + + + + + print($type); ?> +
-
+
+ + +
+

Powered by

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
- \ No newline at end of file From 5841d5ee57a0d703a5bf7260c9aefbbf0f3fe203 Mon Sep 17 00:00:00 2001 From: Khushboo Verma <43381712+vermakhushboo@users.noreply.github.com> Date: Thu, 3 Apr 2025 12:35:05 +0530 Subject: [PATCH 03/21] Add tests for error pages --- .../Services/Sites/SitesCustomServerTest.php | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/tests/e2e/Services/Sites/SitesCustomServerTest.php b/tests/e2e/Services/Sites/SitesCustomServerTest.php index 35bc9bb410..c5b93f4fb4 100644 --- a/tests/e2e/Services/Sites/SitesCustomServerTest.php +++ b/tests/e2e/Services/Sites/SitesCustomServerTest.php @@ -2503,4 +2503,74 @@ class SitesCustomServerTest extends Scope $this->cleanupSite($siteId); } + + public function testErrorPages(): void + { + // non-existent domain page + $domain = 'non-existent-page.sites.localhost'; + $proxyClient = new Client(); + $proxyClient->setEndpoint('http://' . $domain); + + $response = $proxyClient->call(Client::METHOD_GET, '/'); + + $this->assertEquals(404, $response['headers']['status-code']); + $this->assertStringContainsString("This page is empty, but you can make it yours.", $response['body']); + + $siteId = $this->setupSite([ + 'siteId' => ID::unique(), + 'name' => 'Astro SSR site', + 'framework' => 'astro', + 'buildRuntime' => 'node-22', + 'outputDirectory' => './dist', + 'buildCommand' => 'cd random', + 'installCommand' => 'npm install', + ]); + $this->assertNotEmpty($siteId); + + $domain = $this->setupSiteDomain($siteId); + + $deployment = $this->createDeployment($siteId, [ + 'code' => $this->packageSite('astro'), + 'activate' => 'true' + ]); + + $deploymentId = $deployment['body']['$id'] ?? ''; + $this->assertNotEmpty($deploymentId); + + $delpoymentDomain = $this->getDeploymentDomain($deploymentId); + $this->assertNotEmpty($delpoymentDomain); + + $proxyClient = new Client(); + $proxyClient->setEndpoint('http://' . $delpoymentDomain); + $response = $proxyClient->call(Client::METHOD_GET, '/', followRedirects: false); + $this->assertEquals(301, $response['headers']['status-code']); + + $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 900, 0); + $apiKey = $jwtObj->encode([ + 'projectCheckDisabled' => true, + 'previewAuthDisabled' => true, + ]); + + // deployment is still building error page + $response = $proxyClient->call(Client::METHOD_GET, '/', followRedirects: false, headers: [ + 'x-appwrite-key' => API_KEY_DYNAMIC . '_' . $apiKey, + ]); + $this->assertEquals(400, $response['headers']['status-code']); + $this->assertStringContainsString("Deployment is still building", $response['body']); + + $this->assertEventually(function () use ($siteId, $deploymentId) { + $deployment = $this->getDeployment($siteId, $deploymentId); + + $this->assertEquals('failed', $deployment['body']['status']); + }, 50000, 500); + + // deployment failed error page + $response = $proxyClient->call(Client::METHOD_GET, '/', followRedirects: false, headers: [ + 'x-appwrite-key' => API_KEY_DYNAMIC . '_' . $apiKey, + ]); + $this->assertEquals(400, $response['headers']['status-code']); + $this->assertStringContainsString("Deployment build failed", $response['body']); + + $this->cleanupSite($siteId); + } } From 8139ec0d6fdecc2d8d833d5637dbcd81a328edf4 Mon Sep 17 00:00:00 2001 From: Khushboo Verma <43381712+vermakhushboo@users.noreply.github.com> Date: Thu, 3 Apr 2025 18:49:41 +0530 Subject: [PATCH 04/21] Add more error pages --- app/config/errors.php | 5 +++++ app/controllers/general.php | 13 ++++++++++++- app/views/general/error.phtml | 26 ++++++++++++++++++++++++-- src/Appwrite/Extend/Exception.php | 1 + 4 files changed, 42 insertions(+), 3 deletions(-) diff --git a/app/config/errors.php b/app/config/errors.php index f9c2f6b5ba..8847ff7c42 100644 --- a/app/config/errors.php +++ b/app/config/errors.php @@ -580,6 +580,11 @@ return [ 'description' => 'Build with the requested ID is already completed and cannot be canceled.', 'code' => 400, ], + Exception::BUILD_CANCELED => [ + 'name' => Exception::BUILD_CANCELED, + 'description' => 'Build with the requested ID has been canceled.', + 'code' => 400, + ], Exception::BUILD_FAILED => [ 'name' => Exception::BUILD_FAILED, 'description' => 'Build with the requested ID failed. Please check the logs for more information.', diff --git a/app/controllers/general.php b/app/controllers/general.php index f671c97183..62a83cf3a6 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -139,6 +139,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw /** @var Database $dbForProject */ $dbForProject = $getProjectDB($project); + /** @var Document $deployment */ $deployment = Authorization::skip(fn () => $dbForProject->getDocument('deployments', $rule->getAttribute('deploymentId'))); if ($deployment->getAttribute('resourceType', '') === 'functions') { @@ -147,6 +148,10 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw $type = 'site'; } + if ($deployment->isEmpty()) { + throw new AppwriteException(AppwriteException::DEPLOYMENT_NOT_FOUND); + } + $resource = $type === 'function' ? Authorization::skip(fn () => $dbForProject->getDocument('functions', $deployment->getAttribute('resourceId', ''))) : Authorization::skip(fn () => $dbForProject->getDocument('sites', $deployment->getAttribute('resourceId', ''))); @@ -239,7 +244,11 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw $requestHeaders = $request->getHeaders(); if ($resource->isEmpty() || !$resource->getAttribute('enabled')) { - throw new AppwriteException(AppwriteException::FUNCTION_NOT_FOUND); + if ($type === 'functions') { + throw new AppwriteException(AppwriteException::FUNCTION_NOT_FOUND); + } else { + throw new AppwriteException(AppwriteException::SITE_NOT_FOUND); + } } if ($isResourceBlocked($project, $type === 'function' ? RESOURCE_TYPE_FUNCTIONS : RESOURCE_TYPE_SITES, $resource->getId())) { @@ -273,6 +282,8 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw if (!$allowAnyStatus && $deployment->getAttribute('status') !== 'ready') { if ($deployment->getAttribute('status') === 'failed') { throw new AppwriteException(AppwriteException::BUILD_FAILED); + } elseif ($deployment->getAttribute('status') === 'canceled') { + throw new AppwriteException(AppwriteException::BUILD_CANCELED); } else { throw new AppwriteException(AppwriteException::BUILD_NOT_READY); } diff --git a/app/views/general/error.phtml b/app/views/general/error.phtml index f36f1957e7..b80d0f61cd 100644 --- a/app/views/general/error.phtml +++ b/app/views/general/error.phtml @@ -9,7 +9,7 @@ $projectName = $this->getParam('projectName', ''); $projectURL = $this->getParam('projectURL', ''); $title = $this->getParam('title', 'Error'); -$knownTypes = ['build_not_ready', 'build_failed', 'rule_not_found']; +$knownTypes = ['build_not_ready', 'build_failed', 'rule_not_found', 'deployment_not_found', 'build_canceled']; $label = ''; $labelClass = ''; $buttons = []; @@ -55,6 +55,28 @@ switch ($type) { ], ]; break; + case 'deployment_not_found': + $label = 'No deployments available'; + $message = 'This page is empty, deploy your site to make it live.'; + $buttons = [ + [ + 'text' => 'View deployments', + 'url' => '/', + 'class' => 'bordered-button' + ], + ]; + break; + case 'build_canceled': + $label = 'Deployment build cancelled'; + $message = 'The build process was cancelled.'; + $buttons = [ + [ + 'text' => 'View deployments', + 'url' => '/', + 'class' => 'bordered-button' + ], + ]; + break; default: $label = 'Error ' . $code; $message = $message; @@ -316,7 +338,7 @@ switch ($type) { - + print($type); ?> diff --git a/src/Appwrite/Extend/Exception.php b/src/Appwrite/Extend/Exception.php index 338da29403..f137725eaf 100644 --- a/src/Appwrite/Extend/Exception.php +++ b/src/Appwrite/Extend/Exception.php @@ -174,6 +174,7 @@ class Exception extends \Exception public const BUILD_NOT_READY = 'build_not_ready'; public const BUILD_IN_PROGRESS = 'build_in_progress'; public const BUILD_ALREADY_COMPLETED = 'build_already_completed'; + public const BUILD_CANCELED = 'build_canceled'; public const BUILD_FAILED = 'build_failed'; /** Execution */ From e718f4252f6b9b5b36d84a8c720295503bb917f9 Mon Sep 17 00:00:00 2001 From: Khushboo Verma <43381712+vermakhushboo@users.noreply.github.com> Date: Mon, 7 Apr 2025 12:56:51 +0530 Subject: [PATCH 05/21] Add test for canceled deployment error --- app/views/general/error.phtml | 4 +- .../Services/Sites/SitesCustomServerTest.php | 37 ++++++++++++++++++- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/app/views/general/error.phtml b/app/views/general/error.phtml index b80d0f61cd..cb992ce67d 100644 --- a/app/views/general/error.phtml +++ b/app/views/general/error.phtml @@ -67,8 +67,8 @@ switch ($type) { ]; break; case 'build_canceled': - $label = 'Deployment build cancelled'; - $message = 'The build process was cancelled.'; + $label = 'Deployment build canceled'; + $message = 'The build process was canceled.'; $buttons = [ [ 'text' => 'View deployments', diff --git a/tests/e2e/Services/Sites/SitesCustomServerTest.php b/tests/e2e/Services/Sites/SitesCustomServerTest.php index c5b93f4fb4..8006794773 100644 --- a/tests/e2e/Services/Sites/SitesCustomServerTest.php +++ b/tests/e2e/Services/Sites/SitesCustomServerTest.php @@ -2529,17 +2529,31 @@ class SitesCustomServerTest extends Scope $domain = $this->setupSiteDomain($siteId); + // test canceled deployment error page $deployment = $this->createDeployment($siteId, [ 'code' => $this->packageSite('astro'), 'activate' => 'true' ]); $deploymentId = $deployment['body']['$id'] ?? ''; - $this->assertNotEmpty($deploymentId); + $this->assertEquals(202, $deployment['headers']['status-code']); + $this->assertNotEmpty($deployment['body']['$id']); + $this->assertEquals(true, (new DatetimeValidator())->isValid($deployment['body']['$createdAt'])); $delpoymentDomain = $this->getDeploymentDomain($deploymentId); $this->assertNotEmpty($delpoymentDomain); + $this->assertEventually(function () use ($siteId, $deploymentId) { + $deployment = $this->getDeployment($siteId, $deploymentId); + + $this->assertEquals(200, $deployment['headers']['status-code']); + $this->assertEquals('building', $deployment['body']['status']); + }, 100000, 250); + + $deployment = $this->cancelDeployment($siteId, $deploymentId); + $this->assertEquals(200, $deployment['headers']['status-code']); + $this->assertEquals('canceled', $deployment['body']['status']); + $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $delpoymentDomain); $response = $proxyClient->call(Client::METHOD_GET, '/', followRedirects: false); @@ -2551,6 +2565,27 @@ class SitesCustomServerTest extends Scope 'previewAuthDisabled' => true, ]); + $response = $proxyClient->call(Client::METHOD_GET, '/', followRedirects: false, headers: [ + 'x-appwrite-key' => API_KEY_DYNAMIC . '_' . $apiKey, + ]); + $this->assertEquals(400, $response['headers']['status-code']); + $this->assertStringContainsString("Deployment build canceled", $response['body']); + + $deployment = $this->createDeployment($siteId, [ + 'code' => $this->packageSite('astro'), + 'activate' => 'true' + ]); + + $deploymentId = $deployment['body']['$id'] ?? ''; + $this->assertNotEmpty($deploymentId); + + $delpoymentDomain = $this->getDeploymentDomain($deploymentId); + $this->assertNotEmpty($delpoymentDomain); + + $proxyClient->setEndpoint('http://' . $delpoymentDomain); + $response = $proxyClient->call(Client::METHOD_GET, '/', followRedirects: false); + $this->assertEquals(301, $response['headers']['status-code']); + // deployment is still building error page $response = $proxyClient->call(Client::METHOD_GET, '/', followRedirects: false, headers: [ 'x-appwrite-key' => API_KEY_DYNAMIC . '_' . $apiKey, From ea1fc208e16a0352c2a57e75f780f7a8d433759c Mon Sep 17 00:00:00 2001 From: Khushboo Verma <43381712+vermakhushboo@users.noreply.github.com> Date: Mon, 7 Apr 2025 14:38:06 +0530 Subject: [PATCH 06/21] Add tests for no active deployments --- app/views/general/error.phtml | 4 ++-- .../Services/Sites/SitesCustomServerTest.php | 24 ++++++++++++------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/app/views/general/error.phtml b/app/views/general/error.phtml index cb992ce67d..164d826486 100644 --- a/app/views/general/error.phtml +++ b/app/views/general/error.phtml @@ -57,7 +57,7 @@ switch ($type) { break; case 'deployment_not_found': $label = 'No deployments available'; - $message = 'This page is empty, deploy your site to make it live.'; + $message = 'This page is empty, activate a deployment to make it live.'; $buttons = [ [ 'text' => 'View deployments', @@ -68,7 +68,7 @@ switch ($type) { break; case 'build_canceled': $label = 'Deployment build canceled'; - $message = 'The build process was canceled.'; + $message = 'This build was canceled and won\'t be deployed.'; $buttons = [ [ 'text' => 'View deployments', diff --git a/tests/e2e/Services/Sites/SitesCustomServerTest.php b/tests/e2e/Services/Sites/SitesCustomServerTest.php index 8006794773..1f0b0b7be6 100644 --- a/tests/e2e/Services/Sites/SitesCustomServerTest.php +++ b/tests/e2e/Services/Sites/SitesCustomServerTest.php @@ -1706,8 +1706,8 @@ class SitesCustomServerTest extends Scope $siteDomain = $this->setupSiteDomain($siteId); $this->assertNotEmpty($siteDomain); - $delpoymentDomain = $this->getDeploymentDomain($deploymentId); - $this->assertNotEmpty($delpoymentDomain); + $deploymentDomain = $this->getDeploymentDomain($deploymentId); + $this->assertNotEmpty($deploymentDomain); $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $siteDomain); @@ -1718,7 +1718,7 @@ class SitesCustomServerTest extends Scope $contentLength = $response['headers']['content-length']; $proxyClient = new Client(); - $proxyClient->setEndpoint('http://' . $delpoymentDomain); + $proxyClient->setEndpoint('http://' . $deploymentDomain); $response = $proxyClient->call(Client::METHOD_GET, '/', followRedirects: false); $this->assertEquals(301, $response['headers']['status-code']); $this->assertStringContainsString('/console/auth/preview', $response['headers']['location']); @@ -2540,8 +2540,8 @@ class SitesCustomServerTest extends Scope $this->assertNotEmpty($deployment['body']['$id']); $this->assertEquals(true, (new DatetimeValidator())->isValid($deployment['body']['$createdAt'])); - $delpoymentDomain = $this->getDeploymentDomain($deploymentId); - $this->assertNotEmpty($delpoymentDomain); + $deploymentDomain = $this->getDeploymentDomain($deploymentId); + $this->assertNotEmpty($deploymentDomain); $this->assertEventually(function () use ($siteId, $deploymentId) { $deployment = $this->getDeployment($siteId, $deploymentId); @@ -2555,7 +2555,7 @@ class SitesCustomServerTest extends Scope $this->assertEquals('canceled', $deployment['body']['status']); $proxyClient = new Client(); - $proxyClient->setEndpoint('http://' . $delpoymentDomain); + $proxyClient->setEndpoint('http://' . $deploymentDomain); $response = $proxyClient->call(Client::METHOD_GET, '/', followRedirects: false); $this->assertEquals(301, $response['headers']['status-code']); @@ -2571,6 +2571,12 @@ class SitesCustomServerTest extends Scope $this->assertEquals(400, $response['headers']['status-code']); $this->assertStringContainsString("Deployment build canceled", $response['body']); + // check site domain for no active deployments + $proxyClient->setEndpoint('http://' . $domain); + $response = $proxyClient->call(Client::METHOD_GET, '/'); + $this->assertEquals(404, $response['headers']['status-code']); + $this->assertStringContainsString("No deployments available", $response['body']); + $deployment = $this->createDeployment($siteId, [ 'code' => $this->packageSite('astro'), 'activate' => 'true' @@ -2579,10 +2585,10 @@ class SitesCustomServerTest extends Scope $deploymentId = $deployment['body']['$id'] ?? ''; $this->assertNotEmpty($deploymentId); - $delpoymentDomain = $this->getDeploymentDomain($deploymentId); - $this->assertNotEmpty($delpoymentDomain); + $deploymentDomain = $this->getDeploymentDomain($deploymentId); + $this->assertNotEmpty($deploymentDomain); - $proxyClient->setEndpoint('http://' . $delpoymentDomain); + $proxyClient->setEndpoint('http://' . $deploymentDomain); $response = $proxyClient->call(Client::METHOD_GET, '/', followRedirects: false); $this->assertEquals(301, $response['headers']['status-code']); From 8db6474d8009375137015ed3cba1bbec7311d12f Mon Sep 17 00:00:00 2001 From: Khushboo Verma <43381712+vermakhushboo@users.noreply.github.com> Date: Tue, 8 Apr 2025 00:28:51 +0530 Subject: [PATCH 07/21] Add tests for functions --- .../Functions/FunctionsCustomServerTest.php | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index caa5bc6d9a..1479795822 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -2177,4 +2177,71 @@ class FunctionsCustomServerTest extends Scope $this->cleanupFunction($functionId); } + + public function testErrorPages(): void + { + // non-existent domain + $domain = 'non-existent-page.functions.localhost'; + + $proxyClient = new Client(); + $proxyClient->setEndpoint('http://' . $domain); + + $response = $proxyClient->call(Client::METHOD_GET, '/'); + + $this->assertEquals(404, $response['headers']['status-code']); + $this->assertStringContainsString("This page is empty, but you can make it yours.", $response['body']); + + // failed deployment + $functionId = $this->setupFunction([ + 'functionId' => ID::unique(), + 'name' => 'Test PHP Cookie executions', + 'runtime' => 'php-8.0', + 'entrypoint' => 'index.php', + 'timeout' => 15, + 'commands' => 'cd random', + 'execute' => ['any'] + ]); + + $domain = $this->setupFunctionDomain($functionId); + + $deployment = $this->createDeployment($functionId, [ + 'entrypoint' => 'index.php', + 'code' => $this->packageFunction('php-cookie'), + 'activate' => true + ]); + + $this->assertEquals(202, $deployment['headers']['status-code']); + + $response = $proxyClient->call(Client::METHOD_GET, '/', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'] + ])); + + $this->assertEquals(404, $response['headers']['status-code']); + $this->assertStringContainsString("This page is empty, but you can make it yours.", $response['body']); + + // canceled deployment + $deployment = $this->createDeployment($functionId, [ + 'entrypoint' => 'index.php', + 'code' => $this->packageFunction('php-cookie'), + 'activate' => true + ]); + + $deploymentId = $deployment['body']['$id'] ?? ''; + $this->assertEquals(202, $deployment['headers']['status-code']); + + $deployment = $this->cancelDeployment($functionId, $deploymentId); + $this->assertEquals(200, $deployment['headers']['status-code']); + $this->assertEquals('canceled', $deployment['body']['status']); + + $response = $proxyClient->call(Client::METHOD_GET, '/', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'] + ])); + + $this->assertEquals(404, $response['headers']['status-code']); + $this->assertStringContainsString("This page is empty, but you can make it yours.", $response['body']); + + $this->cleanupFunction($functionId); + } } From 9f00ad47f3508ae340ff3a7be31bb08689113df5 Mon Sep 17 00:00:00 2001 From: Khushboo Verma <43381712+vermakhushboo@users.noreply.github.com> Date: Tue, 8 Apr 2025 11:07:32 +0530 Subject: [PATCH 08/21] Added function execute tests --- .../Functions/FunctionsCustomServerTest.php | 44 ++++++++++++++++--- 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 1479795822..fa62e40267 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -2189,12 +2189,12 @@ class FunctionsCustomServerTest extends Scope $response = $proxyClient->call(Client::METHOD_GET, '/'); $this->assertEquals(404, $response['headers']['status-code']); - $this->assertStringContainsString("This page is empty, but you can make it yours.", $response['body']); + $this->assertStringContainsString('This page is empty, but you can make it yours.', $response['body']); // failed deployment $functionId = $this->setupFunction([ 'functionId' => ID::unique(), - 'name' => 'Test PHP Cookie executions', + 'name' => 'Test Error Pages', 'runtime' => 'php-8.0', 'entrypoint' => 'index.php', 'timeout' => 15, @@ -2203,10 +2203,11 @@ class FunctionsCustomServerTest extends Scope ]); $domain = $this->setupFunctionDomain($functionId); + $proxyClient->setEndpoint('http://' . $domain); $deployment = $this->createDeployment($functionId, [ 'entrypoint' => 'index.php', - 'code' => $this->packageFunction('php-cookie'), + 'code' => $this->packageFunction('php'), 'activate' => true ]); @@ -2218,12 +2219,12 @@ class FunctionsCustomServerTest extends Scope ])); $this->assertEquals(404, $response['headers']['status-code']); - $this->assertStringContainsString("This page is empty, but you can make it yours.", $response['body']); + $this->assertStringContainsString('This page is empty, activate a deployment to make it live.', $response['body']); // canceled deployment $deployment = $this->createDeployment($functionId, [ 'entrypoint' => 'index.php', - 'code' => $this->packageFunction('php-cookie'), + 'code' => $this->packageFunction('php'), 'activate' => true ]); @@ -2240,7 +2241,38 @@ class FunctionsCustomServerTest extends Scope ])); $this->assertEquals(404, $response['headers']['status-code']); - $this->assertStringContainsString("This page is empty, but you can make it yours.", $response['body']); + $this->assertStringContainsString('This page is empty, activate a deployment to make it live.', $response['body']); + + $this->cleanupFunction($functionId); + + // no execute permission + $functionId = $this->setupFunction([ + 'functionId' => ID::unique(), + 'name' => 'Test Error Pages', + 'runtime' => 'php-8.0', + 'entrypoint' => 'index.php', + 'execute' => [], + 'timeout' => 15, + ]); + + $domain = $this->setupFunctionDomain($functionId); + $proxyClient->setEndpoint('http://' . $domain); + + $deploymentId = $this->setupDeployment($functionId, [ + 'entrypoint' => 'index.php', + 'code' => $this->packageFunction('php'), + 'activate' => true + ]); + + $this->assertNotEmpty($deploymentId); + + $response = $proxyClient->call(Client::METHOD_GET, '/', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'] + ])); + + $this->assertEquals(401, $response['headers']['status-code']); + $this->assertStringContainsString('user_unauthorized', $response['body']); $this->cleanupFunction($functionId); } From 83401c1004d737920ccc4eb9f2a86848672a4243 Mon Sep 17 00:00:00 2001 From: Khushboo Verma <43381712+vermakhushboo@users.noreply.github.com> Date: Wed, 9 Apr 2025 10:13:54 +0530 Subject: [PATCH 09/21] Add metadata to buttons --- app/config/errors.php | 5 ++ app/controllers/general.php | 76 +++++++++++++++++++------------ app/views/general/error.phtml | 51 +++++---------------- src/Appwrite/Extend/Exception.php | 25 +++++++++- 4 files changed, 88 insertions(+), 69 deletions(-) diff --git a/app/config/errors.php b/app/config/errors.php index 8847ff7c42..0aed627dda 100644 --- a/app/config/errors.php +++ b/app/config/errors.php @@ -546,6 +546,11 @@ return [ 'description' => 'Function runtime could not be detected.', 'code' => 400, ], + Exception::FUNCTION_EXECUTE_PERMISSION_DENIED => [ + 'name' => Exception::FUNCTION_EXECUTE_PERMISSION_DENIED, + 'description' => 'To execute function using domain, execute permissions must include "any" or "guests".', + 'code' => 403, + ], /** Sites */ Exception::SITE_NOT_FOUND => [ diff --git a/app/controllers/general.php b/app/controllers/general.php index 62a83cf3a6..189ab2fee7 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -58,8 +58,6 @@ Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE); function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Reader $geodb, callable $isResourceBlocked, string $previewHostname, ?Key $apiKey) { - $utopia->getRoute()?->label('error', __DIR__ . '/../views/general/error.phtml'); - $host = $request->getHostname() ?? ''; if (!empty($previewHostname)) { $host = $previewHostname; @@ -77,24 +75,29 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw )[0] ?? new Document(); } + $protocol = $request->getProtocol(); + $errorView = __DIR__ . '/../views/general/error.phtml'; + $url = $protocol . '://' . System::getEnv('_APP_DOMAIN', ''); + if ($rule->isEmpty()) { if ($host === System::getEnv('_APP_DOMAIN_FUNCTIONS', '') || $host === System::getEnv('_APP_DOMAIN_SITES', '')) { - throw new AppwriteException(AppwriteException::GENERAL_ACCESS_FORBIDDEN, 'This domain cannot be used for security reasons. Please use any subdomain instead.'); + throw new AppwriteException(AppwriteException::GENERAL_ACCESS_FORBIDDEN, 'This domain cannot be used for security reasons. Please use any subdomain instead.', view: $errorView); } if (\str_ends_with($host, System::getEnv('_APP_DOMAIN_FUNCTIONS', '')) || \str_ends_with($host, System::getEnv('_APP_DOMAIN_SITES', ''))) { - throw new AppwriteException(AppwriteException::RULE_NOT_FOUND, 'This domain is not connected to any Appwrite resources. Visit domains tab under function/site settings to configure it.'); + $exception = new AppwriteException(AppwriteException::RULE_NOT_FOUND, 'This domain is not connected to any Appwrite resources. Visit domains tab under function/site settings to configure it.', view: $errorView); + + $exception->addCTA('Start with this domain', $url . '/console'); + throw $exception; } if (System::getEnv('_APP_OPTIONS_ROUTER_PROTECTION', 'disabled') === 'enabled') { if ($host !== 'localhost' && $host !== APP_HOSTNAME_INTERNAL && $host !== System::getEnv('_APP_CONSOLE_DOMAIN', '')) { - throw new AppwriteException(AppwriteException::GENERAL_ACCESS_FORBIDDEN, 'Router protection does not allow accessing Appwrite over this domain. Please add it as custom domain to your project or disable _APP_OPTIONS_ROUTER_PROTECTION environment variable.'); + throw new AppwriteException(AppwriteException::GENERAL_ACCESS_FORBIDDEN, 'Router protection does not allow accessing Appwrite over this domain. Please add it as custom domain to your project or disable _APP_OPTIONS_ROUTER_PROTECTION environment variable.', view: $errorView); } } // Act as API - no Proxy logic - $utopia->getRoute()?->label('error', ''); - return false; } @@ -114,7 +117,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw if (array_key_exists('proxy', $project->getAttribute('services', []))) { $status = $project->getAttribute('services', [])['proxy']; if (!$status) { - throw new AppwriteException(AppwriteException::GENERAL_SERVICE_DISABLED); + throw new AppwriteException(AppwriteException::GENERAL_SERVICE_DISABLED, view: $errorView); } } @@ -130,7 +133,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw if (System::getEnv('_APP_OPTIONS_COMPUTE_FORCE_HTTPS', 'disabled') === 'enabled') { // Force HTTPS if ($request->getProtocol() !== 'https' && $request->getHostname() !== APP_HOSTNAME_INTERNAL) { if ($request->getMethod() !== Request::METHOD_GET) { - throw new AppwriteException(AppwriteException::GENERAL_PROTOCOL_UNSUPPORTED, 'Method unsupported over HTTP. Please use HTTPS instead.'); + throw new AppwriteException(AppwriteException::GENERAL_PROTOCOL_UNSUPPORTED, 'Method unsupported over HTTP. Please use HTTPS instead.', view: $errorView); } return $response->redirect('https://' . $request->getHostname() . $request->getURI()); } @@ -149,7 +152,9 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw } if ($deployment->isEmpty()) { - throw new AppwriteException(AppwriteException::DEPLOYMENT_NOT_FOUND); + $exception = new AppwriteException(AppwriteException::DEPLOYMENT_NOT_FOUND, view: $errorView); + $exception->addCTA('View deployments', $url . '/console'); // TODO: fix this URL + throw $exception; } $resource = $type === 'function' ? @@ -245,14 +250,14 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw if ($resource->isEmpty() || !$resource->getAttribute('enabled')) { if ($type === 'functions') { - throw new AppwriteException(AppwriteException::FUNCTION_NOT_FOUND); + throw new AppwriteException(AppwriteException::FUNCTION_NOT_FOUND, view: $errorView); } else { - throw new AppwriteException(AppwriteException::SITE_NOT_FOUND); + throw new AppwriteException(AppwriteException::SITE_NOT_FOUND, view: $errorView); } } if ($isResourceBlocked($project, $type === 'function' ? RESOURCE_TYPE_FUNCTIONS : RESOURCE_TYPE_SITES, $resource->getId())) { - throw new AppwriteException(AppwriteException::GENERAL_RESOURCE_BLOCKED); + throw new AppwriteException(AppwriteException::GENERAL_RESOURCE_BLOCKED, view: $errorView); } $version = match ($type) { @@ -275,24 +280,40 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw } if (\is_null($runtime)) { - throw new AppwriteException(AppwriteException::FUNCTION_RUNTIME_UNSUPPORTED, 'Runtime "' . $resource->getAttribute('runtime', '') . '" is not supported'); + throw new AppwriteException(AppwriteException::FUNCTION_RUNTIME_UNSUPPORTED, 'Runtime "' . $resource->getAttribute('runtime', '') . '" is not supported', view: $errorView); } $allowAnyStatus = !\is_null($apiKey) && $apiKey->isDeploymentStatusIgnored(); if (!$allowAnyStatus && $deployment->getAttribute('status') !== 'ready') { - if ($deployment->getAttribute('status') === 'failed') { - throw new AppwriteException(AppwriteException::BUILD_FAILED); - } elseif ($deployment->getAttribute('status') === 'canceled') { - throw new AppwriteException(AppwriteException::BUILD_CANCELED); - } else { - throw new AppwriteException(AppwriteException::BUILD_NOT_READY); + $errorView = __DIR__ . '/../views/general/error.phtml'; + $status = $deployment->getAttribute('status'); + $ctaUrl = ''; + + switch ($status) { + case 'failed': + $exception = new AppwriteException(AppwriteException::BUILD_FAILED, view: $errorView); + $ctaUrl = '/console/project-' . $project->getId() . '/sites/site-' . $resource->getId() . '/deployments/deployment-' . $deployment->getId(); + $exception->addCTA('View logs', $url . $ctaUrl); + break; + case 'canceled': + $exception = new AppwriteException(AppwriteException::BUILD_CANCELED, view: $errorView); + $ctaUrl = '/console/project-' . $project->getId() . '/sites/site-' . $resource->getId() . '/deployments'; + $exception->addCTA('View deployments', $url . $ctaUrl); + break; + default: + $exception = new AppwriteException(AppwriteException::BUILD_NOT_READY, view: $errorView); + $ctaUrl = '/console/project-' . $project->getId() . '/sites/site-' . $resource->getId() . '/deployments/deployment-' . $deployment->getId(); + $exception->addCTA('Reload', '/'); + $exception->addCTA('View logs', $url . $ctaUrl); + break; } + throw $exception; } if ($type === 'function') { $permissions = $resource->getAttribute('execute'); if (!(\in_array('any', $permissions)) && !(\in_array('guests', $permissions))) { - throw new AppwriteException(AppwriteException::USER_UNAUTHORIZED, 'To execute function using domain, execute permissions must include "any" or "guests"'); + throw new AppwriteException(AppwriteException::FUNCTION_EXECUTE_PERMISSION_DENIED, view: $errorView); } } @@ -612,7 +633,6 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw return true; } elseif ($type === 'api') { - $utopia->getRoute()?->label('error', ''); return false; } elseif ($type === 'redirect') { $url = $rule->getAttribute('redirectUrl', ''); @@ -625,10 +645,9 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw $response->redirect($url, \intval($rule->getAttribute('redirectStatusCode', 301))); return true; } else { - throw new AppwriteException(AppwriteException::GENERAL_SERVER_ERROR, 'Unknown resource type ' . $type); + throw new AppwriteException(AppwriteException::GENERAL_SERVER_ERROR, 'Unknown resource type ' . $type, view: $errorView); } - $utopia->getRoute()?->label('error', ''); return false; } @@ -1220,10 +1239,10 @@ App::error() ->addHeader('Pragma', 'no-cache') ->setStatusCode($code); - $template = ($route) ? $route->getLabel('error', null) : null; + $view = $error->getView(); - if ($template) { - $layout = new View($template); + if ($view) { + $layout = new View($view); $layout ->setParam('title', $project->getAttribute('name') . ' - Error') @@ -1233,7 +1252,8 @@ App::error() ->setParam('message', $output['message'] ?? '') ->setParam('type', $output['type'] ?? '') ->setParam('code', $output['code'] ?? '') - ->setParam('trace', $output['trace'] ?? []); + ->setParam('trace', $output['trace'] ?? []) + ->setParam('exception', $error); $response->html($layout->render()); return; diff --git a/app/views/general/error.phtml b/app/views/general/error.phtml index 164d826486..7156a31951 100644 --- a/app/views/general/error.phtml +++ b/app/views/general/error.phtml @@ -8,74 +8,45 @@ $trace = $this->getParam('trace', []); $projectName = $this->getParam('projectName', ''); $projectURL = $this->getParam('projectURL', ''); $title = $this->getParam('title', 'Error'); +$exception = $this->getParam('exception', null); $knownTypes = ['build_not_ready', 'build_failed', 'rule_not_found', 'deployment_not_found', 'build_canceled']; $label = ''; $labelClass = ''; $buttons = []; +foreach ($exception->getCTAs() as $index => $cta) { + $class = ($index === 0) ? 'bordered-button' : 'button'; + + $buttons[] = [ + 'text' => $cta['label'], + 'url' => $cta['url'], + 'class' => $class + ]; +} + switch ($type) { case 'build_not_ready': $label = 'Deployment is still building'; $message = 'The page will update after the build completes.'; $labelClass = 'warning'; - $buttons = [ - [ - 'text' => 'Reload', - 'url' => '/', - 'class' => 'bordered-button' - ], - [ - 'text' => 'View logs', - 'url' => $projectURL, - 'class' => 'button' - ], - ]; break; case 'build_failed': $label = 'Deployment build failed'; $message = 'An error occurred during the build process.'; $labelClass = 'error'; - $buttons = [ - [ - 'text' => 'View logs', - 'url' => '/', - 'class' => 'bordered-button' - ], - ]; break; case 'rule_not_found': $label = 'Nothing is here yet'; $message = 'This page is empty, but you can make it yours.'; - $buttons = [ - [ - 'text' => 'Start with this domain', - 'url' => '/', - 'class' => 'bordered-button' - ], - ]; break; case 'deployment_not_found': $label = 'No deployments available'; $message = 'This page is empty, activate a deployment to make it live.'; - $buttons = [ - [ - 'text' => 'View deployments', - 'url' => '/', - 'class' => 'bordered-button' - ], - ]; break; case 'build_canceled': $label = 'Deployment build canceled'; $message = 'This build was canceled and won\'t be deployed.'; - $buttons = [ - [ - 'text' => 'View deployments', - 'url' => '/', - 'class' => 'bordered-button' - ], - ]; break; default: $label = 'Error ' . $code; diff --git a/src/Appwrite/Extend/Exception.php b/src/Appwrite/Extend/Exception.php index f137725eaf..2d83667d44 100644 --- a/src/Appwrite/Extend/Exception.php +++ b/src/Appwrite/Extend/Exception.php @@ -165,6 +165,7 @@ class Exception extends \Exception public const FUNCTION_SYNCHRONOUS_TIMEOUT = 'function_synchronous_timeout'; public const FUNCTION_TEMPLATE_NOT_FOUND = 'function_template_not_found'; public const FUNCTION_RUNTIME_NOT_DETECTED = 'function_runtime_not_detected'; + public const FUNCTION_EXECUTE_PERMISSION_DENIED = 'function_execute_permission_denied'; /** Deployments */ public const DEPLOYMENT_NOT_FOUND = 'deployment_not_found'; @@ -321,11 +322,14 @@ class Exception extends \Exception protected string $type = ''; protected array $errors = []; protected bool $publish; + private array $ctas = []; + private string $view = ''; - public function __construct(string $type = Exception::GENERAL_UNKNOWN, string $message = null, int|string $code = null, \Throwable $previous = null) + public function __construct(string $type = Exception::GENERAL_UNKNOWN, string $message = null, int|string $code = null, \Throwable $previous = null, string $view = '') { $this->errors = Config::getParam('errors'); $this->type = $type; + $this->view = $view; $this->code = $code ?? $this->errors[$type]['code']; // Mark string errors like HY001 from PDO as 500 errors @@ -375,4 +379,23 @@ class Exception extends \Exception { return $this->publish; } + + public function addCTA(string $label, string $url): self + { + $this->ctas[] = [ + 'label' => $label, + 'url' => $url + ]; + return $this; + } + + public function getCTAs(): array + { + return $this->ctas; + } + + public function getView(): string + { + return $this->view; + } } From 0b4b7f52968b0db9a2438f135a1f137a810d0fae Mon Sep 17 00:00:00 2001 From: Khushboo Verma <43381712+vermakhushboo@users.noreply.github.com> Date: Wed, 9 Apr 2025 10:19:38 +0530 Subject: [PATCH 10/21] Update error message --- tests/e2e/Services/Functions/FunctionsCustomServerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index fa62e40267..e44d66435a 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -2272,7 +2272,7 @@ class FunctionsCustomServerTest extends Scope ])); $this->assertEquals(401, $response['headers']['status-code']); - $this->assertStringContainsString('user_unauthorized', $response['body']); + $this->assertStringContainsString('function_execute_permission_denied', $response['body']); $this->cleanupFunction($functionId); } From 921311e91eb177bca71f4b4c921815fe238449c2 Mon Sep 17 00:00:00 2001 From: Khushboo Verma <43381712+vermakhushboo@users.noreply.github.com> Date: Wed, 9 Apr 2025 11:58:54 +0530 Subject: [PATCH 11/21] Fix tests --- app/config/errors.php | 2 +- tests/e2e/Services/Sites/SitesCustomServerTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/config/errors.php b/app/config/errors.php index 0aed627dda..e6c9830a30 100644 --- a/app/config/errors.php +++ b/app/config/errors.php @@ -549,7 +549,7 @@ return [ Exception::FUNCTION_EXECUTE_PERMISSION_DENIED => [ 'name' => Exception::FUNCTION_EXECUTE_PERMISSION_DENIED, 'description' => 'To execute function using domain, execute permissions must include "any" or "guests".', - 'code' => 403, + 'code' => 401, ], /** Sites */ diff --git a/tests/e2e/Services/Sites/SitesCustomServerTest.php b/tests/e2e/Services/Sites/SitesCustomServerTest.php index 1f0b0b7be6..a67af33392 100644 --- a/tests/e2e/Services/Sites/SitesCustomServerTest.php +++ b/tests/e2e/Services/Sites/SitesCustomServerTest.php @@ -1654,7 +1654,7 @@ class SitesCustomServerTest extends Scope $response = $proxyClient->call(Client::METHOD_GET, '/'); - $this->assertEquals(401, $response['headers']['status-code']); + $this->assertEquals(404, $response['headers']['status-code']); $this->assertStringContainsString("This domain is not connected to any Appwrite resource yet", $response['body']); $site = $this->createSite([ From 39476fa3d54d093f259f56524470ceec1a0cc725 Mon Sep 17 00:00:00 2001 From: Khushboo Verma <43381712+vermakhushboo@users.noreply.github.com> Date: Wed, 9 Apr 2025 23:20:06 +0530 Subject: [PATCH 12/21] Fix button URL for no active deployments --- app/controllers/general.php | 8 ++++++-- docker-compose.yml | 2 +- tests/e2e/Services/Sites/SitesCustomServerTest.php | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index 189ab2fee7..d2bbe9792d 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -152,8 +152,11 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw } if ($deployment->isEmpty()) { + $resourceType = $rule->getAttribute('deploymentResourceType', ''); + $resourceId = $rule->getAttribute('deploymentResourceId', ''); + $type = ($resourceType === 'site') ? 'sites' : 'functions'; $exception = new AppwriteException(AppwriteException::DEPLOYMENT_NOT_FOUND, view: $errorView); - $exception->addCTA('View deployments', $url . '/console'); // TODO: fix this URL + $exception->addCTA('View deployments', $url . '/console/project-' . $projectId . '/' . $type . '/' . $resourceType . '-' . $resourceId); throw $exception; } @@ -1459,7 +1462,8 @@ App::wildcard() ->groups(['api']) ->label('scope', 'global') ->action(function () { - throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND); + $errorView = __DIR__ . '/../views/general/error.phtml'; + throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND, view: $errorView); }); foreach (Config::getParam('services', []) as $service) { diff --git a/docker-compose.yml b/docker-compose.yml index 3ba03a6b79..276834ea35 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -205,7 +205,7 @@ services: appwrite-console: <<: *x-logging container_name: appwrite-console - image: appwrite/console:5.3.0-sites-rc.35 + image: appwrite/console:5.3.0-sites-rc.38 restart: unless-stopped networks: - appwrite diff --git a/tests/e2e/Services/Sites/SitesCustomServerTest.php b/tests/e2e/Services/Sites/SitesCustomServerTest.php index a67af33392..a533ed3022 100644 --- a/tests/e2e/Services/Sites/SitesCustomServerTest.php +++ b/tests/e2e/Services/Sites/SitesCustomServerTest.php @@ -1655,7 +1655,7 @@ class SitesCustomServerTest extends Scope $response = $proxyClient->call(Client::METHOD_GET, '/'); $this->assertEquals(404, $response['headers']['status-code']); - $this->assertStringContainsString("This domain is not connected to any Appwrite resource yet", $response['body']); + $this->assertStringContainsString("This page is empty, but you can make it yours.", $response['body']); $site = $this->createSite([ 'siteId' => ID::unique(), From 98a52947096f2573e492ec71eab887c9be436a5c Mon Sep 17 00:00:00 2001 From: Khushboo Verma <43381712+vermakhushboo@users.noreply.github.com> Date: Wed, 9 Apr 2025 23:35:08 +0530 Subject: [PATCH 13/21] Handle route_not_found --- app/controllers/general.php | 6 +++++- app/views/general/error.phtml | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index d2bbe9792d..c78d7ef925 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -1463,7 +1463,11 @@ App::wildcard() ->label('scope', 'global') ->action(function () { $errorView = __DIR__ . '/../views/general/error.phtml'; - throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND, view: $errorView); + $protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https'; + $url = $protocol . '://' . System::getEnv('_APP_DOMAIN', ''); + $exception = new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND, view: $errorView); + $exception->addCTA('Go to homepage', $url); + throw $exception; }); foreach (Config::getParam('services', []) as $service) { diff --git a/app/views/general/error.phtml b/app/views/general/error.phtml index 7156a31951..791ef695d6 100644 --- a/app/views/general/error.phtml +++ b/app/views/general/error.phtml @@ -10,7 +10,7 @@ $projectURL = $this->getParam('projectURL', ''); $title = $this->getParam('title', 'Error'); $exception = $this->getParam('exception', null); -$knownTypes = ['build_not_ready', 'build_failed', 'rule_not_found', 'deployment_not_found', 'build_canceled']; +$knownTypes = ['build_not_ready', 'build_failed', 'rule_not_found', 'deployment_not_found', 'build_canceled', 'general_route_not_found']; $label = ''; $labelClass = ''; $buttons = []; @@ -48,6 +48,10 @@ switch ($type) { $label = 'Deployment build canceled'; $message = 'This build was canceled and won\'t be deployed.'; break; + case 'general_route_not_found': + $label = 'Page not found'; + $message = 'The page you\'re looking for doesn\'t exist.'; + break; default: $label = 'Error ' . $code; $message = $message; From e78a3e1f83b0f0bbb15ae303a3c2c3468ff39517 Mon Sep 17 00:00:00 2001 From: Khushboo Verma <43381712+vermakhushboo@users.noreply.github.com> Date: Thu, 10 Apr 2025 00:11:17 +0530 Subject: [PATCH 14/21] Fix test --- tests/e2e/Services/Sites/SitesCustomServerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/Services/Sites/SitesCustomServerTest.php b/tests/e2e/Services/Sites/SitesCustomServerTest.php index a533ed3022..1ed3d99bf1 100644 --- a/tests/e2e/Services/Sites/SitesCustomServerTest.php +++ b/tests/e2e/Services/Sites/SitesCustomServerTest.php @@ -2434,7 +2434,7 @@ class SitesCustomServerTest extends Scope }, 100000, 500); $response = $proxyClient->call(Client::METHOD_GET, '/'); - $this->assertStringContainsString('build_failed', $response['body']); + $this->assertStringContainsString('Deployment build failed', $response['body']); $this->cleanupSite($siteId); } From 7bb548aecbe9dc4858e8054e217341b3723bab21 Mon Sep 17 00:00:00 2001 From: Khushboo Verma <43381712+vermakhushboo@users.noreply.github.com> Date: Thu, 10 Apr 2025 00:39:36 +0530 Subject: [PATCH 15/21] Fix test --- tests/e2e/Services/Sites/SitesCustomServerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/Services/Sites/SitesCustomServerTest.php b/tests/e2e/Services/Sites/SitesCustomServerTest.php index 1ed3d99bf1..43993c7b13 100644 --- a/tests/e2e/Services/Sites/SitesCustomServerTest.php +++ b/tests/e2e/Services/Sites/SitesCustomServerTest.php @@ -2434,7 +2434,7 @@ class SitesCustomServerTest extends Scope }, 100000, 500); $response = $proxyClient->call(Client::METHOD_GET, '/'); - $this->assertStringContainsString('Deployment build failed', $response['body']); + $this->assertStringContainsString('his page is empty, activate a deployment to make it live.', $response['body']); $this->cleanupSite($siteId); } From d733e8b75fe82a24bfb9eda3b828362c4ac01c3f Mon Sep 17 00:00:00 2001 From: Khushboo Verma <43381712+vermakhushboo@users.noreply.github.com> Date: Thu, 10 Apr 2025 16:39:37 +0530 Subject: [PATCH 16/21] Add trace button --- app/controllers/general.php | 29 +++++++ app/views/general/error.phtml | 143 ++++++++++++++++++++++++++++++++-- 2 files changed, 164 insertions(+), 8 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index c78d7ef925..80097cc471 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -555,6 +555,35 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw $execution->setAttribute('responseStatusCode', $executionResponse['statusCode']); $execution->setAttribute('responseHeaders', $headersFiltered); $execution->setAttribute('duration', $executionResponse['duration']); + if ($executionResponse['statusCode'] >= 500) { //TODO: if body is empty + $errorView = __DIR__ . '/../views/general/error.phtml'; + $layout = new View($errorView); + $layout + ->setParam('title', $project->getAttribute('name') . ' - Error') + ->setParam('development', App::isDevelopment()) + ->setParam('message', empty($executionResponse['body']) ? 'A server error occurred.' : $executionResponse['body']) + ->setParam('type', 'general_server_error') + ->setParam('code', $executionResponse['statusCode']) + ->setParam('trace', []) + ->setParam('exception', null); + + $response->html($layout->render()); + return; + } elseif ($executionResponse['statusCode'] >= 400) { + $errorView = __DIR__ . '/../views/general/error.phtml'; + $layout = new View($errorView); + $layout + ->setParam('title', $project->getAttribute('name') . ' - Error') + ->setParam('development', App::isDevelopment()) + ->setParam('message', empty($executionResponse['body']) ? 'A client error occurred.' : $executionResponse['body']) + ->setParam('type', 'client_error') + ->setParam('code', $executionResponse['statusCode']) + ->setParam('trace', []) + ->setParam('exception', null); + + $response->html($layout->render()); + return; + } } catch (\Throwable $th) { $durationEnd = \microtime(true); diff --git a/app/views/general/error.phtml b/app/views/general/error.phtml index 791ef695d6..75ec39b970 100644 --- a/app/views/general/error.phtml +++ b/app/views/general/error.phtml @@ -15,13 +15,24 @@ $label = ''; $labelClass = ''; $buttons = []; -foreach ($exception->getCTAs() as $index => $cta) { - $class = ($index === 0) ? 'bordered-button' : 'button'; +if($exception !== null && $exception instanceof AppwriteException) { + foreach ($exception->getCTAs() as $index => $cta) { + $class = ($index === 0) ? 'bordered-button' : 'button'; + + $buttons[] = [ + 'text' => $cta['label'], + 'url' => $cta['url'], + 'class' => $class + ]; + } +} +if ($development && !empty($buttons)) { $buttons[] = [ - 'text' => $cta['label'], - 'url' => $cta['url'], - 'class' => $class + 'text' => 'View error trace', + 'url' => '#', + 'class' => 'button', + 'x-on:click' => "page = 'trace'" ]; } @@ -65,6 +76,7 @@ switch ($type) { + - +
-
+
print($labelClass); ?>>print($label); ?>

print($message); ?>

- @@ -317,6 +411,39 @@ switch ($type) { print($type); ?>
+ +
+ +
+ +
+ +
+ +
Error trace
+ $traceItem): ?> +
+ +
file
+
print($traceItem['file']); ?>
+ + + +
line
+
print($traceItem['line']); ?>
+ + + +
function
+
print($traceItem['function']); ?>
+ + + +
args
+
print(json_encode($traceItem['args'], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); ?>
+ +
+
From 60fafe8feb1ae17b7438298e9f3636ca924562c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 15 Apr 2025 11:40:32 +0200 Subject: [PATCH 17/21] PR review changes --- app/config/errors.php | 4 +- app/controllers/general.php | 72 ++++++++----------- app/views/general/error.phtml | 65 +++++++---------- src/Appwrite/Extend/Exception.php | 10 +-- .../Functions/FunctionsCustomServerTest.php | 2 +- 5 files changed, 63 insertions(+), 90 deletions(-) diff --git a/app/config/errors.php b/app/config/errors.php index e6c9830a30..2c1bda995d 100644 --- a/app/config/errors.php +++ b/app/config/errors.php @@ -546,8 +546,8 @@ return [ 'description' => 'Function runtime could not be detected.', 'code' => 400, ], - Exception::FUNCTION_EXECUTE_PERMISSION_DENIED => [ - 'name' => Exception::FUNCTION_EXECUTE_PERMISSION_DENIED, + Exception::FUNCTION_EXECUTE_PERMISSION_MISSING => [ + 'name' => Exception::FUNCTION_EXECUTE_PERMISSION_MISSING, 'description' => 'To execute function using domain, execute permissions must include "any" or "guests".', 'code' => 401, ], diff --git a/app/controllers/general.php b/app/controllers/general.php index 4ea341e393..c4317324fc 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -74,10 +74,9 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw ]) )[0] ?? new Document(); } - - $protocol = $request->getProtocol(); + $errorView = __DIR__ . '/../views/general/error.phtml'; - $url = $protocol . '://' . System::getEnv('_APP_DOMAIN', ''); + $url = (System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https') . '://' . System::getEnv('_APP_DOMAIN', ''); if ($rule->isEmpty()) { if ($host === System::getEnv('_APP_DOMAIN_FUNCTIONS', '') || $host === System::getEnv('_APP_DOMAIN_SITES', '')) { @@ -288,9 +287,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw $allowAnyStatus = !\is_null($apiKey) && $apiKey->isDeploymentStatusIgnored(); if (!$allowAnyStatus && $deployment->getAttribute('status') !== 'ready') { - $errorView = __DIR__ . '/../views/general/error.phtml'; $status = $deployment->getAttribute('status'); - $ctaUrl = ''; switch ($status) { case 'failed': @@ -316,7 +313,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw if ($type === 'function') { $permissions = $resource->getAttribute('execute'); if (!(\in_array('any', $permissions)) && !(\in_array('guests', $permissions))) { - throw new AppwriteException(AppwriteException::FUNCTION_EXECUTE_PERMISSION_DENIED, view: $errorView); + throw new AppwriteException(AppwriteException::FUNCTION_EXECUTE_PERMISSION_MISSING, view: $errorView); } } @@ -538,6 +535,22 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw } } } + + // Branded error pages (when developer left body empty) + if ($executionResponse['statusCode'] >= 400 && empty($executionResponse['body'])) { + $layout = new View($errorView); + $layout + ->setParam('title', $project->getAttribute('name') . ' - Error') + ->setParam('type', 'empty_proxy_error') + ->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']); + } + } + } $headersFiltered = []; foreach ($executionResponse['headers'] as $key => $value) { @@ -554,35 +567,6 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw $execution->setAttribute('responseStatusCode', $executionResponse['statusCode']); $execution->setAttribute('responseHeaders', $headersFiltered); $execution->setAttribute('duration', $executionResponse['duration']); - if ($executionResponse['statusCode'] >= 500) { //TODO: if body is empty - $errorView = __DIR__ . '/../views/general/error.phtml'; - $layout = new View($errorView); - $layout - ->setParam('title', $project->getAttribute('name') . ' - Error') - ->setParam('development', App::isDevelopment()) - ->setParam('message', empty($executionResponse['body']) ? 'A server error occurred.' : $executionResponse['body']) - ->setParam('type', 'general_server_error') - ->setParam('code', $executionResponse['statusCode']) - ->setParam('trace', []) - ->setParam('exception', null); - - $response->html($layout->render()); - return; - } elseif ($executionResponse['statusCode'] >= 400) { - $errorView = __DIR__ . '/../views/general/error.phtml'; - $layout = new View($errorView); - $layout - ->setParam('title', $project->getAttribute('name') . ' - Error') - ->setParam('development', App::isDevelopment()) - ->setParam('message', empty($executionResponse['body']) ? 'A client error occurred.' : $executionResponse['body']) - ->setParam('type', 'client_error') - ->setParam('code', $executionResponse['statusCode']) - ->setParam('trace', []) - ->setParam('exception', null); - - $response->html($layout->render()); - return; - } } catch (\Throwable $th) { $durationEnd = \microtime(true); @@ -1273,10 +1257,15 @@ App::error() ->addHeader('Pragma', 'no-cache') ->setStatusCode($code); - $view = $error->getView(); + $template = $error->getView() ?? (($route) ? $route->getLabel('error', null) : null); + + // TODO: Ideally use group 'api' here, but all wildcard routes seem to have 'api' at the moment + if(!\str_starts_with($route->getPath(), '/v1')) { + $template = __DIR__ . '/../views/general/error.phtml'; + } - if ($view) { - $layout = new View($view); + if (!empty($template)) { + $layout = new View($template); $layout ->setParam('title', $project->getAttribute('name') . ' - Error') @@ -1495,12 +1484,7 @@ App::wildcard() ->groups(['api']) ->label('scope', 'global') ->action(function () { - $errorView = __DIR__ . '/../views/general/error.phtml'; - $protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https'; - $url = $protocol . '://' . System::getEnv('_APP_DOMAIN', ''); - $exception = new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND, view: $errorView); - $exception->addCTA('Go to homepage', $url); - throw $exception; + throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND); }); foreach (Config::getParam('services', []) as $service) { diff --git a/app/views/general/error.phtml b/app/views/general/error.phtml index 75ec39b970..1f071b3299 100644 --- a/app/views/general/error.phtml +++ b/app/views/general/error.phtml @@ -2,20 +2,17 @@ $development = $this->getParam('development', false); $type = $this->getParam('type', 'general_server_error'); $code = $this->getParam('code', 500); -$errorID = $this->getParam('errorID', 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'); $message = $this->getParam('message', ''); $trace = $this->getParam('trace', []); -$projectName = $this->getParam('projectName', ''); -$projectURL = $this->getParam('projectURL', ''); $title = $this->getParam('title', 'Error'); $exception = $this->getParam('exception', null); -$knownTypes = ['build_not_ready', 'build_failed', 'rule_not_found', 'deployment_not_found', 'build_canceled', 'general_route_not_found']; +$isSimpleMessage = true; $label = ''; $labelClass = ''; $buttons = []; -if($exception !== null && $exception instanceof AppwriteException) { +if($exception !== null && method_exists($exception, 'getCTAs')) { foreach ($exception->getCTAs() as $index => $cta) { $class = ($index === 0) ? 'bordered-button' : 'button'; @@ -27,15 +24,6 @@ if($exception !== null && $exception instanceof AppwriteException) { } } -if ($development && !empty($buttons)) { - $buttons[] = [ - 'text' => 'View error trace', - 'url' => '#', - 'class' => 'button', - 'x-on:click' => "page = 'trace'" - ]; -} - switch ($type) { case 'build_not_ready': $label = 'Deployment is still building'; @@ -66,6 +54,7 @@ switch ($type) { default: $label = 'Error ' . $code; $message = $message; + $isSimpleMessage = false; break; } ?> @@ -143,12 +132,19 @@ switch ($type) { margin-top: 8px; margin-bottom: 32px; } - - .content.default-error h1 { - font-size: var(--font-size-M, 16px); + + .content h1 { margin-bottom: 20px; } + .content.small-error h1 { + font-size: var(--font-size-M, 20px); + } + + .content.large-error h1 { + font-size: var(--font-size-XXXL, 32px); + } + .bordered-button { border-radius: var(--border-radius-S, 8px); font-family: var(--font-family-sansSerif, Inter), sans-serif; @@ -263,11 +259,6 @@ switch ($type) { letter-spacing: -0.45px; } - .back-button:before { - content: "<"; - font-size: 16px; - } - .trace-grid { display: grid; grid-template-columns: auto 1fr; @@ -391,35 +382,33 @@ switch ($type) {
-
-
print($labelClass); ?>>print($label); ?>
+
+
print($label); ?>

print($message); ?>

+ print($type); ?> +
+
- - - print($type); ?> + + +
- -
- -
-
- +
Error trace
$traceItem): ?>
@@ -440,14 +429,14 @@ switch ($type) {
args
-
print(json_encode($traceItem['args'], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); ?>
+
print(\var_export($traceItem['args'], true)); ?>
-
+

Powered by

diff --git a/src/Appwrite/Extend/Exception.php b/src/Appwrite/Extend/Exception.php index 2d83667d44..f8f7b9ac6c 100644 --- a/src/Appwrite/Extend/Exception.php +++ b/src/Appwrite/Extend/Exception.php @@ -165,7 +165,7 @@ class Exception extends \Exception public const FUNCTION_SYNCHRONOUS_TIMEOUT = 'function_synchronous_timeout'; public const FUNCTION_TEMPLATE_NOT_FOUND = 'function_template_not_found'; public const FUNCTION_RUNTIME_NOT_DETECTED = 'function_runtime_not_detected'; - public const FUNCTION_EXECUTE_PERMISSION_DENIED = 'function_execute_permission_denied'; + public const FUNCTION_EXECUTE_PERMISSION_MISSING = 'function_execute_permission_missing'; /** Deployments */ public const DEPLOYMENT_NOT_FOUND = 'deployment_not_found'; @@ -323,9 +323,9 @@ class Exception extends \Exception protected array $errors = []; protected bool $publish; private array $ctas = []; - private string $view = ''; + private ?string $view = null; - public function __construct(string $type = Exception::GENERAL_UNKNOWN, string $message = null, int|string $code = null, \Throwable $previous = null, string $view = '') + public function __construct(string $type = Exception::GENERAL_UNKNOWN, string $message = null, int|string $code = null, \Throwable $previous = null, ?string $view = null) { $this->errors = Config::getParam('errors'); $this->type = $type; @@ -380,7 +380,7 @@ class Exception extends \Exception return $this->publish; } - public function addCTA(string $label, string $url): self + public function addCTA(string $label, ?string $url = null): self { $this->ctas[] = [ 'label' => $label, @@ -394,7 +394,7 @@ class Exception extends \Exception return $this->ctas; } - public function getView(): string + public function getView(): ?string { return $this->view; } diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index e44d66435a..f940ecf11d 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -2272,7 +2272,7 @@ class FunctionsCustomServerTest extends Scope ])); $this->assertEquals(401, $response['headers']['status-code']); - $this->assertStringContainsString('function_execute_permission_denied', $response['body']); + $this->assertStringContainsString('FUNCTION_EXECUTE_PERMISSION_MISSING', $response['body']); $this->cleanupFunction($functionId); } From 0fc41b27ee6dd46e0a4e49a5bdf36ea5030167bb Mon Sep 17 00:00:00 2001 From: Khushboo Verma Date: Tue, 15 Apr 2025 10:48:25 +0000 Subject: [PATCH 18/21] Dark theme for trace error page --- app/controllers/general.php | 8 ++--- app/views/general/error.phtml | 29 ++++++++++++++++++- .../Services/Sites/SitesCustomServerTest.php | 6 ++-- 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index dc026f238c..03f53b9582 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -74,7 +74,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw ]) )[0] ?? new Document(); } - + $errorView = __DIR__ . '/../views/general/error.phtml'; $url = (System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https') . '://' . System::getEnv('_APP_DOMAIN', ''); @@ -535,7 +535,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw } } } - + // Branded error pages (when developer left body empty) if ($executionResponse['statusCode'] >= 400 && empty($executionResponse['body'])) { $layout = new View($errorView); @@ -1304,9 +1304,9 @@ App::error() ->setStatusCode($code); $template = $error->getView() ?? (($route) ? $route->getLabel('error', null) : null); - + // TODO: Ideally use group 'api' here, but all wildcard routes seem to have 'api' at the moment - if(!\str_starts_with($route->getPath(), '/v1')) { + if (!\str_starts_with($route->getPath(), '/v1')) { $template = __DIR__ . '/../views/general/error.phtml'; } diff --git a/app/views/general/error.phtml b/app/views/general/error.phtml index 1f071b3299..48133087fd 100644 --- a/app/views/general/error.phtml +++ b/app/views/general/error.phtml @@ -240,7 +240,7 @@ switch ($type) { } .error-trace { - max-width: 800px; + max-width: 900px; padding: 20px; font-family: var(--font-family-sansSerif, Inter), sans-serif; } @@ -374,8 +374,35 @@ switch ($type) { .type { background: var(--color-overlay-on-neutral, rgba(25, 25, 28, 1)); color: var(--color-fgColor-neutral-secondary, #C3C3C6); + border: var(--border-width-S, 1px) solid var(--color-border-neutral-strong, #414146); + } + + .back-button { + color: var(--color-fgColor-neutral-secondary, #C3C3C6); + } + + .trace-grid { + background: var(--color-bgColor-neutral-secondary, #1D1D21); border: var(--border-width-S, 1px) solid var(--color-border-neutral-strong, #2D2D31); } + + .trace-grid-header { + background: var(--color-bgColor-neutral-secondary, #19191C); + border: var(--border-width-S, 1px) solid var(--color-border-neutral-strong, #2D2D31); + color: var(--color-fgColor-neutral-secondary, #C3C3C6); + } + + .trace-label { + color: var(--color-fgColor-neutral-secondary, #C3C3C6); + } + + .trace-value { + color: var(--color-fgColor-neutral-secondary, #C3C3C6); + } + + .trace-args { + color: var(--color-fgColor-neutral-secondary, #C3C3C6); + } } diff --git a/tests/e2e/Services/Sites/SitesCustomServerTest.php b/tests/e2e/Services/Sites/SitesCustomServerTest.php index db61767ffa..df6abfc833 100644 --- a/tests/e2e/Services/Sites/SitesCustomServerTest.php +++ b/tests/e2e/Services/Sites/SitesCustomServerTest.php @@ -2612,10 +2612,10 @@ class SitesCustomServerTest extends Scope ]); $this->assertEquals(400, $response['headers']['status-code']); $this->assertStringContainsString("Deployment build failed", $response['body']); - + $this->cleanupSite($siteId); } - + public function testEmptySiteSource(): void { $siteId = $this->setupSite([ @@ -2707,7 +2707,7 @@ class SitesCustomServerTest extends Scope $this->assertEquals('failed', $deployment['body']['status'], 'Deployment status does not match: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT)); $this->assertStringContainsString('Error:', $deployment['body']['buildLogs'], 'Deployment logs do not match: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT)); }, 100000, 500); - + $this->cleanupSite($siteId); } } From dbe42beb458cddc2010167ab50feca40fc17d14e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 15 Apr 2025 14:16:04 +0200 Subject: [PATCH 19/21] Improve tests --- app/controllers/general.php | 6 +- app/views/general/error.phtml | 44 ++++++++- .../Functions/FunctionsCustomServerTest.php | 95 +++++++++++++++++-- .../Services/Sites/SitesCustomServerTest.php | 38 ++++---- tests/resources/functions/php/index.php | 6 ++  a.txt | 8 ++ 6 files changed, 161 insertions(+), 36 deletions(-) create mode 100644  a.txt diff --git a/app/controllers/general.php b/app/controllers/general.php index 03f53b9582..079b7f642b 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -313,7 +313,9 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw if ($type === 'function') { $permissions = $resource->getAttribute('execute'); if (!(\in_array('any', $permissions)) && !(\in_array('guests', $permissions))) { - throw new AppwriteException(AppwriteException::FUNCTION_EXECUTE_PERMISSION_MISSING, view: $errorView); + $exception = new AppwriteException(AppwriteException::FUNCTION_EXECUTE_PERMISSION_MISSING, view: $errorView); + $exception->addCTA('View settings', $url . '/console/project-' . $project->getId() . '/functions/function-' . $resource->getId() . '/settings'); + throw $exception; } } @@ -548,6 +550,8 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw 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'; } } } diff --git a/app/views/general/error.phtml b/app/views/general/error.phtml index 48133087fd..d60b81f36c 100644 --- a/app/views/general/error.phtml +++ b/app/views/general/error.phtml @@ -25,6 +25,34 @@ if($exception !== null && method_exists($exception, 'getCTAs')) { } switch ($type) { + case 'empty_proxy_error': + $type = ''; + $label = 'Error ' . $code; + + $message = $code >= 500 ? 'An unexpected server error occured.' : 'An unexpected client error occured.'; + switch($code) { + case 401: + $message = 'You must sign in to access this page.'; + break; + case 403: + $message = 'You are not authorized to access this page.'; + break; + case 404: + $message = 'The page you are looking for does not exist.'; + break; + case 504: + $message = 'The server did not respond in time.'; + break; + case 501: + $message = 'This page is not implemented yet.'; + break; + } + + break; + case 'function_execute_permission_missing': + $label = 'Execution not permitted'; + $labelClass = 'warning'; + break; case 'build_not_ready': $label = 'Deployment is still building'; $message = 'The page will update after the build completes.'; @@ -40,7 +68,7 @@ switch ($type) { $message = 'This page is empty, but you can make it yours.'; break; case 'deployment_not_found': - $label = 'No deployments available'; + $label = 'No active deployments'; $message = 'This page is empty, activate a deployment to make it live.'; break; case 'build_canceled': @@ -246,7 +274,7 @@ switch ($type) { } .back-button { - margin-bottom: 24px; + margin-bottom: 12px; display: flex; align-items: center; gap: 8px; @@ -258,6 +286,10 @@ switch ($type) { line-height: 140%; letter-spacing: -0.45px; } + + .back-button:hover { + text-decoration: underline; + } .trace-grid { display: grid; @@ -412,9 +444,11 @@ switch ($type) {
print($label); ?>

print($message); ?>

-
- print($type); ?> -
+ +
+ print($type); ?> +
+
diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index c07b83a7e7..f2de2c7737 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -2189,7 +2189,8 @@ class FunctionsCustomServerTest extends Scope $response = $proxyClient->call(Client::METHOD_GET, '/'); $this->assertEquals(404, $response['headers']['status-code']); - $this->assertStringContainsString('This page is empty, but you can make it yours.', $response['body']); + $this->assertStringContainsString('Nothing is here yet', $response['body']); + $this->assertStringContainsString('Start with this domain', $response['body']); // failed deployment $functionId = $this->setupFunction([ @@ -2198,7 +2199,7 @@ class FunctionsCustomServerTest extends Scope 'runtime' => 'php-8.0', 'entrypoint' => 'index.php', 'timeout' => 15, - 'commands' => 'cd random', + 'commands' => 'cd non-existing-directory', 'execute' => ['any'] ]); @@ -2219,7 +2220,8 @@ class FunctionsCustomServerTest extends Scope ])); $this->assertEquals(404, $response['headers']['status-code']); - $this->assertStringContainsString('This page is empty, activate a deployment to make it live.', $response['body']); + $this->assertStringContainsString('No active deployments', $response['body']); + $this->assertStringContainsString('View deployments', $response['body']); // canceled deployment $deployment = $this->createDeployment($functionId, [ @@ -2241,29 +2243,32 @@ class FunctionsCustomServerTest extends Scope ])); $this->assertEquals(404, $response['headers']['status-code']); - $this->assertStringContainsString('This page is empty, activate a deployment to make it live.', $response['body']); + $this->assertStringContainsString('No active deployments', $response['body']); + $this->assertStringContainsString('View deployments', $response['body']); $this->cleanupFunction($functionId); + } - // no execute permission + public function testErrorPagesPermissions(): void + { $functionId = $this->setupFunction([ 'functionId' => ID::unique(), 'name' => 'Test Error Pages', 'runtime' => 'php-8.0', 'entrypoint' => 'index.php', - 'execute' => [], 'timeout' => 15, + 'commands' => '', + 'execute' => ['users'] ]); $domain = $this->setupFunctionDomain($functionId); + $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $domain); $deploymentId = $this->setupDeployment($functionId, [ - 'entrypoint' => 'index.php', 'code' => $this->packageFunction('php'), 'activate' => true ]); - $this->assertNotEmpty($deploymentId); $response = $proxyClient->call(Client::METHOD_GET, '/', array_merge([ @@ -2272,7 +2277,79 @@ class FunctionsCustomServerTest extends Scope ])); $this->assertEquals(401, $response['headers']['status-code']); - $this->assertStringContainsString('FUNCTION_EXECUTE_PERMISSION_MISSING', $response['body']); + $this->assertStringContainsString('Execution not permitted', $response['body']); + $this->assertStringContainsString('View settings', $response['body']); + + $this->cleanupFunction($functionId); + } + + public function testErrorPagesEmptyBody(): void + { + $functionId = $this->setupFunction([ + 'functionId' => ID::unique(), + 'name' => 'Test Error Pages', + 'runtime' => 'php-8.0', + 'entrypoint' => 'index.php', + 'timeout' => 15, + 'commands' => '', + 'execute' => ['any'] + ]); + + $domain = $this->setupFunctionDomain($functionId); + $proxyClient = new Client(); + $proxyClient->setEndpoint('http://' . $domain); + + $deploymentId = $this->setupDeployment($functionId, [ + 'code' => $this->packageFunction('php'), + 'activate' => true + ]); + $this->assertNotEmpty($deploymentId); + + $response = $proxyClient->call(Client::METHOD_GET, '/custom-response?code=404', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'] + ])); + $this->assertEquals(404, $response['headers']['status-code']); + $this->assertStringContainsString('Error 404', $response['body']); + $this->assertStringContainsString('does not exist', $response['body']); + + $response = $proxyClient->call(Client::METHOD_GET, '/custom-response?code=504', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'] + ])); + $this->assertEquals(504, $response['headers']['status-code']); + $this->assertStringContainsString('Error 504', $response['body']); + $this->assertStringContainsString('respond in time', $response['body']); + + $response = $proxyClient->call(Client::METHOD_GET, '/custom-response?code=400', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'] + ])); + $this->assertEquals(400, $response['headers']['status-code']); + $this->assertStringContainsString('Error 400', $response['body']); + $this->assertStringContainsString('unexpected client error', $response['body']); + + $response = $proxyClient->call(Client::METHOD_GET, '/custom-response?code=500', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'] + ])); + $this->assertEquals(500, $response['headers']['status-code']); + $this->assertStringContainsString('Error 500', $response['body']); + $this->assertStringContainsString('unexpected server error', $response['body']); + + $response = $proxyClient->call(Client::METHOD_GET, '/custom-response?code=400&body=CustomError400', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'] + ])); + $this->assertEquals(400, $response['headers']['status-code']); + $this->assertStringContainsString('CustomError400', $response['body']); + + $response = $proxyClient->call(Client::METHOD_GET, '/custom-response?code=500&body=CustomError500', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'] + ])); + $this->assertEquals(500, $response['headers']['status-code']); + $this->assertStringContainsString('CustomError500', $response['body']); $this->cleanupFunction($functionId); } diff --git a/tests/e2e/Services/Sites/SitesCustomServerTest.php b/tests/e2e/Services/Sites/SitesCustomServerTest.php index df6abfc833..e5f88461b2 100644 --- a/tests/e2e/Services/Sites/SitesCustomServerTest.php +++ b/tests/e2e/Services/Sites/SitesCustomServerTest.php @@ -2435,7 +2435,7 @@ class SitesCustomServerTest extends Scope }, 100000, 500); $response = $proxyClient->call(Client::METHOD_GET, '/'); - $this->assertStringContainsString('his page is empty, activate a deployment to make it live.', $response['body']); + $this->assertStringContainsString('This page is empty, activate a deployment to make it live.', $response['body']); $this->cleanupSite($siteId); } @@ -2515,16 +2515,16 @@ class SitesCustomServerTest extends Scope $response = $proxyClient->call(Client::METHOD_GET, '/'); $this->assertEquals(404, $response['headers']['status-code']); - $this->assertStringContainsString("This page is empty, but you can make it yours.", $response['body']); + $this->assertStringContainsString('Nothing is here yet', $response['body']); + $this->assertStringContainsString('Start with this domain', $response['body']); $siteId = $this->setupSite([ 'siteId' => ID::unique(), - 'name' => 'Astro SSR site', - 'framework' => 'astro', + 'name' => 'Static site', + 'framework' => 'other', 'buildRuntime' => 'node-22', - 'outputDirectory' => './dist', - 'buildCommand' => 'cd random', - 'installCommand' => 'npm install', + 'outputDirectory' => './', + 'buildCommand' => 'sleep 5 && cd non-existing-directory', ]); $this->assertNotEmpty($siteId); @@ -2532,29 +2532,20 @@ class SitesCustomServerTest extends Scope // test canceled deployment error page $deployment = $this->createDeployment($siteId, [ - 'code' => $this->packageSite('astro'), + 'code' => $this->packageSite('static'), 'activate' => 'true' ]); - $deploymentId = $deployment['body']['$id'] ?? ''; $this->assertEquals(202, $deployment['headers']['status-code']); $this->assertNotEmpty($deployment['body']['$id']); - $this->assertEquals(true, (new DatetimeValidator())->isValid($deployment['body']['$createdAt'])); - - $deploymentDomain = $this->getDeploymentDomain($deploymentId); - $this->assertNotEmpty($deploymentDomain); - - $this->assertEventually(function () use ($siteId, $deploymentId) { - $deployment = $this->getDeployment($siteId, $deploymentId); - - $this->assertEquals(200, $deployment['headers']['status-code']); - $this->assertEquals('building', $deployment['body']['status']); - }, 100000, 250); $deployment = $this->cancelDeployment($siteId, $deploymentId); $this->assertEquals(200, $deployment['headers']['status-code']); $this->assertEquals('canceled', $deployment['body']['status']); + $deploymentDomain = $this->getDeploymentDomain($deploymentId); + $this->assertNotEmpty($deploymentDomain); + $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $deploymentDomain); $response = $proxyClient->call(Client::METHOD_GET, '/', followRedirects: false); @@ -2571,12 +2562,14 @@ class SitesCustomServerTest extends Scope ]); $this->assertEquals(400, $response['headers']['status-code']); $this->assertStringContainsString("Deployment build canceled", $response['body']); + $this->assertStringContainsString("View deployments", $response['body']); // check site domain for no active deployments $proxyClient->setEndpoint('http://' . $domain); $response = $proxyClient->call(Client::METHOD_GET, '/'); $this->assertEquals(404, $response['headers']['status-code']); - $this->assertStringContainsString("No deployments available", $response['body']); + $this->assertStringContainsString('No active deployments', $response['body']); + $this->assertStringContainsString('View deployments', $response['body']); $deployment = $this->createDeployment($siteId, [ 'code' => $this->packageSite('astro'), @@ -2599,6 +2592,8 @@ class SitesCustomServerTest extends Scope ]); $this->assertEquals(400, $response['headers']['status-code']); $this->assertStringContainsString("Deployment is still building", $response['body']); + $this->assertStringContainsString("View logs", $response['body']); + $this->assertStringContainsString("Reload", $response['body']); $this->assertEventually(function () use ($siteId, $deploymentId) { $deployment = $this->getDeployment($siteId, $deploymentId); @@ -2612,6 +2607,7 @@ class SitesCustomServerTest extends Scope ]); $this->assertEquals(400, $response['headers']['status-code']); $this->assertStringContainsString("Deployment build failed", $response['body']); + $this->assertStringContainsString("View logs", $response['body']); $this->cleanupSite($siteId); } diff --git a/tests/resources/functions/php/index.php b/tests/resources/functions/php/index.php index 27a9418b3c..6c67f27ee1 100644 --- a/tests/resources/functions/php/index.php +++ b/tests/resources/functions/php/index.php @@ -1,6 +1,12 @@ req->path === '/custom-response') { + $code = (int) ($context->req->query['code'] ?? '200'); + $body = $context->req->query['body'] ?? ''; + return $context->res->send($body, $code); + } + $context->log('body-is-' . ($context->req->body ?? '')); $context->log('custom-header-is-' . ($context->req->headers['x-custom-header'] ?? '')); $context->log('method-is-' . \strtolower($context->req->method ?? '')); diff --git a/ a.txt b/ a.txt new file mode 100644 index 0000000000..c62bf8784d --- /dev/null +++ b/ a.txt @@ -0,0 +1,8 @@ +PHPUnit 9.6.22 by Sebastian Bergmann and contributors. + +Tests\E2E\Services\Sites\SitesCustomServerTest::testErrorPages ended in 7904.421256 milliseconds +. 1 / 1 (100%) + +Time: 00:07.907, Memory: 30.16 MB + +OK (1 test, 67 assertions) From 8777fdb417facef8a887a97ad7922c0a721c0f59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 15 Apr 2025 14:16:12 +0200 Subject: [PATCH 20/21] Remove leftover ---  a.txt | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644  a.txt diff --git a/ a.txt b/ a.txt deleted file mode 100644 index c62bf8784d..0000000000 --- a/ a.txt +++ /dev/null @@ -1,8 +0,0 @@ -PHPUnit 9.6.22 by Sebastian Bergmann and contributors. - -Tests\E2E\Services\Sites\SitesCustomServerTest::testErrorPages ended in 7904.421256 milliseconds -. 1 / 1 (100%) - -Time: 00:07.907, Memory: 30.16 MB - -OK (1 test, 67 assertions) From e53ec21a2c960b71884fee44516f6f5297444105 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 15 Apr 2025 15:17:43 +0200 Subject: [PATCH 21/21] Improve copy --- app/controllers/general.php | 2 +- app/views/general/error.phtml | 2 +- tests/e2e/Services/Sites/SitesBase.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index 079b7f642b..d5feba15f6 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -543,7 +543,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw $layout = new View($errorView); $layout ->setParam('title', $project->getAttribute('name') . ' - Error') - ->setParam('type', 'empty_proxy_error') + ->setParam('type', 'proxy_error_override') ->setParam('code', $executionResponse['statusCode']); $executionResponse['body'] = $layout->render(); diff --git a/app/views/general/error.phtml b/app/views/general/error.phtml index d60b81f36c..60afe86494 100644 --- a/app/views/general/error.phtml +++ b/app/views/general/error.phtml @@ -25,7 +25,7 @@ if($exception !== null && method_exists($exception, 'getCTAs')) { } switch ($type) { - case 'empty_proxy_error': + case 'proxy_error_override': $type = ''; $label = 'Error ' . $code; diff --git a/tests/e2e/Services/Sites/SitesBase.php b/tests/e2e/Services/Sites/SitesBase.php index ff7e4b283f..00edcc1b72 100644 --- a/tests/e2e/Services/Sites/SitesBase.php +++ b/tests/e2e/Services/Sites/SitesBase.php @@ -49,7 +49,7 @@ trait SitesBase 'x-appwrite-key' => $this->getProject()['apiKey'], ])); $this->assertEquals('ready', $deployment['body']['status'], 'Deployment status is not ready, deployment: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT)); - }, 100000, 500); + }, 150000, 500); // Not === so multipart/form-data works fine too if (($params['activate'] ?? false) == true) {