You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在Next.js客户端应用中处理Gemini API的429请求过多错误

问题:Gemini 2.5 Pro免费层配额耗尽触发429错误,重试机制无效

在Next.js客户端应用中使用@google/genai包生成面试题时,反复出现429 Too Many Requests错误。已经实现了指数退避重试机制(5秒、10秒、20秒),但三次尝试后依然失败。错误信息显示已超出Gemini 2.5 Pro免费层的所有配额限制,包括每日令牌数、每日请求数、每分钟请求数和每分钟令牌数。

相关代码

GeminiAIModal.js

import { GoogleGenAI } from '@google/genai';

const apiKey = process.env.NEXT_PUBLIC_GEMINI_API_KEY;
if (!apiKey) {
    throw new Error('NEXT_PUBLIC_GEMINI_API_KEY is not set in environment variables');
}

const ai = new GoogleGenAI({ apiKey });
const config = {
    thinkingConfig: {
        thinkingBudget: -1, // Unlimited thinking budget as per your UI
    },
    responseMimeType: 'application/json', // Structured output
};
const model = 'gemini-2.5-pro';

async function withRetry(fn, maxRetries = 3, baseDelay = 5000) {
    for (let attempt = 1; attempt <= maxRetries; attempt++) {
        try {
            return await fn();
        } catch (error) {
            if (error.message.includes('429') && attempt < maxRetries) {
                const delay = baseDelay * Math.pow(2, attempt - 1);
                console.log(`429 error, retrying after ${delay}ms (attempt ${attempt}/${maxRetries})`);
                await new Promise(resolve => setTimeout(resolve, delay));
                continue;
            }
            throw error;
        }
    }
}

export async function generateInterviewQuestions(prompt) {
    try {
        const result = await withRetry(async () => {
            const response = await ai.models.generateContentStream({
                model,
                config,
                contents: [
                    {
                        role: 'user',
                        parts: [{ text: prompt }],
                    },
                ],
            });
            let fullText = '';
            for await (const chunk of response) {
                fullText += chunk.text;
            }
            return fullText;
        });
        return JSON.parse(result);
    } catch (error) {
        console.error('Error in generateInterviewQuestions:', error);
        throw new Error('Failed to generate interview questions from Gemini API');
    }
}

AddCardWithDialog.jsx

'use client';

import React, { useState } from 'react';
import { Dialog } from '@headlessui/react';
import { XMarkIcon } from '@heroicons/react/24/solid';
import { generateInterviewQuestions } from '../../utils/GeminiAIModal';

function AddCardWithDialog({ title, description }) {
    const [isOpen, setIsOpen] = useState(false);
    const [form, setForm] = useState({
        role: '',
        description: '',
        experience: '',
    });
    const [isLoading, setIsLoading] = useState(false);
    const [error, setError] = useState(null);

    const handleStartInterview = async (e) => {
        e.preventDefault();
        setIsLoading(true);
        setError(null);

        const contents = `job position: ${form.role},
            job description: ${form.description},
            no of year experience: ${form.experience},
            depends of those info give me 5 interview qs in json format
            give qs and answers in json format`;

        try {
            const result = await generateInterviewQuestions(contents);
            console.log('Interview Questions:', result);
            setIsOpen(false);
        } catch (err) {
            console.error('Error generating questions:', err);
            setError('Failed to generate interview questions. Please try again.');
        } finally {
            setIsLoading(false);
        }
    };

    const handleChange = (e) => {
        setForm({ ...form, [e.target.name]: e.target.value });
    };

    return (
        <>
            <div
                onClick={() => setIsOpen(true)}
                className='p-10 border rounded-2xl bg-secondary hover:scale-105 hover:shadow-lg cursor-pointer transition-all'
            >
                <h2 className='text-lg text-center font-semibold text-gray-800'>{title}</h2>
            </div>

            <Dialog open={isOpen} onClose={() => setIsOpen(false)} className='relative z-50'>
                <div className='fixed inset-0 bg-black/30 backdrop-blur-sm' />

                <div className='fixed inset-0 flex items-center justify-center p-4'>
                    <Dialog.Panel className='relative w-full max-w-lg rounded-2xl bg-white p-6 shadow-xl'>
                        <button
                            onClick={() => setIsOpen(false)}
                            className='absolute top-4 right-4 text-gray-500 hover:text-red-600 transition-colors'
                        >
                            <XMarkIcon className='w-6 h-6' />
                        </button>

                        <Dialog.Title className='text-2xl font-bold text-gray-900 mb-1'>
                            Tell us more about Job you are interviewing
                        </Dialog.Title>
                        <Dialog.Description className='text-gray-500 mb-6'>
                            Add Details about job position, your skills and year of experience
                        </Dialog.Description>

                        <div className='space-y-4'>
                            <div>
                                <label className='block text-sm font-medium mb-1 text-gray-700'>
                                    Job Position / Role name
                                </label>
                                <input
                                    type='text'
                                    name='role'
                                    value={form.role}
                                    onChange={handleChange}
                                    className='w-full border rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500'
                                />
                            </div>

                            <div>
                                <label className='block text-sm font-medium mb-1 text-gray-700'>
                                    Job Description / Tech Stack in Short
                                </label>
                                <textarea
                                    name='description'
                                    rows={3}
                                    value={form.description}
                                    onChange={handleChange}
                                    className='w-full border rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none'
                                />
                            </div>

                            <div>
                                <label className='block text-sm font-medium mb-1 text-gray-700'>
                                    No of Year Experience
                                </label>
                                <input
                                    type='number'
                                    name='experience'
                                    value={form.experience}
                                    onChange={handleChange}
                                    className='w-full border rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500'
                                />
                            </div>
                        </div>

                        {error && (
                            <p className='mt-4 text-red-600 text-sm'>{error}</p>
                        )}

                        <div className='mt-6 flex justify-end space-x-3'>
                            <button
                                onClick={() => setIsOpen(false)}
                                className='px-4 py-2 rounded-md bg-gray-200 text-gray-600 hover:bg-gray-300'
                                disabled={isLoading}
                            >
                                Cancel
                            </button>
                            <button
                                onClick={handleStartInterview}
                                className='px-4 py-2 rounded-md bg-blue-600 text-white hover:bg-blue-700 disabled:bg-blue-400'
                                disabled={isLoading}
                            >
                                {isLoading ? 'Generating...' : 'Start Interview'}
                            </button>
                        </div>
                    </Dialog.Panel>
                </div>
            </Dialog>
        </>
    );
}

export default AddCardWithDialog;

错误日志

<project-path>\utils\GeminiAIModal.js:24 429 error, retrying after 5000ms (attempt 1/3)
<project-path>\utils\GeminiAIModal.js:36
POST https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:streamGenerateContent?alt=sse 429 (Too Many Requests)
[...stack trace...]
<project-path>\utils\GeminiAIModal.js:24 429 error, retrying after 10000ms (attempt 2/3)
[...similar 429 error...]
<project-path>\utils\GeminiAIModal.js:54  Error in generateInterviewQuestions: ClientError: got status: 429 . {"error":{"message":"{\n  \"error\": {\n    \"code\": 429,\n    \"message\": \"You exceeded your current quota, please check your plan and billing details. For more information on this error, head to: https://ai.google.dev/gemini-api/docs/rate-limits.\",\n    \"status\": \"RESOURCE_EXHAUSTED\",\n    \"details\": [\n      {\n        \"@type\": \"type.googleapis.com/google.rpc.QuotaFailure\",\n        \"violations\": [\n          {\n            \"quotaMetric\": \"generativelanguage.googleapis.com/generate_content_free_tier_input_token_count\",\n            \"quotaId\": \"GenerateContentInputTokensPerModelPerDay-FreeTier\",\n            \"quotaDimensions\": {\n              \"location\": \"global\",\n              \"model\": \"gemini-2.5-pro\"\n            }\n          },\n          {\n            \"quotaMetric\": \"generativelanguage.googleapis.com/generate_content_free_tier_requests\",\n            \"quotaId\": \"GenerateRequestsPerDayPerProjectPerModel-FreeTier\",\n            \"quotaDimensions\": {\n              \"model\": \"gemini-2.5-pro\",\n              \"location\": \"global\"\n            }\n          },\n          {\n            \"quotaMetric\": \"generativelanguage.googleapis.com/generate_content_free_tier_requests\",\n            \"quotaId\": \"GenerateRequestsPerMinutePerProjectPerModel-FreeTier\",\n            \"quotaDimensions\": {\n              \"model\": \"gemini-2.5-pro\",\n              \"location\": \"global\"\n            }\n          },\n          {\n            \"quotaMetric\": \"generativelanguage.googleapis.com/generate_content_free_tier_input_token_count\",\n            \"quotaId\": \"GenerateContentInputTokensPerModelPerMinute-FreeTier\",\n            \"quotaDimensions\": {\n              \"location\": \"global\",\n              \"model\": \"gemini-2.5-pro\"\n            }\n          }\n        ]\n      },\n      {\n        \"@type\": \"type.googleapis.com/google.rpc.Help\",\n        \"links\": [\n          {\n            \"description\": \"Learn more about Gemini API quotas\",\n            \"url\": \"https://ai.google.dev/gemini-api/docs/rate-limits\"\n          }\n        ]\n      },\n      {\n        \"@type\": \"type.googleapis.com/google.rpc.RetryInfo\",\n        \"retryDelay\": \"49s\"\n      }\n    ]\n  }\n}\n","code":429,"status":""}}
    at throwErrorIfNotOK (<project-path>\node_modules\@google\genai\dist\web\index.mjs:11201:1)
    [...stack trace...]
<project-path>\components\ui\AddCardWithDialog.jsx:34  Error generating questions: Error: Failed to generate interview questions from Gemini API
    at generateInterviewQuestions (<project-path>\utils\GeminiAIModal.js:55:15)
    [...stack trace...]
解决方案

1. 处理配额耗尽问题

  • 免费层配额每日重置,测试场景可等次日再使用;正式使用直接升级付费套餐,提升配额上限。

2. 优化重试逻辑,遵循API返回的重试延迟

当前固定指数退避的间隔不符合API要求,应优先使用错误返回的retryDelay值,避免在配额未恢复时反复请求。修改withRetry函数:

async function withRetry(fn, maxRetries = 3) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      if (error.message.includes('429') && attempt < maxRetries) {
        let delay = 5000 * Math.pow(2, attempt - 1);
        // 解析错误中的官方重试延迟
        if (error.response?.data?.error?.details) {
          const retryInfo = error.response.data.error.details.find(item => item['@type'] === 'type.googleapis.com/google.rpc.RetryInfo');
          if (retryInfo?.retryDelay) {
            const seconds = parseInt(retryInfo.retryDelay.replace('s', ''), 10);
            delay = seconds * 1000;
          }
        }
        console.log(`429 error, retrying after ${delay}ms (attempt ${attempt}/${maxRetries})`);
        await new Promise(resolve => setTimeout(resolve, delay));
        continue;
      }
      throw error;
    }
  }
}

3. 减少请求和令牌消耗

  • 客户端限流:给handleStartInterview加防抖逻辑,限制用户1分钟内只能发起一次请求,避免重复点击超额消耗配额。
  • 精简提示词:简化提示内容,减少输入令牌数,示例:
const contents = `根据以下信息生成5道面试题及答案,返回JSON格式:
职位:${form.role}
工作描述:${form.description}
经验年限:${form.experience}年`;
  • 迁移到服务端调用:把Gemini API调用移到Next.js的API路由中,避免在客户端暴露API密钥(当前NEXT_PUBLIC_GEMINI_API_KEY会导致密钥前端可见,存在被盗用风险),同时在服务端基于IP或用户会话做更严格的请求限制。

4. 切换到低配额限制的模型

暂时不想付费的话,可换成Gemini 1.5 Flash免费层,它的配额更宽松,适合测试场景,只需修改模型名称:

const model = 'gemini-1.5-flash';

内容的提问来源于stack exchange,提问作者Mohamed Hedi ben salah

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.12 21:13:08