{"record":{"id":"a54ac89a01a41430","repo":"DIYgod/RSSHub","slug":"failed-to-retrieve-access-token-from-mangadex-api","errorCode":null,"errorMessage":"Failed to retrieve access token from MangaDex API.","messagePattern":"Failed to retrieve access token from MangaDex API\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"lib/routes/mangadex/_access.ts","lineNumber":107,"sourceCode":"    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,\n        },\n        form: {\n            grant_type: 'refresh_token',\n            refresh_token: config.mangadex.refreshToken,\n            client_id: config.mangadex.clientId,\n            client_secret: config.mangadex.clientSecret,\n        },\n    });\n\n    const accessToken = response?.data?.access_token;\n    if (!accessToken) {\n        throw new Error('Failed to retrieve access token from MangaDex API.');\n    }\n    return accessToken;\n};\n\nexport default getToken;\n","sourceCodeStart":89,"sourceCodeEnd":113,"githubUrl":"https://github.com/DIYgod/RSSHub/blob/bed535e0879dc71c5aff6f1e7bd1ac21ede40115/lib/routes/mangadex/_access.ts#L89-L113","documentation":"Thrown after POSTing grant_type=refresh_token to the MangaDex token endpoint when the response body has no access_token. Unlike an HTTP 400 (which getToken catches and falls back to user credentials), this fires when the endpoint returns 200 with an unusable body, so the automatic fallback does not trigger.","triggerScenarios":"POST to constants.API.TOKEN with grant_type=refresh_token where response.data.access_token is undefined/null. The refresh token is expired, revoked, belongs to another client, or MangaDex returned an error object with a non-400 status.","commonSituations":"Cached MANGADEX_REFRESH_TOKEN older than 30 days (MangaDex refresh tokens expire); refresh token minted under a different clientId; refresh token pasted with trailing whitespace; Keycloak returned an error body but with status 200.","solutions":["If MANGADEX_USERNAME/PASSWORD are set, let getToken fall back: confirm the FetchError-400 branch fires by checking logs; if it does not, the response is a 200-with-error and you must obtain a fresh refresh token.","Regenerate the refresh token by performing one password-grant login, then store the returned refresh_token as MANGADEX_REFRESH_TOKEN.","Verify the refresh token has no leading/trailing whitespace and was issued for the same clientId/clientSecret pair.","Add response.data inspection before the throw to expose the underlying Keycloak error_description."],"exampleFix":"// before\nconst accessToken = response?.data?.access_token;\nif (!accessToken) {\n    throw new Error('Failed to retrieve access token from MangaDex API.');\n}\n// after\nconst accessToken = response?.data?.access_token;\nif (!accessToken) {\n    const detail = response?.data?.error_description || JSON.stringify(response?.data);\n    throw new Error(`Failed to retrieve access token from MangaDex API: ${detail}`);\n}","handlingStrategy":"retry","validationCode":"if (!config.mangadex.refreshToken) {\n    // getToken will take the password path instead; no point validating a missing token\n    return;\n}\n// a refresh token is a Keycloak JWT; cheap sanity check on shape\nconst parts = config.mangadex.refreshToken.split('.');\nif (parts.length < 2) {\n    throw new Error('MANGADEX_REFRESH_TOKEN does not look like a JWT');\n}","typeGuard":"const isRefreshTokenBody = (v: unknown): v is { access_token: string } =>\n    typeof v === 'object' && v !== null && typeof (v as { access_token?: unknown }).access_token === 'string';","tryCatchPattern":"try {\n    token = await getToken(); // getToken already retries via password grant on HTTP 400\n} catch (e) {\n    if (/access token/i.test((e as Error).message)) {\n        // refresh token likely expired; clear and let the next request re-auth\n        delete config.mangadex.refreshToken;\n    }\n    throw e;\n}","preventionTips":["Treat refresh tokens as expiring credentials; rotate them periodically.","Always pair MANGADEX_REFRESH_TOKEN with username/password so getToken can fall back.","Trim whitespace when pasting tokens to avoid silent 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"}