在开发环境中,OAuth 回调端点 /api/oauth/callback 被调用了两次,导致第二次调用时出现错误:
{
"detail": "OAuth 登录失败: 无效的令牌响应格式: {'error': 'invalid_grant', 'error_description': '授权码已被使用'}"
}
在开发环境中,React 18 的 StrictMode 会故意渲染组件两次,以帮助发现副作用问题。这导致 useEffect 被执行两次。
// main.tsx
<StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</StrictMode>
最初尝试使用 useState 来防止重复调用:
const [isProcessing, setIsProcessing] = useState(false);
useEffect(() => {
if (isProcessing) {
return; // 试图阻止重复调用
}
setIsProcessing(true);
// ... 处理逻辑
}, [searchParams, navigate, setAuth, isProcessing]); // ❌ 问题在这里!
问题:isProcessing 被添加到依赖数组中,当它从 false 变为 true 时,会触发 useEffect 重新运行,从而绕过了防重复调用的检查。
使用 useRef 而不是 useState 来跟踪处理状态:
const isProcessingRef = useRef(false);
useEffect(() => {
// 防止 React StrictMode 导致的重复调用
if (isProcessingRef.current) {
console.log('Already processing, skipping duplicate call');
return;
}
const processCallback = async () => {
isProcessingRef.current = true;
console.log('Starting OAuth callback processing...');
try {
// ... OAuth 处理逻辑
} catch (err) {
// ... 错误处理
}
};
processCallback();
}, [searchParams, navigate, setAuth]); // ✅ 不包含 isProcessingRef
ref.current 不会触发组件重渲染useRef 返回的对象在组件生命周期内保持不变,不需要添加到依赖数组ref 的值也会保持1. 第一次渲染
- isProcessingRef.current = false
- 检查通过,开始处理
- isProcessingRef.current = true
- 调用 /api/oauth/callback (第一次)
2. StrictMode 触发第二次渲染
- isProcessingRef.current = true (保持不变)
- 检查失败,跳过处理
- 不调用 /api/oauth/callback
结果:只调用一次 ✅
1. 第一次渲染
- isProcessing = false
- 检查通过,开始处理
- setIsProcessing(true)
- 调用 /api/oauth/callback (第一次)
2. isProcessing 变化触发 useEffect 重新运行
- isProcessing = true
- 检查失败,跳过... 等等!
3. StrictMode 触发第二次渲染
- isProcessing = false (重置)
- 检查通过,开始处理
- 调用 /api/oauth/callback (第二次) ❌
结果:调用两次 ❌
cd web && yarn start lq_labelhttp://localhost:4200/login查看浏览器控制台:
OAuth callback URL parameters: {code: "...", state: "..."}
Starting OAuth callback processing...
Code: NhbmBfgF4XMJcsAlykbs2GOAec_0ylqu9JcAKvCcffA
State: O_Fo8TTBRAHdTxw4GTZD_ZmvAz0fD1KnQIo4TE337fc
Exchanging code for tokens...
Already processing, skipping duplicate call ← 第二次调用被阻止
Token exchange successful
查看后端日志:
INFO: 127.0.0.1:xxxxx - "GET /api/oauth/callback?code=...&state=... HTTP/1.1" 200 OK
只有一次请求! ✅
在生产环境中,StrictMode 会被禁用,所以不会有重复调用问题。但保留这个修复不会有任何负面影响。
// main.tsx
// ❌ 不推荐:失去 StrictMode 的开发时检查
<BrowserRouter>
<App />
</BrowserRouter>
缺点:失去 React 18 的开发时检查和警告。
useEffect(() => {
const controller = new AbortController();
const processCallback = async () => {
try {
const response = await fetch('/api/oauth/callback', {
signal: controller.signal
});
// ...
} catch (err) {
if (err.name === 'AbortError') return;
// ...
}
};
processCallback();
return () => controller.abort();
}, [searchParams]);
缺点:更复杂,且第一次请求仍然会被发送(只是被取消)。
useRef 跟踪处理状态这个修复简单、有效,且不影响生产环境的行为。
修复日期: 2024-01-22
文件: web/apps/lq_label/src/components/oauth-callback/oauth-callback.tsx
状态: ✅ 已修复