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

這篇幫到你了嗎?

剛解決了一個棘手的報錯?花兩分鐘記錄下來,幫助下一個遇到同樣問題的開發者。

貢獻一條解法