StackTraces
Spring AI已验证

Spring AI WebFlux 报 block() not supported

@debug_master13 天前更新阅读 6 min00

WebFlux 的请求跑在 reactor-http-nio-* 事件循环线程上,这个线程不允许阻塞。chatClient...call().content() 底层会阻塞等整段响应,于是 Reactor 直接抛 IllegalStateException,而不是死等。WebFlux 里要流式:用 .stream().content() 返回 Flux<String>;确实需要阻塞时切到 Schedulers.boundedElastic()。

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

  • 业务场景:Spring WebFlux 项目里接 Spring AI。在 WebFlux controller、WebFilter 或 gateway filter 里直接调 chatClient.prompt()...call().content();或者拿到 chatModel.stream(prompt) 的 Flux,想收集成字符串,就在事件循环线程里 blockLast()。
  • 环境:Spring AI 1.1.x + Spring WebFlux(Netty)。线程名里带 reactor-http-nio-。

StackTrace(堆栈日志)

java.lang.IllegalStateException: block()/blockFirst()/blockLast() are blocking, which is not supported in thread reactor-http-nio-3
    at reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:83)
    at reactor.core.publisher.Mono.block(Mono.java:1758)

关键信息有两个:方法名(block 系列)和线程名(reactor-http-nio-*)。线程名对上了,基本就是这个问题。

Reproduce(复现代码)

WebFlux controller 里直接 call:

@GetMapping("/chat")
public Mono<String> chat(@RequestParam String q) {
    // 事件循环线程上执行,call() 会阻塞
    String answer = chatClient.prompt().user(q).call().content();
    return Mono.just(answer);
}

或者把流式结果在事件循环里收集:

@GetMapping("/chat/stream")
public Mono<String> stream(@RequestParam String q) {
    // 想"先收齐再返回",block 直接触发异常
    String all = chatClient.prompt().user(q).stream().content()
            .collectList()
            .block()
            .stream()
            .collect(Collectors.joining());
    return Mono.just(all);
}

Root Cause(根因分析)

事件循环线程禁止阻塞

Netty(WebFlux 默认容器)用少量线程处理所有请求,线程名形如 reactor-http-nio-N。Reactor 在这些线程上装了“禁止阻塞”的检查:一旦调用 block()、blockFirst()、blockLast(),直接抛 IllegalStateException。

抛异常是保护,不是 bug

为什么不干脆等下去?因为线程一阻塞,同一个事件循环上排队的其他请求全卡住,吞吐直接崩。Reactor 宁可当场报错,也不让一个慢调用拖垮整条事件循环。

call() 本质上就是阻塞

ChatClient 的 .call() 会等模型把整段回答生成完,再返回完整文本;.content() 只是取其中字符串。它内部最终会走到阻塞等待,所以在 WebFlux 请求线程上调用,等于主动阻塞事件循环。

call() 和 stream() 不能在同一次请求里混用

一次请求要么走 .call()(一次性拿到完整响应),要么走 .stream()(拿 Flux)。别在流式链路里再 call() 一把,也别指望同一个 response spec 两种都来一遍。

The Fix(解决方案)

1. WebFlux 里返回 Flux,别中途收集

@GetMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> stream(@RequestParam String q) {
    return chatClient.prompt().user(q).stream().content();
}

把 Flux<String> 直接交给框架,由它订阅、写出。中间不要 collectList(),也不要 block()。

2. 确实要阻塞就切线程池

写库、调同步 SDK 这类必须阻塞的操作,切到 Schedulers.boundedElastic():

@GetMapping("/chat")
public Mono<String> chat(@RequestParam String q) {
    return Mono.fromCallable(() -> chatClient.prompt().user(q).call().content())
            .subscribeOn(Schedulers.boundedElastic());
}

boundedElastic 是给阻塞任务准备的弹性线程池,不会占用事件循环。别用 Schedulers.parallel(),那是给 CPU 密集任务用的,同样不该塞阻塞调用。

3. 别替库擦屁股

如果调用栈里的 block 不是你自己写的,而是某个库内部干的,先把那个库换成响应式版本(比如用 WebClient 而不是 RestClient),比在外面套一层线程池更干净。

4. 排查办法

看异常里的线程名。带 reactor-http-nio- 的,就是事件循环;带 boundedElastic- 的,说明已经切过去了。

References(参考来源)

最后更新于 2026-09-28

这篇帮到你了吗?

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

贡献一条解法