appwrite/appwrite · error · Appwrite\Extend\Exception

project_invalid_success_url

project_invalid_success_url

Error message

Invalid redirect URL for OAuth success.

What it means

Thrown by GET /v1/account/sessions/oauth2/:provider/redirect when the success redirect URL fails validation against the project's allowed hostnames (and no dev key is present). The redirect validator enforces allow-listed domains to prevent open-redirect abuse.

Source

Thrown at app/controllers/api/account.php:1566

            ], fn ($domain) => \is_string($domain) && $domain !== '');

            if (!empty($domains)) {
                $rules = $authorization->skip(fn () => $dbForPlatform->find('rules', [
                    Query::equal('domain', \array_values(\array_unique($domains))),
                    Query::equal('projectInternalId', [$project->getSequence()]),
                    Query::limit(2)
                ]));

                foreach ($rules as $rule) {
                    $allowedHostnames = $redirectValidator->getAllowedHostnames();
                    $allowedHostnames[] = $rule->getAttribute('domain', '');
                    $redirectValidator->setAllowedHostnames($allowedHostnames);
                }
            }
        }

        if ($devKey->isEmpty() && !$redirectValidator->isValid($state['success'])) {
            throw new Exception(Exception::PROJECT_INVALID_SUCCESS_URL);
        }

        if ($devKey->isEmpty() && !empty($state['failure']) && !$redirectValidator->isValid($state['failure'])) {
            throw new Exception(Exception::PROJECT_INVALID_FAILURE_URL);
        }
        $failure = [];
        if (!empty($state['failure'])) {
            $failure = URLParser::parse($state['failure']);
        }

        $failureRedirect = (function (string $type, ?string $message = null, ?int $code = null, ?\Throwable $previous = null, array $params = []) use ($failure, $response, $project, $oauthDefaultFailure) {
            $exception = new Exception($type, $message, $code, $previous, params: $params);
            if (!empty($failure)) {
                $query = URLParser::parseQuery($failure['query']);
                $query['error'] = json_encode([
                    'message' => $exception->getMessage(),
                    'type' => $exception->getType(),
                    'code' => !\is_null($code) ? $code : $exception->getCode(),

View on GitHub (pinned to cd368e707d)

Solutions

  1. Add the success URL's host to the project's allowed hostnames in Console > Auth > Allowed Hostnames.
  2. Use a registered project domain or a listed localhost host.
  3. If developing locally, use a dev key or add the local host to the allow-list.

Example fix

// before
await account.createOAuth2Session('github', 'https://unregistered.example.com/success'); // throws

// after: add unregistered.example.com to allowed hostnames, then
await account.createOAuth2Session('github', 'https://app.example.com/success');
Defensive patterns

Strategy: validation

Validate before calling

const allowedHosts = ['app.example.com','localhost'];
function successUrlOk(url) {
  const host = new URL(url).hostname;
  return allowedHosts.includes(host);
}
if (!successUrlOk(success)) throw new Error('Register success host in Console');

Type guard

const isAllowedHost = (url, hosts) => hosts.includes(new URL(url).hostname);

Try / catch

try { await account.createOAuth2Session(p, success, failure); } catch (e) { if (e?.type === 'project_invalid_success_url') { promptRegisterHost(success); } else throw e; }

Prevention

When it happens

Trigger: success URL host not in the project's allowed hostname list; success URL omitted/malformed when required; cross-origin redirect to an unregistered domain.

Common situations: New frontend domain not added to Auth > Allowed hosts; localhost used but not allow-listed; success URL pointing to a third-party domain.

Related errors


AI-assisted analysis of appwrite/appwrite@cd368e707d (2026-08-12). Data as JSON: /api/errors/57b67f4edbe0c183. Report an issue: GitHub.