gildas-lormeau/SingleFile · error · Error

unknown_token

unknown_token

Error message

unknown_token

What it means

gdrive's refreshAuthToken exchanges the stored refresh token with Google's OAuth token endpoint. A 400 response means Google rejected the refresh grant — typically an invalid, expired, or revoked refresh token — so the library throws 'unknown_token'.

Source

Thrown at src/lib/gdrive/gdrive.js:91

			} else {
				delete this.accessToken;
				delete this.refreshToken;
				delete this.expirationDate;
			}
		}
	}
	async refreshAuthToken() {
		if (this.refreshToken) {
			const httpResponse = await fetch(TOKEN_URL, {
				method: "POST",
				headers: { "Content-Type": "application/x-www-form-urlencoded" },
				body: "client_id=" + this.clientId +
					"&refresh_token=" + this.refreshToken +
					"&grant_type=refresh_token" +
					"&client_secret=" + this.clientKey
			});
			if (httpResponse.status == 400) {
				throw new Error("unknown_token");
			}
			const response = await getJSON(httpResponse);
			this.accessToken = response.access_token;
			if (response.refresh_token) {
				this.refreshToken = response.refresh_token;
			}
			if (response.expires_in) {
				this.expirationDate = Date.now() + (response.expires_in * 1000);
			}
			return { accessToken: this.accessToken, refreshToken: this.refreshToken, expirationDate: this.expirationDate };
		} else {
			try {
				if (browser.identity && browser.identity.removeCachedAuthToken && this.accessToken) {
					await browser.identity.removeCachedAuthToken({ token: this.accessToken });
				}
				this.accessToken = await getAuthToken({ interactive: false });
				return { revokableAccessToken: this.accessToken };
				// eslint-disable-next-line no-unused-vars

View on GitHub (pinned to 517fb7c5cf)

Solutions

  1. Re-run the interactive OAuth flow to obtain a fresh refresh token (delete stored credentials first).
  2. Verify clientId/clientKey (secret) match the OAuth client that issued the refresh token.
  3. Ask the user to re-authorize the app in Google Account permissions if it was revoked.
  4. Catch this error in upload code and fall back to launching a new auth flow instead of retrying the same token.

Example fix

// before
await gdrive.auth({ refreshToken: storedToken }); // throws unknown_token
// after
try {
  await gdrive.auth({ refreshToken: storedToken });
} catch (e) {
  if (e.message === 'unknown_token') {
    await gdrive.auth({ interactive: true }); // new OAuth flow
  } else throw e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

// verify a refresh token exists and matches current client credentials before auth
if (!refreshToken || clientId !== CLIENT_ID_USED_AT_ISSUANCE || clientKey !== CLIENT_SECRET) {
  await gdrive.auth({ interactive: true });
}

Try / catch

try {
  await gdrive.auth({ refreshToken });
} catch (e) {
  if (e.message === 'unknown_token') {
    await gdrive.auth({ interactive: true }); // re-run OAuth to get a new refresh token
  } else throw e;
}

Prevention

When it happens

Trigger: Calling auth/upload with a refresh token that has been revoked, expired (unused for 6+ months), invalidated by changing the Google account password, or issued for a different client_id/client_secret pair.

Common situations: Users revoking app access in Google Account settings; Google auto-expiring dormant refresh tokens; dev/client_secret mismatch after switching OAuth clients; multiple authorizations invalidating older tokens on unverified apps.

Related errors


AI-assisted analysis of gildas-lormeau/SingleFile@517fb7c5cf (2026-09-01). Data as JSON: /api/errors/54e5e79e4e9f4dc7. Report an issue: GitHub.