spring-projects/spring-ai · warning

Transport has no request handler registered. Remember to cal

Error message

Transport has no request handler registered. Remember to call connect!

What it means

A warning logged in WebClientStreamableHttpTransport.reconnect when the transport's request handler (set during connect()) is absent. Messages cannot be routed to the MCP session handler, so the code falls back to a request handler that errors every message with IllegalStateException("No request handler"). This happens when reconnect() runs before or after the transport was properly connected.

Source

Thrown at mcp/transport/mcp-spring-webflux/src/main/java/org/springframework/ai/mcp/client/webflux/transport/WebClientStreamableHttpTransport.java:231

	@Override
	public Mono<Void> closeGracefully() {
		return Mono.defer(() -> {
			logger.debug("Graceful close triggered");
			McpTransportSession<Disposable> currentSession = this.activeSession
				.getAndSet(ClosedMcpTransportSession.INSTANCE);
			if (currentSession != null) {
				return Mono.from(currentSession.closeGracefully());
			}
			return Mono.empty();
		});
	}

	private Mono<Disposable> reconnect(@Nullable McpTransportStream<Disposable> stream) {
		return Mono.deferContextual(ctx -> {
			var rh = this.handler.get();
			if (rh == null) {
				logger.warn("Transport has no request handler registered. Remember to call connect!");
			}

			final Function<Mono<McpSchema.JSONRPCMessage>, Mono<McpSchema.JSONRPCMessage>> requestHandler = rh != null
					? rh : msg -> Mono.error(new IllegalStateException("No request handler"));

			final McpTransportSession<Disposable> transportSession = this.activeSession.get();

			if (ClosedMcpTransportSession.INSTANCE.equals(transportSession)) {
				throw new McpTransportSessionClosedException();
			}
			if (stream != null) {
				if (logger.isDebugEnabled()) {
					logger.debug("Reconnecting stream " + stream.streamId() + " with lastId " + stream.lastId());
				}
			}
			else {
				logger.debug("Reconnecting with no prior stream");
			}

View on GitHub (pinned to 98a7beda4f)

Solutions

  1. Always call transport.connect() before sending messages or relying on streams
  2. Ensure the connect() Mono is subscribed to and completed (it registers the handler) before traffic flows
  3. Check shutdown ordering so reconnects are not triggered after disposal
  4. Inspect lifecycle code for disposing the transport while its SSE stream auto-reconnects

Example fix

// before
WebClientStreamableHttpTransport transport = WebClientStreamableHttpTransport.builder().build();
transport.sendMessage(message).block(); // handler not registered yet
// after
WebClientStreamableHttpTransport transport = WebClientStreamableHttpTransport.builder().build();
transport.connect().block(); // registers the request handler
transport.sendMessage(message).block();
Defensive patterns

Strategy: type-guard

Validate before calling

// only proceed after connect completes
transport.connect().block(Duration.ofSeconds(10));

Type guard

boolean isConnected(WebClientStreamableHttpTransport t) {
    try {
        var f = WebClientStreamableHttpTransport.class.getDeclaredField("handler");
        f.setAccessible(true);
        return ((java.util.concurrent.atomic.AtomicReference<?>) f.get(t)).get() != null;
    } catch (Exception e) { return false; }
}

Try / catch

try {
    transport.sendMessage(msg).block();
} catch (IllegalStateException e) {
    if (e.getMessage().contains("No request handler")) {
        transport.connect().block();
    }
}

Prevention

When it happens

Trigger: Calling reconnect() (directly or via stream re-establishment) on a transport whose connect() was never invoked; connect() was disposed/aborted, clearing the handler reference; a race where the SSE stream reconnects after the client shut down.

Common situations: Managing transport lifecycle manually and forgetting connect(); shutdown ordering issues where streams reconnect during application teardown; failed initialization leaving the handler unset but the stream retrying.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of spring-projects/spring-ai@98a7beda4f (2026-09-11). Data as JSON: /api/errors/94178a56cd15ce93. Report an issue: GitHub.