nextcloud/all-in-one · error · \Exception

Could not extract the API token from the deSEC login respons

Error message

Could not extract the API token from the deSEC login response. Please try again.

What it means

POST /auth/login/ returned 200/201, but the body either failed json_decode (JSON_THROW_ON_ERROR fires and, being a JsonException, escapes the same code path in practice) or the decoded value was not an array containing a string 'token' field. The documented contract {"token": "..."} was violated.

Source

Thrown at php/src/Desec/DesecManager.php:263

            ]);
        } catch (TransferException $e) {
            throw new \Exception('Could not reach the deSEC API: ' . $e->getMessage());
        }

        $code = $res->getStatusCode();
        $body = $res->getBody()->getContents();

        if ($code === 400 || $code === 403) {
            throw new \Exception('Could not log in to deSEC: invalid email address or password.');
        }

        if ($code !== 200 && $code !== 201) {
            throw new \Exception('Unexpected response from deSEC during login (HTTP ' . $code . '): ' . $body);
        }

        $data = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
        if (!is_array($data) || !isset($data['token']) || !is_string($data['token'])) {
            throw new \Exception('Could not extract the API token from the deSEC login response. Please try again.');
        }

        return $data['token'];
    }

    /**
     * Registers a dedyn.io domain for the authenticated account.
     * When $slug is empty a random 10-character slug is tried up to MAX_SLUG_ATTEMPTS times.
     *
     * When a specific slug is requested and creation fails because the name is unavailable
     * (HTTP 400/409) or the account's domain limit is reached (HTTP 403), the domain may
     * already belong to this very account — a user reusing a slug they registered earlier.
     * In that case we reuse the existing domain instead of failing, so an existing-account
     * login can point AIO at a domain the user already owns. (deSEC returns 400 when a name
     * conflicts with another user's zone and 403 once the per-account domain limit is hit;
     * both look like a failure here even though the user owns the name.)
     *
     * @return string the fully-qualified domain name that was registered

View on GitHub (pinned to 6b788eec5e)

Solutions

  1. Log the raw login response body and compare it against the deSEC OpenAPI auth/login schema
  2. Check https://desec.io/ docs for API response-shape changes
  3. If a proxy interferes, bypass or correctly configure it for desec.io
  4. Retry once — transient truncation can yield invalid JSON
Defensive patterns

Strategy: retry

Type guard

// Shape check for a deSEC login payload — usable if you call the API directly
function isValidDesecTokenResponse(mixed $data): bool {
    return is_array($data) && isset($data['token']) && is_string($data['token']) && $data['token'] !== '';
}

Try / catch

try {
    $token = $manager->loginAccount($email, $password);
} catch (\Exception $e) {
    if (str_contains($e->getMessage(), 'Could not extract the API token')) {
        sleep(2);
        return $manager->loginAccount($email, $password); // one retry: transient truncation is plausible
    }
    throw $e;
}

Prevention

When it happens

Trigger: deSEC API change renaming/moving the token field; an HTML page returned by an intermediary proxy behind a 200 status; a truncated response body.

Common situations: Transparent proxy or captive portal rewriting responses; desec.io deploying a new API version; extremely rare malformed payload on the wire.

Related errors


AI-assisted analysis of nextcloud/all-in-one@6b788eec5e (2026-08-21). Data as JSON: /api/errors/f975718e06c3f143. Report an issue: GitHub.