{"record":{"id":"cf729ce02a4707a3","repo":"DIYgod/RSSHub","slug":"failed-to-retrieve-refresh-token-from-mangadex-api","errorCode":null,"errorMessage":"Failed to retrieve refresh token from MangaDex API.","messagePattern":"Failed to retrieve refresh token from MangaDex API\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"lib/routes/mangadex/_access.ts","lineNumber":77,"sourceCode":"\n    const response = await got.post(constants.API.TOKEN, {\n        headers: {\n            'User-Agent': config.trueUA,\n        },\n        form: {\n            grant_type: 'password',\n            username: config.mangadex.username,\n            password: config.mangadex.password,\n            client_id: config.mangadex.clientId,\n            client_secret: config.mangadex.clientSecret,\n        },\n    });\n\n    const refreshToken = response?.data?.refresh_token;\n    const accessToken = response?.data?.access_token;\n\n    if (!refreshToken || !accessToken) {\n        throw new Error('Failed to retrieve refresh token from MangaDex API.');\n    }\n\n    config.mangadex.refreshToken = refreshToken; // cache the refresh token\n    return accessToken;\n};\n\nconst getAccessTokenByRefreshToken = async () => {\n    if (!config.mangadex.clientId || !config.mangadex.clientSecret) {\n        throw new ConfigNotFoundError('Cannot get access token since MangaDex client ID or secret is not set.');\n    }\n\n    if (!config.mangadex.refreshToken) {\n        throw new ConfigNotFoundError('Cannot get access token since MangaDex refresh token is not set.');\n    }\n\n    const response = await got.post(constants.API.TOKEN, {\n        headers: {\n            'User-Agent': config.trueUA,","sourceCodeStart":59,"sourceCodeEnd":95,"githubUrl":"https://github.com/DIYgod/RSSHub/blob/bed535e0879dc71c5aff6f1e7bd1ac21ede40115/lib/routes/mangadex/_access.ts#L59-L95","documentation":"Thrown after POSTing credentials to the MangaDex OpenID token endpoint (auth.mangadex.org/realms/mangadex/protocol/openid-connect/token) with grant_type=password when the parsed body is missing either refresh_token or access_token. It means the request did not raise an HTTP error but the returned payload is not a valid token response (often Keycloak returns an error object with HTTP 200). This guards the assumption that a successful password grant yields both tokens.","triggerScenarios":"POST to constants.API.TOKEN with username/password/client_id/client_secret where response.data.refresh_token or response.data.access_token is undefined, null, or empty string. Happens when Keycloak rejects credentials inside the body, the account is 2FA-locked, clientId/clientSecret are wrong, or the response shape changed.","commonSituations":"MANGADEX_USERNAME / MANGADEX_PASSWORD env vars unset or stale; password containing special characters that were not encoded into form fields; clientId/clientSecret that do not match a registered MangaDex personal client; account with mandatory 2FA; MangaDex Keycloak realm temporarily returning error payloads with 200 status.","solutions":["Verify MANGADEX_USERNAME and MANGADEX_PASSWORD are set in the environment and the password is still valid by logging in at mangadex.org.","Confirm MANGADEX_CLIENT_ID and MANGADEX_CLIENT_SECRET match a Personal API Client created under mangadex.org/settings/options (not the legacy API client).","Temporarily log response.data (and response.statusCode) right before the throw to see the Keycloak error_description, then fix the specific cause.","If the account uses 2FA, generate a personal client + refresh token out-of-band and set MANGADEX_REFRESH_TOKEN instead of username/password.","If MangaDex changed the token payload shape, update _access.ts to read the new field path."],"exampleFix":"// before\nif (!refreshToken || !accessToken) {\n    throw new Error('Failed to retrieve refresh token from MangaDex API.');\n}\n// after\nif (!refreshToken || !accessToken) {\n    const detail = response?.data?.error_description || JSON.stringify(response?.data);\n    throw new Error(`Failed to retrieve refresh token from MangaDex API: ${detail}`);\n}","handlingStrategy":"try-catch","validationCode":"// pre-flight: ensure the password-grant inputs are present\nconst need = ['MANGADEX_USERNAME','MANGADEX_PASSWORD','MANGADEX_CLIENT_ID','MANGADEX_CLIENT_SECRET'];\nconst missing = need.filter((k) => !process.env[k]);\nif (missing.length) {\n    throw new Error('Missing MangaDex env: ' + missing.join(', '));\n}","typeGuard":"interface MangadexTokenResponse {\n    access_token?: string;\n    refresh_token?: string;\n    error_description?: string;\n}\nconst isTokenResponse = (v: unknown): v is MangadexTokenResponse =>\n    typeof v === 'object' && v !== null &&\n    typeof (v as MangadexTokenResponse).access_token === 'string';","tryCatchPattern":"// in the caller of getToken / getAccessTokenByUserCredentials\ntry {\n    token = await getToken();\n} catch (e) {\n    // re-auth path or surface a config-flavored error to the operator\n    throw new ConfigNotFoundError('MangaDex auth failed; check MANGADEX_* credentials: ' + (e as Error).message);\n}","preventionTips":["Store all MANGADEX_* credentials as env vars rather than editing config.ts.","Validate credentials with a one-off curl to the token endpoint before enabling the route.","Set MASTODON-style monitoring on the mangadex:access-token cache key to detect repeated auth failures."],"tags":["mangadex","authentication","oauth","config","network"],"backgroundTag":null,"analyzedSha":"bed535e0879dc71c5aff6f1e7bd1ac21ede40115","analyzedAt":"2026-08-12T19:29:35.364Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}