DIYgod/RSSHub · error · Error

Failed to retrieve refresh token from MangaDex API.

Error message

Failed to retrieve refresh token from MangaDex API.

What it means

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.

Source

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

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

    const refreshToken = response?.data?.refresh_token;
    const accessToken = response?.data?.access_token;

    if (!refreshToken || !accessToken) {
        throw new Error('Failed to retrieve refresh token from MangaDex API.');
    }

    config.mangadex.refreshToken = refreshToken; // cache the refresh token
    return accessToken;
};

const getAccessTokenByRefreshToken = async () => {
    if (!config.mangadex.clientId || !config.mangadex.clientSecret) {
        throw new ConfigNotFoundError('Cannot get access token since MangaDex client ID or secret is not set.');
    }

    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,

View on GitHub (pinned to bed535e087)

Solutions

  1. Verify MANGADEX_USERNAME and MANGADEX_PASSWORD are set in the environment and the password is still valid by logging in at mangadex.org.
  2. Confirm MANGADEX_CLIENT_ID and MANGADEX_CLIENT_SECRET match a Personal API Client created under mangadex.org/settings/options (not the legacy API client).
  3. Temporarily log response.data (and response.statusCode) right before the throw to see the Keycloak error_description, then fix the specific cause.
  4. If the account uses 2FA, generate a personal client + refresh token out-of-band and set MANGADEX_REFRESH_TOKEN instead of username/password.
  5. If MangaDex changed the token payload shape, update _access.ts to read the new field path.

Example fix

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

Strategy: try-catch

Validate before calling

// pre-flight: ensure the password-grant inputs are present
const need = ['MANGADEX_USERNAME','MANGADEX_PASSWORD','MANGADEX_CLIENT_ID','MANGADEX_CLIENT_SECRET'];
const missing = need.filter((k) => !process.env[k]);
if (missing.length) {
    throw new Error('Missing MangaDex env: ' + missing.join(', '));
}

Type guard

interface MangadexTokenResponse {
    access_token?: string;
    refresh_token?: string;
    error_description?: string;
}
const isTokenResponse = (v: unknown): v is MangadexTokenResponse =>
    typeof v === 'object' && v !== null &&
    typeof (v as MangadexTokenResponse).access_token === 'string';

Try / catch

// in the caller of getToken / getAccessTokenByUserCredentials
try {
    token = await getToken();
} catch (e) {
    // re-auth path or surface a config-flavored error to the operator
    throw new ConfigNotFoundError('MangaDex auth failed; check MANGADEX_* credentials: ' + (e as Error).message);
}

Prevention

When it happens

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

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

Related errors


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