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 { @Override public 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> { @Override public JdkClientHttpRequestFactoryBuilder customize(JdkClientHttpRequestFactoryBuilder builder) { return builder.withHttpClientCustomizer(httpClientBuilder -> httpClientBuilder.version(HttpClient.Version.HTTP_1_1)); } }
再测试已无HTTP2协商相关请求头,可以正常调用模型。