HttpClientStreamableHttpTransport: reconnect() called before status code check causes duplicate connections on 405
Dieses Issue hat noch niemand übernommen.
Bewertung
- Schwierigkeit
- 3/5
- Geschätzter Aufwand
- 1-2 Tage
- Anfängerfreundlichkeit
- 75/100
- Issue-Typ
- Bug
- Klarheit
- Klar beschrieben
- Aktivitätsstatus
- Ruhig
- Bereich
- backend-api-design, networking
Rechercherichtung
Beginnen Sie mit den Response-Handlern in mcp-core/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java und mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/client/transport/WebClientStreamableHttpTransport.java. Prüfen Sie die Statuscode-Zweige rund um markInitialized() und reconnect(). Als erledigt gilt die Aufgabe, wenn Nicht-2xx-Antworten, insbesondere 405, keinen GET auslösen, während erfolgreiche Antworten die Sitzungsinitialisierung beibehalten und der Fallback auf SSE doppelte Sitzungen vermeidet.
Vom Indexierungsmodell aus dem Issue-Text verfasst.
Beschreibung
Description
When connecting to an MCP server that doesn't support Streamable HTTP transport (returns 405 Method Not Allowed), HttpClientStreamableHttpTransport still calls markInitialized() and reconnect(), which triggers an unnecessary GET request.
This causes issues when using transport fallback (Streamable HTTP → SSE), as duplicate SSE sessions are created on the upstream server.
Steps to Reproduce
- Set up an MCP proxy/server that only supports SSE transport (not Streamable HTTP)
- Return
405 Method Not Allowedfor POST requests to the Streamable HTTP endpoint - Use a client that tries both transports in parallel (Streamable HTTP and SSE)
- Observe that the Streamable HTTP transport sends an additional GET request after receiving 405
Expected Behavior
When the server returns an error status (like 405), reconnect() should NOT be called. The transport should simply fail and let the fallback mechanism use SSE transport.
Actual Behavior
markInitialized() and reconnect() are called BEFORE checking the status code, so even error responses trigger a GET request:
})).flatMap(responseEvent -> {
if (transportSession.markInitialized(
responseEvent.responseInfo().headers().firstValue("mcp-session-id").orElseGet(() -> null))) {
// Once we have a session, we try to open an async stream for
// the server to send notifications and requests out-of-band.
reconnect(null).contextWrite(deliveredSink.contextView()).subscribe();
}
String sessionRepresentation = sessionIdOrPlaceholder(transportSession);
int statusCode = responseEvent.responseInfo().statusCode();
if (statusCode >= 200 && statusCode < 300) {
Also Affected: WebClientStreamableHttpTransport
The same bug exists in WebClientStreamableHttpTransport (lines 312-324):
.exchangeToFlux(response -> {
if (transportSession
.markInitialized(response.headers().asHttpHeaders().getFirst(HttpHeaders.MCP_SESSION_ID))) {
// Once we have a session, we try to open an async stream for
// the server to send notifications and requests out-of-band.
reconnect(null).contextWrite(sink.contextView()).subscribe();
}
String sessionRepresentation = sessionIdOrPlaceholder(transportSession);
// The spec mentions only ACCEPTED, but the existing SDKs can return
// 200 OK for notifications
if (response.statusCode().is2xxSuccessful()) {
Both implementations have the identical issue: markInitialized() and reconnect() are called before checking the HTTP status code.
Note: Why this is still a bug even though reconnect() handles 405
I noticed that reconnect() already has 405 handling logic:
} else if (statusCode == METHOD_NOT_ALLOWED) {
logger.debug("The server does not support SSE streams, using request-response mode.");
return Flux.empty();
}
However, this doesn't prevent the issue because:
- The GET request is already sent - By the time
reconnect()handles the 405 response, the HTTP request has already been made - Upstream creates a new session - When the GET request reaches the upstream server, it creates a new SSE session before returning any response
- Duplicate sessions exist - Now there are two sessions on upstream: one from SSE transport's GET, another from Streamable HTTP's
reconnect()GET
The flow:
1. SSE transport: GET /sse → upstream creates session #1
2. Streamable HTTP: POST /mcp → 405
3. markInitialized(null) → returns true
4. reconnect() → GET /mcp sent → upstream creates session #2 (the damage is done!)
5. reconnect() receives response, handles 405 gracefully (but too late)
6. SSE transport: POST to session #1 → success
7. Result: session #2 is orphaned, potential timeout issues
The fix should prevent the GET request from being sent in the first place, not just handle the response gracefully.
Suggested Fix
Move markInitialized() and reconnect() inside the success status code check.
For HttpClientStreamableHttpTransport:
})).flatMap(responseEvent -> {
String sessionRepresentation = sessionIdOrPlaceholder(transportSession);
int statusCode = responseEvent.responseInfo().statusCode();
if (statusCode >= 200 && statusCode < 300) {
// Only initialize session and open async stream for successful responses
if (transportSession.markInitialized(
responseEvent.responseInfo().headers().firstValue("mcp-session-id").orElseGet(() -> null))) {
// Once we have a session, we try to open an async stream for
// the server to send notifications and requests out-of-band.
reconnect(null).contextWrite(deliveredSink.contextView()).subscribe();
}
For WebClientStreamableHttpTransport:
.exchangeToFlux(response -> {
String sessionRepresentation = sessionIdOrPlaceholder(transportSession);
if (response.statusCode().is2xxSuccessful()) {
// Only initialize session and open async stream for successful responses
if (transportSession
.markInitialized(response.headers().asHttpHeaders().getFirst(HttpHeaders.MCP_SESSION_ID))) {
reconnect(null).contextWrite(sink.contextView()).subscribe();
}
Impact
This bug affects any scenario where:
- A proxy or server doesn't support Streamable HTTP (returns 405)
- Client implements transport fallback (try Streamable HTTP first, fall back to SSE)
- Results in duplicate upstream sessions and potential timeout issues
Environment
- MCP Java SDK version: latest (this code has been present since the initial Streamable HTTP implementation in commit c711f83)
- Vorherrschende Sprache
- Java
- Sterne
- 3.7k
- Forks
- 1.1k
- Ø Merge
- 1 T. 15 Std.
- Gemergte PRs (30 T.)
- 9
Beitragsleitfaden
Erste Schritte
- Lesen Sie das ganze Issue und danach den Beitragsleitfaden des Projekts.
- Schreiben Sie ins Issue, dass Sie es übernehmen — das erspart doppelte Arbeit.
- Forken Sie das Repository und arbeiten Sie in einem Branch.
- Öffnen Sie einen Pull Request, der die Issue-Nummer nennt.
Mehr aus modelcontextprotocol/java-sdk
-
area/transport bug P2
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 88/100
modelcontextprotocol/java-sdk#1136 ·
-
area/client bug P2
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 84/100
modelcontextprotocol/java-sdk#1124 · 1 Kommentar ·
-
ServerCapabilities.logging is added unconditionally, overriding the caller's explicit capabilities Offenbug P2 ready for work
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 68/100
modelcontextprotocol/java-sdk#1086 · 1 Kommentar ·
-
enhancement good first issue P3
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 82/100
modelcontextprotocol/java-sdk#1067 ·
-
bug P2 ready for work
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 74/100
modelcontextprotocol/java-sdk#898 · 1 Kommentar ·
Alle Issues in modelcontextprotocol/java-sdk
Ähnliche Issues
-
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 75/100
elastic/gradle-plugins#157 ·
-
enhancement Tools
Schwierigkeit 1/5 Unter einer Stunde Anfängerfreundlichkeit 75/100
-
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 70/100
apache/rocketmq-dashboard#5008 ·
-
bug
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 75/100
-
DETECT_PARAMETER_NAMES=false silently disables @ConstructorProperties-based Creator detection too Offen
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 70/100
FasterXML/jackson-databind#6229 ·