{"record":{"id":"80f3d7e26c1f824f","repo":"yikart/AiToEarn","slug":"responsecode-channelauthsessioncompleted","errorCode":"ResponseCode.ChannelAuthSessionCompleted","errorMessage":"ChannelAuthSessionCompleted","messagePattern":"ChannelAuthSessionCompleted","errorType":"error_code","errorClass":"AppException","httpStatus":null,"severity":"warning","filePath":"project/aitoearn-backend/apps/aitoearn-server/src/core/channels/auth/auth.service.ts","lineNumber":115,"sourceCode":"  }\n\n  async completeCallback(\n    platform: AccountType,\n    callbackInput: Omit<AuthCallbackInput, 'session'>,\n    sessionId: string,\n  ): Promise<AuthCallbackResult> {\n    const session = await this.redis.getChannelAuthSession<AuthSession>(sessionId)\n    if (!this.isAccountAuthSessionRecord(session)) {\n      throw new AppException(ResponseCode.ChannelAuthSessionInvalid)\n    }\n    if (this.isSessionExpired(session)) {\n      throw new AppException(ResponseCode.ChannelAuthSessionInvalid)\n    }\n    if (session.platform !== platform) {\n      throw new AppException(ResponseCode.ChannelAuthPlatformMismatch)\n    }\n    if (session.status !== ChannelAuthSessionStatus.Pending) {\n      throw new AppException(ResponseCode.ChannelAuthSessionCompleted)\n    }\n\n    const provider = this.registry.getAuth(platform)\n    const credentialResult = await provider.exchangeCode({\n      ...callbackInput,\n      session,\n    })\n\n    const credentialContext = credentialResult.accessToken\n      ? this.toCredentialContext(credentialResult)\n      : undefined\n    const profile = credentialResult.profile\n      ?? (credentialContext\n        ? await provider.getProfile(credentialContext)\n        : undefined)\n    if (!profile) {\n      throw new AppException(ResponseCode.ChannelAuthPlatformUidMissing)\n    }","sourceCodeStart":97,"sourceCodeEnd":133,"githubUrl":"https://github.com/yikart/AiToEarn/blob/d3aa8bea5b146a8675607cf0144d891aad3e9683/project/aitoearn-backend/apps/aitoearn-server/src/core/channels/auth/auth.service.ts#L97-L133","documentation":"Thrown in AuthService.completeCallback when the session status is not Pending — the flow was already completed (or failed/marked). Auth sessions are single-use: once completed and an account connected, further callbacks with the same state are rejected.","triggerScenarios":"completeCallback (or the callback endpoint) is hit a second time with the same state/sessionId — e.g. the provider retries the redirect, the user double-clicks/back-button refreshes the callback URL, or the same authorization code is delivered twice.","commonSituations":"Browser refresh on the callback page resubmitting the OAuth code; provider-side automatic retry of the redirect; user replaying a success URL from history; duplicate webhook/callback delivery.","solutions":["Treat this as benign idempotency: return the already-connected account instead of restarting the flow if appropriate for your UX.","Restart the auth flow to link additional or different accounts.","Guard the client against double submission (disable the button, avoid refresh on POST callbacks).","If provider retries are expected, configure the platform callback to tolerate completed sessions by looking up the session's stored accountId/accounts."],"exampleFix":"// before: refreshing the callback URL re-runs the flow\nGET /api/channels/auth/callback/instagram?code=abc&state=s1  // 2nd time -> error\n\n// after: on ChannelAuthSessionCompleted, show the existing result instead\ntry { await completeCallback(...) }\ncatch (e) {\n  if (getErrorCode(e) === ResponseCode.ChannelAuthSessionCompleted) {\n    showAlreadyConnectedMessage() // do not restart flow\n  }\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  await completeCallback(platform, input, sessionId)\n}\ncatch (e) {\n  if (getErrorCode(e) === ResponseCode.ChannelAuthSessionCompleted) {\n    return renderAlreadyConnectedView(sessionId) // idempotent success\n  }\n  throw e\n}","preventionTips":["Disable submit buttons after first click","Avoid cacheable callback GET responses so refresh doesn't replay","Treat completed-session callbacks as idempotent successes server-side","Deduplicate provider retries using the authorization code/state as idempotency key"],"tags":["oauth","session","idempotency","double-submit"],"backgroundTag":"oauth-session-already-used","analyzedSha":"d3aa8bea5b146a8675607cf0144d891aad3e9683","analyzedAt":"2026-08-31T14:19:24.185Z","schemaVersion":2},"datasetVersion":"2026-08-31T19:17:28.585Z"}