{"record":{"id":"f975718e06c3f143","repo":"nextcloud/all-in-one","slug":"could-not-extract-the-api-token-from-the-desec-log","errorCode":null,"errorMessage":"Could not extract the API token from the deSEC login response. Please try again.","messagePattern":"Could not extract the API token from the deSEC login response\\. Please try again\\.","errorType":"exception","errorClass":"\\Exception","httpStatus":422,"severity":"error","filePath":"php/src/Desec/DesecManager.php","lineNumber":263,"sourceCode":"            ]);\n        } catch (TransferException $e) {\n            throw new \\Exception('Could not reach the deSEC API: ' . $e->getMessage());\n        }\n\n        $code = $res->getStatusCode();\n        $body = $res->getBody()->getContents();\n\n        if ($code === 400 || $code === 403) {\n            throw new \\Exception('Could not log in to deSEC: invalid email address or password.');\n        }\n\n        if ($code !== 200 && $code !== 201) {\n            throw new \\Exception('Unexpected response from deSEC during login (HTTP ' . $code . '): ' . $body);\n        }\n\n        $data = json_decode($body, true, 512, JSON_THROW_ON_ERROR);\n        if (!is_array($data) || !isset($data['token']) || !is_string($data['token'])) {\n            throw new \\Exception('Could not extract the API token from the deSEC login response. Please try again.');\n        }\n\n        return $data['token'];\n    }\n\n    /**\n     * Registers a dedyn.io domain for the authenticated account.\n     * When $slug is empty a random 10-character slug is tried up to MAX_SLUG_ATTEMPTS times.\n     *\n     * When a specific slug is requested and creation fails because the name is unavailable\n     * (HTTP 400/409) or the account's domain limit is reached (HTTP 403), the domain may\n     * already belong to this very account — a user reusing a slug they registered earlier.\n     * In that case we reuse the existing domain instead of failing, so an existing-account\n     * login can point AIO at a domain the user already owns. (deSEC returns 400 when a name\n     * conflicts with another user's zone and 403 once the per-account domain limit is hit;\n     * both look like a failure here even though the user owns the name.)\n     *\n     * @return string the fully-qualified domain name that was registered","sourceCodeStart":245,"sourceCodeEnd":281,"githubUrl":"https://github.com/nextcloud/all-in-one/blob/6b788eec5e61733cf03ed380a3572e43ae3f11ce/php/src/Desec/DesecManager.php#L245-L281","documentation":"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.","triggerScenarios":"deSEC API change renaming/moving the token field; an HTML page returned by an intermediary proxy behind a 200 status; a truncated response body.","commonSituations":"Transparent proxy or captive portal rewriting responses; desec.io deploying a new API version; extremely rare malformed payload on the wire.","solutions":["Log the raw login response body and compare it against the deSEC OpenAPI auth/login schema","Check https://desec.io/ docs for API response-shape changes","If a proxy interferes, bypass or correctly configure it for desec.io","Retry once — transient truncation can yield invalid JSON"],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":"// Shape check for a deSEC login payload — usable if you call the API directly\nfunction isValidDesecTokenResponse(mixed $data): bool {\n    return is_array($data) && isset($data['token']) && is_string($data['token']) && $data['token'] !== '';\n}","tryCatchPattern":"try {\n    $token = $manager->loginAccount($email, $password);\n} catch (\\Exception $e) {\n    if (str_contains($e->getMessage(), 'Could not extract the API token')) {\n        sleep(2);\n        return $manager->loginAccount($email, $password); // one retry: transient truncation is plausible\n    }\n    throw $e;\n}","preventionTips":["Treat a schema drift here as an API-version incident — diff the raw body against the documented {token: string} shape","Ensure no intermediary proxy can rewrite desec.io responses","Pin and review deSEC API docs when bumping integration code"],"tags":["php","desec","json","api-contract"],"backgroundTag":"malformed-api-response","analyzedSha":"6b788eec5e61733cf03ed380a3572e43ae3f11ce","analyzedAt":"2026-08-21T05:47:24.382Z","schemaVersion":2},"datasetVersion":"2026-08-21T11:28:35.574Z"}