Spring AI調用sglang模型返回HTTP 400分析處理
一、問題描述
環境
- java21
- springboot: 3.5.5
- spring-ai: 1.0.1
問題描述
Spring AI調用公司部署的sglang大模型返回錯誤HTTP 400 - {"object":"error","message":[{'type': 'missing', 'loc': ('body',), 'msg': 'Field required', 'input': None}]","type":"Bad Request","param":null,"code":400}
,但調用公網模型沒問題,使用postman調用內網模型也沒問題。
二、分析解決
使用wireshark捕包對比Spring AI發出的請求和postman請求差異,發現Spring AI的請求多了請求頭Transfer-Encoding: chunked
,postman加上此請求頭后也報了同樣的錯誤,猜測是公司部署的sglang不支持分塊傳輸。
觀察異常堆棧,有一個exchange(DefaultRestClient.java:540)
,看名字應該是發送請求的入口,從這里打斷點調試。
- 定位到583行的
clientRequest.execute()
,繼續追蹤,發現底層調用的是jdk提供的HttpClientImpl
。 - 這個客戶端使用了大量的異步操作,先定位到
Exchange#responseAsyncImpl0
,然后定位到Http1Request#headers
,可見由requestPublisher#contentLength
決定是否為流式請求,當值為-1時添加請求頭Transfer-Encoding: chunked
。而且在JdkClientHttpRequest#buildRequest
方法中,自動排除了connection、content-length、expect、host、upgrade幾個請求頭。 - 向前追蹤,requestPublisher構建于
JdkClientHttpRequest#bodyPublisher
,當請求頭中存在contentLength時,才會構建包含contentLength的requestPublisher。這里推測當請求體為固定大小時,會添加contentLength請求頭。 - 回到
DefaultRestClient#createRequest
,這里有兩種客戶端構建方式,一種是存在攔截器時通過InterceptionClientHttpRequestFactory
構建,另一種是通過默認的JdkClientHttpRequestFactory
。 JdkClientHttpRequest
繼承自AbstractStreamingClientHttpRequest
,請求體使用流式傳輸。InterceptionClientHttpRequestFactory
繼承自AbstractBufferingClientHttpRequest
,請求體會完全緩存,在executeInternal
方法中會自動添加Content-Length
請求頭。- 給
DefaultRestClient
構造方法打斷點,向上一步步找到DefaultRestClientBuilder
、RestClientAutoConfiguration#restClientBuilder
、RestClientBuilderConfigurer
、RestClientAutoConfiguration#restClientBuilderConfigurer
,發現注入參數ObjectProvider<RestClientCustomizer> customizerProvider
,于是自定義Bean如下。import org.springframework.boot.web.client.RestClientCustomizer; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestClient;@Configuration public class RestClientConfig implements RestClientCustomizer {@Overridepublic void customize(RestClient.Builder restClientBuilder) {restClientBuilder.requestInterceptor((request, body, execution) -> execution.execute(request, body));} }
- 此時請求頭中已經添加了
Content-Length
,但還是報錯。
再次使用wireshark捕包,發現請求中多了請求頭Connection: Upgrade
和Upgrade: h2c
來協商升級到HTTP2,推測應該是sglang服務端不支持。定位到ExchangeImpl#get
,這里會判斷需要使用的HTTP版本,進一步定位到MultiExchange#version
,發現會依次獲取request.version、client.version直到取到非空值。request中的version追蹤后發現是空值且無法定制,于是嘗試修改client.version。
- client為
HttpClientImpl
類,打斷點追蹤,由JdkHttpClientBuilder#build
構建,并支持通過customizer
進行自定義。 - 繼續向上追蹤,找到
JdkClientHttpRequestFacotryBuilder#createClientHttpRequestFactory
、AbstractClientHttpRequestFactoryBuilder#build
,這里有一組customizers通過LambdaSafe#callbacks
對JdkClientHttpReuqestFactory
進行自定義。 - 給
AbstractClientHttpRequestFactoryBuilder
構造方法打打斷點,向上追蹤, 找到HttpClientAutoConfiguration#clientHttpRequestFactoryBuilder
,發現注入參數ObjectProvider<ClientHttpRequestFactoryBuilzer<?>> clientHttpRequestFactoryBuilderCustomizers
,于是自定義Bean如下。import org.springframework.boot.autoconfigure.http.client.ClientHttpRequestFactoryBuilderCustomizer; import org.springframework.boot.http.client.JdkClientHttpRequestFactoryBuilder; import org.springframework.context.annotation.Configuration;import java.net.http.HttpClient;@Configuration public class HttpClientConfig implements ClientHttpRequestFactoryBuilderCustomizer<JdkClientHttpRequestFactoryBuilder> {@Overridepublic JdkClientHttpRequestFactoryBuilder customize(JdkClientHttpRequestFactoryBuilder builder) {return builder.withHttpClientCustomizer(httpClientBuilder -> httpClientBuilder.version(HttpClient.Version.HTTP_1_1));} }
再測試已無HTTP2協商相關請求頭,可以正常調用模型。但是還有一點要注意,當classpath中包含其他http客戶端時,可能會采用其他ClientHttpRequestFactoryBuilder
,詳見ClientHttpRequestFactoryBuilder#detect
,這時可能需要修改HttpClientConfig
的泛型類型并重寫customize
方法。