StackTraces
Spring AI已验证

Spring AI 调 OpenAI 报 429 Too Many Requests

@debug_master32 天前更新阅读 6 min00

429 不是「重试几次就好」这么简单。同一句 429 背后有 rate_limit、insufficient_quota 等不同 error.code,前者才该重试,后者重试没用。而且 Spring AI 默认对 4xx 不重试,直接抛 NonTransientAiException,很多人看到「重试没生效」其实是配置问题。

Issue & Env(案发现场与环境)

  • 业务场景:并发或高频调 OpenAI,请求弹 429,被 Spring AI 包成异常抛出,没按预期重试。
  • 环境:Spring AI 1.x / 2.0,OpenAI 官方 API。重试相关配置在 spring.ai.retry 前缀下,默认 on-client-errors=false。

StackTrace(堆栈日志)

底层 HTTP 层抛的是 TooManyRequests,对应的 OpenAI 错误响应原文长这样:

{
    "error": {
        "message": "Rate limit reached for requests",
        "type": "rate_limit_error",
        "param": null,
        "code": "rate_limit_exceeded"
    }
}

Spring AI 默认不重试 4xx,把它包成不可重试异常往外抛,报错原文:

org.springframework.ai.retry.NonTransientAiException: 429 - {
    "error": {
        "message": "Rate limit reached for requests",
        "type": "rate_limit_error"
    }
}

完整堆栈,从 Spring AI 的重试判断一路回到业务调用:

org.springframework.ai.retry.NonTransientAiException: 429 - {
    "error": {
        "message": "Rate limit reached for requests",
        "type": "rate_limit_error"
    }
}
    at org.springframework.ai.retry.RetryUtils.handleResponseError(RetryUtils.java:163)
    at org.springframework.ai.chat.model.ChatModel.call(ChatModel.java:204)
    at org.springframework.ai.chat.client.DefaultChatClient$DefaultChatClientRequestSpec$InternalChatClientRequest.call(DefaultChatClient.java:686)
    at com.example.demo.chat.OpenAiService.summarize(OpenAiService.java:36)
    at com.example.demo.chat.ChatController.summarize(ChatController.java:23)
    at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205)
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117)
Caused by: org.springframework.web.client.HttpClientErrorException$TooManyRequests: 429 Too Many Requests
    at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:123)
    at org.springframework.web.client.RestClient$DefaultResponseSpec$DefaultBodySpec.evaluate(RestClient.java:1187)

Reproduce(复现代码)

最小依赖:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>

配好 key 后,用一个循环把短时间内的请求量打上去:

for (int i = 0; i < 100; i++) {
    chatClient.prompt().user("hi").call().content();
}

一旦撞上账户的 RPM(每分钟请求数)或 TPM(每分钟 token 数)限制,就抛 429。报错是否重试,取决于 spring.ai.retry 的配置。

Root Cause(根因分析)

两件事容易混,得拆开看。

第一件,429 背后的原因不止一种。OpenAI 返回 429 时,error.code 可能是 rate_limit(临时限流),也可能是 credit_balance_exhausted、organization_spend_limit_exceeded 这类额度、余额、限额耗尽。前者等一会儿再试能过,后者重试多少次都不会恢复访问——官方文档明确说,billing 或 quota 类错误重试无用,得去充值或调整 limit。

第二件,Spring AI 的默认重试策略。spring.ai.retry.on-client-errors 默认 false,意思是 4xx 客户端错误一律不重试,直接抛 NonTransientAiException。429 属于 4xx,所以默认情况下你根本看不到重试。想让 429 走重试,要么开 on-client-errors=true,要么精准一点,把 429 加进 spring.ai.retry.on-http-codes。

还有一个坑:有些上游(尤其 Azure)的 429 会带一个超大 Retry-After 头,比如 24 小时。客户端要是盲从,会把线程挂一整天(issue #2078)。所以重试最好带退避上限,别无条件尊重 Retry-After。

The Fix(解决方案)

第一步:看 error.code 分流

拿到 429 先看 error.code,别急着加重试。

  • rate_limit:临时限流,降频或重试。
  • credit_balance_exhausted、organization_spend_limit_exceeded 等:额度类,去充值或调 limit,重试没用。

第二步:临时限流才配重试

只对 429 开重试,其他 4xx 保持不重试:

spring:
  ai:
    retry:
      max-attempts: 5
      backoff:
        initial-interval: 1s
        multiplier: 2
        max-interval: 10s
      on-http-codes:
        - 429

on-http-codes 只放你确定要重试的状态码;exclude-on-http-codes 放你明确不想重试的码。两者都别开太宽,避免把额度类 429 也卷进重试。

第三步:控并发、拆 key

重试只是兜底,根子上得降并发、控制频率,或给不同服务分配独立的 key,别多个服务共用一个 key 顶同一个限额。

References(参考来源)

最后更新于 2026-09-09

这篇帮到你了吗?

刚解决了一个棘手的报错?花两分钟记录下来,帮助下一个遇到同样问题的开发者。

贡献一条解法