DIYgod/RSSHub · error · Error

Failed to retrieve access token from MangaDex API.

Error message

Failed to retrieve access token from MangaDex API.

What it means

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.

Source

Thrown at lib/routes/mangadex/_access.ts:107

    if (!config.mangadex.refreshToken) {
        throw new ConfigNotFoundError('Cannot get access token since MangaDex refresh token is not set.');
    }

    const response = await got.post(constants.API.TOKEN, {
        headers: {
            'User-Agent': config.trueUA,
        },
        form: {
            grant_type: 'refresh_token',
            refresh_token: config.mangadex.refreshToken,
            client_id: config.mangadex.clientId,
            client_secret: config.mangadex.clientSecret,
        },
    });

    const accessToken = response?.data?.access_token;
    if (!accessToken) {
        throw new Error('Failed to retrieve access token from MangaDex API.');
    }
    return accessToken;
};

export default getToken;

View on GitHub (pinned to bed535e087)

Solutions

  1. 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.
  2. Regenerate the refresh token by performing one password-grant login, then store the returned refresh_token as MANGADEX_REFRESH_TOKEN.
  3. Verify the refresh token has no leading/trailing whitespace and was issued for the same clientId/clientSecret pair.
  4. Add response.data inspection before the throw to expose the underlying Keycloak error_description.

Example fix

// before
const accessToken = response?.data?.access_token;
if (!accessToken) {
    throw new Error('Failed to retrieve access token from MangaDex API.');
}
// after
const accessToken = response?.data?.access_token;
if (!accessToken) {
    const detail = response?.data?.error_description || JSON.stringify(response?.data);
    throw new Error(`Failed to retrieve access token from MangaDex API: ${detail}`);
}
Defensive patterns

Strategy: retry

Validate before calling

if (!config.mangadex.refreshToken) {
    // getToken will take the password path instead; no point validating a missing token
    return;
}
// a refresh token is a Keycloak JWT; cheap sanity check on shape
const parts = config.mangadex.refreshToken.split('.');
if (parts.length < 2) {
    throw new Error('MANGADEX_REFRESH_TOKEN does not look like a JWT');
}

Type guard

const isRefreshTokenBody = (v: unknown): v is { access_token: string } =>
    typeof v === 'object' && v !== null && typeof (v as { access_token?: unknown }).access_token === 'string';

Try / catch

try {
    token = await getToken(); // getToken already retries via password grant on HTTP 400
} catch (e) {
    if (/access token/i.test((e as Error).message)) {
        // refresh token likely expired; clear and let the next request re-auth
        delete config.mangadex.refreshToken;
    }
    throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of DIYgod/RSSHub@bed535e087 (2026-08-12). Data as JSON: /api/errors/a54ac89a01a41430. Report an issue: GitHub.