{"record":{"id":"916050d9b3098101","repo":"gildas-lormeau/SingleFile","slug":"error-message-google-drive","errorCode":null,"errorMessage":"error.message + \" (Google Drive)\"","messagePattern":"error\\.message \\+ \" \\(Google Drive\\)\"","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/core/bg/downloads.js","lineNumber":542,"sourceCode":"\nasync function saveToGDrive(taskId, filename, blob, authOptions, uploadOptions) {\n\ttry {\n\t\tawait getAuthInfo(authOptions);\n\t\tconst taskInfo = business.getTaskInfo(taskId);\n\t\tif (!taskInfo || !taskInfo.cancelled) {\n\t\t\treturn await gDrive.upload(filename, blob, uploadOptions, callback => business.setCancelCallback(taskId, callback));\n\t\t}\n\t}\n\tcatch (error) {\n\t\tif (error.message == \"invalid_token\") {\n\t\t\tlet authInfo;\n\t\t\ttry {\n\t\t\t\tauthInfo = await gDrive.refreshAuthToken();\n\t\t\t} catch (error) {\n\t\t\t\tif (error.message == \"unknown_token\") {\n\t\t\t\t\tauthInfo = await getAuthInfo(authOptions, true);\n\t\t\t\t} else {\n\t\t\t\t\tthrow new Error(error.message + \" (Google Drive)\", { cause: error });\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (authInfo) {\n\t\t\t\tawait config.setAuthInfo(authInfo);\n\t\t\t} else {\n\t\t\t\tawait config.removeAuthInfo();\n\t\t\t}\n\t\t\treturn await saveToGDrive(taskId, filename, blob, authOptions, uploadOptions);\n\t\t} else {\n\t\t\tthrow new Error(error.message + \" (Google Drive)\", { cause: error });\n\t\t}\n\t}\n}\n\nasync function saveToDropbox(taskId, filename, blob, uploadOptions) {\n\ttry {\n\t\tawait getDropboxAuthInfo();\n\t\tconst taskInfo = business.getTaskInfo(taskId);","sourceCodeStart":524,"sourceCodeEnd":560,"githubUrl":"https://github.com/gildas-lormeau/SingleFile/blob/517fb7c5cf2096d89933b747e862d8ecf616a9f9/src/core/bg/downloads.js#L524-L560","documentation":"In saveToGDrive, when refreshing the Google OAuth token fails with any message other than \"unknown_token\", the error is rethrown with an \" (Google Drive)\" suffix and the original as cause. It means Google Drive token refresh failed for a non-recoverable reason.","triggerScenarios":"gDrive.refreshAuthToken() rejects with e.g. 'invalid_grant' during saveToGDrive called from downloadContent/downloadCompressedContent; only 'unknown_token' is handled by re-auth, everything else is rethrown.","commonSituations":"User revoked the app in Google account settings; refresh token expired after 6 months of inactivity or password change; OAuth client credentials changed; Google API quota or server outage.","solutions":["Re-authorize Google Drive access in SingleFile options to obtain fresh tokens","Check error.cause: 'invalid_grant' means the refresh token is dead and re-consent is required","Verify the system clock is accurate (token validation is time-sensitive)","Check Google Cloud OAuth client config if you self-hosted the credentials"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// verify stored auth before saving\nconst authInfo = await config.getAuthInfo();\nif (!authInfo || !authInfo.refreshToken) {\n  await getAuthInfo(authOptions, true); // force interactive re-auth\n}","typeGuard":null,"tryCatchPattern":"try {\n  await saveToGDrive(taskId, filename, blob, authOptions, uploadOptions);\n} catch (error) {\n  if (error.message.endsWith('(Google Drive)') && String(error.cause).includes('invalid_grant')) {\n    await getAuthInfo(authOptions, true); // force re-consent\n  } else { throw error; }\n}","preventionTips":["Re-authorize the Google account periodically to keep the refresh token alive","Do not revoke the app in Google security settings","Keep system clock synchronized","Treat 'invalid_grant' in error.cause as a signal to re-consent, not retry"],"tags":["google-drive","oauth","token-refresh","error-wrapping"],"backgroundTag":"oauth-token-refresh-failed","analyzedSha":"517fb7c5cf2096d89933b747e862d8ecf616a9f9","analyzedAt":"2026-09-01T10:05:25.770Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T15:18:49.778Z"}