Skip to content

Commit 2b9406d

Browse files
committed
Expose request URI in McpHttpClientAuthorizationErrorHandler
Breaking change Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
1 parent 6b71136 commit 2b9406d

8 files changed

Lines changed: 278 additions & 79 deletions

File tree

mcp-core/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import io.modelcontextprotocol.client.transport.ResponseSubscribers.ResponseEvent;
2525
import io.modelcontextprotocol.client.transport.customizer.McpAsyncHttpClientRequestCustomizer;
2626
import io.modelcontextprotocol.client.transport.customizer.McpHttpClientAuthorizationErrorHandler;
27+
import io.modelcontextprotocol.client.transport.customizer.McpHttpClientTransportAuthorizationErrorHandler;
2728
import io.modelcontextprotocol.client.transport.customizer.McpSyncHttpClientRequestCustomizer;
2829
import io.modelcontextprotocol.common.McpTransportContext;
2930
import io.modelcontextprotocol.json.McpJsonDefaults;
@@ -120,7 +121,7 @@ public class HttpClientStreamableHttpTransport implements McpClientTransport {
120121

121122
private final boolean openConnectionOnStartup;
122123

123-
private final McpHttpClientAuthorizationErrorHandler authorizationErrorHandler;
124+
private final McpHttpClientTransportAuthorizationErrorHandler authorizationErrorHandler;
124125

125126
private final boolean resumableStreams;
126127

@@ -139,7 +140,8 @@ public class HttpClientStreamableHttpTransport implements McpClientTransport {
139140
private HttpClientStreamableHttpTransport(McpJsonMapper jsonMapper, HttpClient httpClient,
140141
HttpRequest.Builder requestBuilder, String baseUri, String endpoint, boolean resumableStreams,
141142
boolean openConnectionOnStartup, McpAsyncHttpClientRequestCustomizer httpRequestCustomizer,
142-
McpHttpClientAuthorizationErrorHandler authorizationErrorHandler, List<String> supportedProtocolVersions) {
143+
McpHttpClientTransportAuthorizationErrorHandler authorizationErrorHandler,
144+
List<String> supportedProtocolVersions) {
143145
this.jsonMapper = jsonMapper;
144146
this.httpClient = httpClient;
145147
this.requestBuilder = requestBuilder;
@@ -295,10 +297,13 @@ private Mono<Disposable> reconnect(McpTransportStream<Disposable> stream) {
295297
int statusCode = responseEvent.responseInfo().statusCode();
296298
if (statusCode == 401 || statusCode == 403) {
297299
logger.debug("Authorization error in reconnect with code {}", statusCode);
300+
var request = requestBuilder.build();
301+
var requestSnapshot = new HttpRequestSnapshot(request.uri(), request.method(),
302+
request.headers());
298303
return Mono.<McpSchema.JSONRPCMessage>error(
299304
new McpHttpClientTransportAuthorizationException(
300305
"Authorization error connecting to SSE stream",
301-
responseEvent.responseInfo()));
306+
responseEvent.responseInfo(), requestSnapshot));
302307
}
303308
else if (statusCode == METHOD_NOT_ALLOWED) {
304309
logger.debug("The server does not support SSE streams, using request-response mode.");
@@ -417,7 +422,8 @@ private Retry authorizationErrorRetrySpec() {
417422
return Mono.deferContextual(ctx -> {
418423
var transportContext = ctx.getOrDefault(McpTransportContext.KEY, McpTransportContext.EMPTY);
419424
return Mono
420-
.from(this.authorizationErrorHandler.handle(authException.getResponseInfo(), transportContext))
425+
.from(this.authorizationErrorHandler.handle(authException.getResponseInfo(),
426+
authException.getRequestSnapshot(), transportContext))
421427
.switchIfEmpty(Mono.just(false))
422428
.flatMap(shouldRetry -> shouldRetry ? Mono.just(retrySignal.totalRetries())
423429
: Mono.error(retrySignal.failure()));
@@ -489,7 +495,6 @@ public Mono<Void> sendMessage(McpSchema.JSONRPCMessage sentMessage) {
489495
return Mono
490496
.from(this.httpRequestCustomizer.customize(builder, "POST", uri, jsonBody, transportContext));
491497
}).flatMapMany(requestBuilder -> Flux.<ResponseEvent>create(responseEventSink -> {
492-
493498
// Create the async request with proper body subscriber selection
494499
Mono.fromFuture(this.httpClient
495500
.sendAsync(requestBuilder.build(), this.toSendMessageBodySubscriber(responseEventSink))
@@ -502,12 +507,14 @@ public Mono<Void> sendMessage(McpSchema.JSONRPCMessage sentMessage) {
502507
}
503508
})).onErrorMap(CompletionException.class, t -> t.getCause()).onErrorComplete().subscribe();
504509

505-
})).flatMap(responseEvent -> {
510+
}).flatMap(responseEvent -> {
506511
int statusCode = responseEvent.responseInfo().statusCode();
507512
if (statusCode == 401 || statusCode == 403) {
513+
var request = requestBuilder.build();
514+
var requestSnapshot = new HttpRequestSnapshot(request.uri(), request.method(), request.headers());
508515
logger.debug("Authorization error in sendMessage with code {}", statusCode);
509516
return Mono.<McpSchema.JSONRPCMessage>error(new McpHttpClientTransportAuthorizationException(
510-
"Authorization error when sending message", responseEvent.responseInfo()));
517+
"Authorization error when sending message", responseEvent.responseInfo(), requestSnapshot));
511518
}
512519

513520
if (transportSession.markInitialized(
@@ -651,13 +658,12 @@ else if (statusCode == BAD_REQUEST) {
651658
if (ref != null) {
652659
transportSession.removeConnection(ref);
653660
}
654-
})
655-
.contextWrite(deliveredSink.contextView())
656-
.subscribe();
661+
})).contextWrite(deliveredSink.contextView()).subscribe();
657662

658663
disposableRef.set(connection);
659664
transportSession.addConnection(connection);
660665
});
666+
661667
}
662668

663669
private static String sessionIdOrPlaceholder(McpTransportSession<?> transportSession) {
@@ -695,7 +701,7 @@ public static class Builder {
695701
private List<String> supportedProtocolVersions = List.of(ProtocolVersions.MCP_2024_11_05,
696702
ProtocolVersions.MCP_2025_03_26, ProtocolVersions.MCP_2025_06_18, ProtocolVersions.MCP_2025_11_25);
697703

698-
private McpHttpClientAuthorizationErrorHandler authorizationErrorHandler = McpHttpClientAuthorizationErrorHandler.NOOP;
704+
private McpHttpClientTransportAuthorizationErrorHandler authorizationErrorHandler = McpHttpClientTransportAuthorizationErrorHandler.NOOP;
699705

700706
/**
701707
* Creates a new builder with the specified base URI.
@@ -828,8 +834,34 @@ public Builder asyncHttpRequestCustomizer(McpAsyncHttpClientRequestCustomizer as
828834
* when sending a message.
829835
* @param authorizationErrorHandler the handler
830836
* @return this builder
837+
* @deprecated in favor of
838+
* {@link #authorizationErrorHandler(McpHttpClientTransportAuthorizationErrorHandler)}
831839
*/
840+
@Deprecated(forRemoval = true, since = "2.0.0")
832841
public Builder authorizationErrorHandler(McpHttpClientAuthorizationErrorHandler authorizationErrorHandler) {
842+
this.authorizationErrorHandler = new McpHttpClientTransportAuthorizationErrorHandler() {
843+
@Override
844+
public Publisher<Boolean> handle(java.net.http.HttpResponse.ResponseInfo responseInfo,
845+
HttpRequestSnapshot requestSnapshot, McpTransportContext context) {
846+
return authorizationErrorHandler.handle(responseInfo, context);
847+
}
848+
849+
@Override
850+
public int maxRetries() {
851+
return authorizationErrorHandler.maxRetries();
852+
}
853+
};
854+
return this;
855+
}
856+
857+
/**
858+
* Sets the handler to be used when the server responds with HTTP 401 or HTTP 403
859+
* when sending a message.
860+
* @param authorizationErrorHandler the handler
861+
* @return this builder
862+
*/
863+
public Builder authorizationErrorHandler(
864+
McpHttpClientTransportAuthorizationErrorHandler authorizationErrorHandler) {
833865
this.authorizationErrorHandler = authorizationErrorHandler;
834866
return this;
835867
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Copyright 2026-2026 the original author or authors.
3+
*/
4+
5+
package io.modelcontextprotocol.client.transport;
6+
7+
import java.net.URI;
8+
import java.net.http.HttpHeaders;
9+
import java.net.http.HttpRequest;
10+
import java.net.http.HttpRequest.BodyPublisher;
11+
12+
/**
13+
* Captures information about an HTTP request. We use this instead of passing the plain
14+
* {@link HttpRequest} object because we want to avoid retaining a reference to the
15+
* request's {@link BodyPublisher}.
16+
*
17+
* @param requestUri the HTTP request URI
18+
* @param method the HTTP method
19+
* @param headers the HTTP request headers
20+
* @author Daniel Garnier-Moiroux
21+
*/
22+
public record HttpRequestSnapshot(URI requestUri, String method, HttpHeaders headers) {
23+
}

mcp-core/src/main/java/io/modelcontextprotocol/client/transport/McpHttpClientTransportAuthorizationException.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,21 @@ public class McpHttpClientTransportAuthorizationException extends McpTransportEx
1919

2020
private final HttpResponse.ResponseInfo responseInfo;
2121

22-
public McpHttpClientTransportAuthorizationException(String message, HttpResponse.ResponseInfo responseInfo) {
22+
private final HttpRequestSnapshot requestSnapshot;
23+
24+
public McpHttpClientTransportAuthorizationException(String message, HttpResponse.ResponseInfo responseInfo,
25+
HttpRequestSnapshot requestSnapshot) {
2326
super(message);
2427
this.responseInfo = responseInfo;
28+
this.requestSnapshot = requestSnapshot;
2529
}
2630

2731
public HttpResponse.ResponseInfo getResponseInfo() {
2832
return responseInfo;
2933
}
3034

35+
public HttpRequestSnapshot getRequestSnapshot() {
36+
return requestSnapshot;
37+
}
38+
3139
}

mcp-core/src/main/java/io/modelcontextprotocol/client/transport/customizer/McpHttpClientAuthorizationErrorHandler.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import java.net.http.HttpResponse;
88

9+
import io.modelcontextprotocol.client.transport.HttpRequestSnapshot;
910
import io.modelcontextprotocol.client.transport.McpHttpClientTransportAuthorizationException;
1011
import io.modelcontextprotocol.common.McpTransportContext;
1112
import org.reactivestreams.Publisher;
@@ -20,7 +21,9 @@
2021
* "https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization">MCP
2122
* Specification: Authorization</a>
2223
* @author Daniel Garnier-Moiroux
24+
* @deprecated in favor of {@link McpHttpClientTransportAuthorizationErrorHandler}
2325
*/
26+
@Deprecated(forRemoval = true, since = "2.0.0")
2427
public interface McpHttpClientAuthorizationErrorHandler {
2528

2629
/**
@@ -38,7 +41,10 @@ public interface McpHttpClientAuthorizationErrorHandler {
3841
* @param context the MCP client transport context
3942
* @return {@link Publisher} emitting true if the original request should be replayed,
4043
* false otherwise.
44+
* @deprecated in favor of
45+
* {@link McpHttpClientTransportAuthorizationErrorHandler#handle(HttpResponse.ResponseInfo, HttpRequestSnapshot, McpTransportContext)}
4146
*/
47+
@Deprecated(forRemoval = true, since = "2.0.0")
4248
Publisher<Boolean> handle(HttpResponse.ResponseInfo responseInfo, McpTransportContext context);
4349

4450
/**
@@ -87,7 +93,10 @@ interface Sync {
8793
* @param responseInfo the HTTP response information
8894
* @param context the MCP client transport context
8995
* @return true if the original request should be replayed, false otherwise.
96+
* @deprecated in favor of
97+
* {@link McpHttpClientTransportAuthorizationErrorHandler.Sync#handle(HttpResponse.ResponseInfo, HttpRequestSnapshot, McpTransportContext)}
9098
*/
99+
@Deprecated(forRemoval = true, since = "2.0.0")
91100
boolean handle(HttpResponse.ResponseInfo responseInfo, McpTransportContext context);
92101

93102
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
* Copyright 2026-2026 the original author or authors.
3+
*/
4+
5+
package io.modelcontextprotocol.client.transport.customizer;
6+
7+
import java.net.URI;
8+
import java.net.http.HttpResponse;
9+
10+
import io.modelcontextprotocol.client.transport.HttpRequestSnapshot;
11+
import io.modelcontextprotocol.client.transport.McpHttpClientTransportAuthorizationException;
12+
import io.modelcontextprotocol.common.McpTransportContext;
13+
import org.reactivestreams.Publisher;
14+
import reactor.core.publisher.Mono;
15+
import reactor.core.scheduler.Schedulers;
16+
17+
/**
18+
* Handle security-related errors in HTTP-client based transports. This class handles MCP
19+
* server responses with status code 401 and 403.
20+
*
21+
* @see <a href=
22+
* "https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization">MCP
23+
* Specification: Authorization</a>
24+
* @author Daniel Garnier-Moiroux
25+
*/
26+
public interface McpHttpClientTransportAuthorizationErrorHandler {
27+
28+
/**
29+
* Handle authorization error (HTTP 401 or 403), and signal whether the HTTP request
30+
* should be retried or not. If the publisher returns true, the original transport
31+
* method (connect, sendMessage) will be replayed with the original arguments.
32+
* Otherwise, the transport will throw an
33+
* {@link McpHttpClientTransportAuthorizationException}, indicating the error status.
34+
* <p>
35+
* If the returned {@link Publisher} errors, the error will be propagated to the
36+
* calling method, to be handled by the caller.
37+
* <p>
38+
* The number of retries is bounded by {@link #maxRetries()}.
39+
* @param responseInfo the HTTP response information
40+
* @param requestSnapshot the HTTP request snapshot that failed authorization
41+
* @param context the MCP client transport context
42+
* @return {@link Publisher} emitting true if the original request should be replayed,
43+
* false otherwise.
44+
*/
45+
Publisher<Boolean> handle(HttpResponse.ResponseInfo responseInfo, HttpRequestSnapshot requestSnapshot,
46+
McpTransportContext context);
47+
48+
/**
49+
* Maximum number of authorization error retries the transport will attempt. When the
50+
* handler signals a retry via {@link #handle}, the transport will replay the original
51+
* request at most this many times. If the authorization error persists after
52+
* exhausting all retries, the transport will propagate the
53+
* {@link McpHttpClientTransportAuthorizationException}.
54+
* <p>
55+
* Defaults to {@code 1}.
56+
* @return the maximum number of retries
57+
*/
58+
default int maxRetries() {
59+
return 1;
60+
}
61+
62+
/**
63+
* A no-op handler, used in the default use-case.
64+
*/
65+
McpHttpClientTransportAuthorizationErrorHandler NOOP = new Noop();
66+
67+
/**
68+
* Create a {@link McpHttpClientTransportAuthorizationErrorHandler} from a synchronous
69+
* handler. Will be subscribed on {@link Schedulers#boundedElastic()}. The handler may
70+
* be blocking.
71+
* @param handler the synchronous handler
72+
* @return an async handler
73+
*/
74+
static McpHttpClientTransportAuthorizationErrorHandler fromSync(Sync handler) {
75+
return (info, snapshot, context) -> Mono.fromCallable(() -> handler.handle(info, snapshot, context))
76+
.subscribeOn(Schedulers.boundedElastic());
77+
}
78+
79+
/**
80+
* Synchronous authorization error handler.
81+
*/
82+
interface Sync {
83+
84+
/**
85+
* Handle authorization error (HTTP 401 or 403), and signal whether the HTTP
86+
* request should be retried or not. If the return value is true, the original
87+
* transport method (connect, sendMessage) will be replayed with the original
88+
* arguments. Otherwise, the transport will throw an
89+
* {@link McpHttpClientTransportAuthorizationException}, indicating the error
90+
* status.
91+
* @param responseInfo the HTTP response information
92+
* @param requestSnapshot the HTTP request snapshot that failed authorization
93+
* @param context the MCP client transport context
94+
* @return true if the original request should be replayed, false otherwise.
95+
*/
96+
boolean handle(HttpResponse.ResponseInfo responseInfo, HttpRequestSnapshot requestSnapshot,
97+
McpTransportContext context);
98+
99+
}
100+
101+
class Noop implements McpHttpClientTransportAuthorizationErrorHandler {
102+
103+
@Override
104+
public Publisher<Boolean> handle(HttpResponse.ResponseInfo responseInfo, HttpRequestSnapshot requestSnapshot,
105+
McpTransportContext context) {
106+
return Mono.just(false);
107+
}
108+
109+
}
110+
111+
}

mcp-core/src/test/java/io/modelcontextprotocol/client/transport/customizer/McpHttpClientAuthorizationErrorHandlerTest.java

Lines changed: 0 additions & 48 deletions
This file was deleted.

0 commit comments

Comments
 (0)