{"record":{"id":"9f8ca6135726d769","repo":"Wei-Shaw/sub2api","slug":"auth-dingtalk-callbackmissingtoken","errorCode":null,"errorMessage":"auth.dingtalk.callbackMissingToken","messagePattern":"auth\\.dingtalk\\.callbackMissingToken","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"frontend/src/views/auth/DingTalkCallbackView.vue","lineNumber":582,"sourceCode":"    states.includes('bind_login_required') ||\n    states.includes('bind_login') ||\n    states.includes('adopt_existing_user_by_email') ||\n    states.includes('existing_account_required') ||\n    states.includes('existing_account_binding_required')\n}\n\nasync function finalizeCompletion(completion: PendingOAuthExchangeResponse, redirect: string) {\n  if (getOAuthCompletionKind(completion) === 'bind') {\n    const bindRedirect = sanitizeRedirectPath(completion.redirect || '/profile')\n    clearPendingAuthSession()\n    clearAllAffiliateReferralCodes()\n    appStore.showSuccess(bindSuccessMessage)\n    await router.replace(bindRedirect)\n    return\n  }\n\n  if (!isOAuthLoginCompletion(completion)) {\n    throw new Error(t('auth.dingtalk.callbackMissingToken'))\n  }\n\n  persistOAuthTokenContext(completion)\n  await authStore.setToken(completion.access_token)\n  clearAllAffiliateReferralCodes()\n  appStore.showSuccess(t('auth.loginSuccess'))\n  await router.replace(redirect)\n}\n\nasync function finalizePendingAccountResponse(completion: DingTalkPendingActionResponse) {\n  applyAdoptionSuggestionState(completion)\n  const redirect = sanitizeRedirectPath(completion.redirect || redirectTo.value)\n\n  // step=email_completion: 用户无邮箱，需要跳到补邮箱页面\n  if (completion.step === 'email_completion' || (completion as Record<string, unknown>)['requires_email_completion'] === true) {\n    await router.replace('/auth/dingtalk/email-completion?redirect=' + encodeURIComponent(redirect))\n    return\n  }","sourceCodeStart":564,"sourceCodeEnd":600,"githubUrl":"https://github.com/Wei-Shaw/sub2api/blob/073e92d17178a1ccdb0a27017f572f10c9c7ab62/frontend/src/views/auth/DingTalkCallbackView.vue#L564-L600","documentation":"In frontend/src/views/auth/DingTalkCallbackView.vue:582, finalizeCompletion() processes a PendingOAuthExchangeResponse from the DingTalk OAuth exchange. After the 'bind' branch is excluded, it requires isOAuthLoginCompletion(completion) (i.e., the response carries an access_token). If the completion is neither bind nor login — no access_token — it throws the localized 'auth.dingtalk.callbackMissingToken'. This indicates the backend exchange endpoint returned an unexpected/incomplete completion shape.","triggerScenarios":"POST exchange of the DingTalk code/state returns 2xx but without access_token: backend session for the callback expired server-side; the exchange response was truncated or the API version returns token under a different field; the user's account is in a pending/intermediate state (e.g., needs account creation) and the frontend treated a non-final response as final.","commonSituations":"User refreshes the callback URL after the one-time exchange session was consumed; backend deployed with mismatched response schema; race where pending-session handoff completes between steps and completion kind is neither 'bind' nor login.","solutions":["Capture the actual exchange response payload in devtools and confirm which field is missing; if the token lives under a different key, fix the API mapping.","Make getOAuthCompletionKind/isOAuthLoginCompletion mutually exhaustive: any third shape should route to a pending-account or error flow rather than throw a raw error to the user.","Handle the double-submit/refresh case by making the exchange idempotent or redirecting the user to restart login cleanly.","Check backend logs for the same request to see whether it intended a pending response the frontend doesn't understand."],"exampleFix":"// before\nif (!isOAuthLoginCompletion(completion)) {\n  throw new Error(t('auth.dingtalk.callbackMissingToken'))\n}\n\n// after\nif (!isOAuthLoginCompletion(completion)) {\n  if (isPendingAccountResponse(completion)) {\n    await finalizePendingAccountResponse(completion)\n    return\n  }\n  throw new Error(t('auth.dingtalk.callbackMissingToken'))\n}","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"function isOAuthLoginCompletion(c: PendingOAuthExchangeResponse): c is PendingOAuthExchangeResponse & { access_token: string } {\n  return getOAuthCompletionKind(c) === 'login' && typeof (c as any).access_token === 'string' && (c as any).access_token.length > 0;\n}","tryCatchPattern":"try { await finalizeCompletion(completion, redirect); }\ncatch (e) {\n  if (e.message === t('auth.dingtalk.callbackMissingToken')) {\n    showError('Sign-in session expired — please log in again');\n    await router.replace('/login'); return;\n  }\n  throw e;\n}","preventionTips":["Make the completion-kind switch exhaustive: bind | login | pending | error, with an explicit default flow","Make the exchange endpoint idempotent to survive callback refresh/double-submit","Log the raw completion object server-side when returning a non-login shape"],"tags":["oauth","dingtalk","callback","session","frontend"],"backgroundTag":null,"analyzedSha":"073e92d17178a1ccdb0a27017f572f10c9c7ab62","analyzedAt":"2026-08-15T14:33:00.750Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}