From 9fd5843dda8a4802b7e6c29b92df7d74ea5558a1 Mon Sep 17 00:00:00 2001 From: matevip Date: Mon, 11 May 2026 11:22:23 +0800 Subject: [PATCH] fix(llm): pin OpenAI-compatible HTTP client to HTTP/1.1 (#89) --- .../vip/mate/agent/AgentGraphBuilder.java | 6 +++++ .../llm/service/ModelDiscoveryService.java | 23 +++++++++++++++++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/mateclaw-server/src/main/java/vip/mate/agent/AgentGraphBuilder.java b/mateclaw-server/src/main/java/vip/mate/agent/AgentGraphBuilder.java index 9c5d24b2..053544a2 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/AgentGraphBuilder.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/AgentGraphBuilder.java @@ -1611,6 +1611,7 @@ public class AgentGraphBuilder { private RestClient.Builder applyHttpTimeouts(RestClient.Builder builder, Integer readTimeoutOverride) { HttpClient httpClient = HttpClient.newBuilder() .connectTimeout(vip.mate.llm.chatmodel.HttpTimeouts.CONNECT_TIMEOUT) + .version(HttpClient.Version.HTTP_1_1) .build(); JdkClientHttpRequestFactory rf = new JdkClientHttpRequestFactory(httpClient); rf.setReadTimeout(vip.mate.llm.chatmodel.HttpTimeouts.resolveReadTimeout(readTimeoutOverride)); @@ -1641,8 +1642,13 @@ public class AgentGraphBuilder { * {@link #applyHttpTimeouts(RestClient.Builder, Integer)}. */ private WebClient.Builder applyHttpTimeoutsToWebClient(WebClient.Builder builder, Integer readTimeoutOverride) { + // Pin HTTP/1.1: many self-hosted OpenAI-compatible servers (vLLM, lmstudio, + // llama.cpp, ollama — all uvicorn/ASGI based) only speak HTTP/1.1 over + // cleartext and slam the socket on the JDK client's default H2C upgrade + // probe, surfacing as "header parser received no bytes" with no body sent. HttpClient httpClient = HttpClient.newBuilder() .connectTimeout(vip.mate.llm.chatmodel.HttpTimeouts.CONNECT_TIMEOUT) + .version(HttpClient.Version.HTTP_1_1) .build(); org.springframework.http.client.reactive.JdkClientHttpConnector connector = new org.springframework.http.client.reactive.JdkClientHttpConnector(httpClient); diff --git a/mateclaw-server/src/main/java/vip/mate/llm/service/ModelDiscoveryService.java b/mateclaw-server/src/main/java/vip/mate/llm/service/ModelDiscoveryService.java index 3319ae1d..c2921f7f 100644 --- a/mateclaw-server/src/main/java/vip/mate/llm/service/ModelDiscoveryService.java +++ b/mateclaw-server/src/main/java/vip/mate/llm/service/ModelDiscoveryService.java @@ -6,6 +6,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; +import org.springframework.http.client.JdkClientHttpRequestFactory; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import org.springframework.web.client.RestClient; @@ -13,6 +14,7 @@ import vip.mate.exception.MateClawException; import vip.mate.llm.model.*; import vip.mate.llm.oauth.OpenAIOAuthService; +import java.net.http.HttpClient; import java.time.Duration; import java.util.*; import java.util.concurrent.CompletableFuture; @@ -413,7 +415,7 @@ public class ModelDiscoveryService { } String apiKey = provider.getApiKey(); - RestClient client = RestClient.builder() + RestClient client = openAiCompatibleClientBuilder() .baseUrl(baseUrl) .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) .build(); @@ -623,7 +625,7 @@ public class ModelDiscoveryService { Map kwargs = modelProviderService.readProviderGenerateKwargs(provider); String completionsPath = resolveCompletionsPath(baseUrl, kwargs); - RestClient.RequestHeadersSpec spec = RestClient.builder() + RestClient.RequestHeadersSpec spec = openAiCompatibleClientBuilder() .baseUrl(baseUrl) .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .build() @@ -915,6 +917,23 @@ public class ModelDiscoveryService { return normalized; } + /** + * Build a RestClient.Builder pinned to HTTP/1.1 for self-hosted OpenAI-compatible + * servers. Java's HttpClient defaults to HTTP/2 and over cleartext attempts an + * H2C upgrade ({@code Upgrade: h2c, Connection: Upgrade, HTTP2-Settings: ...}). + * Uvicorn-based stacks (vLLM, lmstudio, llama.cpp, ollama) reject the upgrade + * by closing the socket mid-handshake — surfacing as either + * "header parser received no bytes" on the chat path or, more subtly, a + * 400 with body=None on the test path because the body never makes it past + * the upgrade negotiation. + */ + private RestClient.Builder openAiCompatibleClientBuilder() { + HttpClient httpClient = HttpClient.newBuilder() + .version(HttpClient.Version.HTTP_1_1) + .build(); + return RestClient.builder().requestFactory(new JdkClientHttpRequestFactory(httpClient)); + } + @SuppressWarnings("unchecked") private void applyCustomHeaders(RestClient.RequestHeadersSpec spec, Map kwargs) { if (kwargs == null) {