spring-projects/spring-ai · warning

Notification: + sentMessage + received non-compliant…

Error message

Notification: + sentMessage + received non-compliant response: + responseMessage

What it means

A warning in directResponseFlux when an HTTP POST returning a direct (non-stream) response body gets a body even though the sent message was a JSON-RPC notification. Per the MCP Streamable HTTP spec, notifications must receive 202 with no body, so any non-empty body is flagged as a non-compliant response and the flux completes normally, discarding the body.

Solutions

  1. Fix the server (or update its MCP transport) to return HTTP 202 with an empty body for notifications
  2. Check intermediary proxies/gateways for response body injection or status rewriting
  3. Ignore the warning if harmless, but verify notifications are actually processed server-side
  4. Test with the reference MCP server implementation to isolate compliance issues

Example fix

// before
// server: return ok().body("accepted", String.class) for notifications
// after
// server: return ResponseEntity.accepted().build(); // 202, no body for notifications
Defensive patterns

Strategy: fallback

Type guard

boolean isNotification(McpSchema.JSONRPCMessage m) {
    return m instanceof McpSchema.JSONRPCNotification;
}
// only send notifications expecting no response body

Prevention

When it happens

Trigger: Server responds 200 with a JSON body to a notification POST; a proxy rewrites 202 responses into 200 with a body; server transport implementation not following the notification-202 rule.

Common situations: Custom or hand-rolled MCP servers replying to notifications; gateway/proxy middleware adding response bodies; older server versions predating strict Streamable HTTP compliance.

Related errors


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

Appendix: source

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

		return response.statusCode().isSameCodeAs(HttpStatus.METHOD_NOT_ALLOWED);
	}

	private static boolean isEventStream(ClientResponse response) {
		return response.statusCode().is2xxSuccessful() && response.headers().contentType().isPresent()
				&& response.headers().contentType().get().isCompatibleWith(MediaType.TEXT_EVENT_STREAM);
	}

	private static String sessionIdOrPlaceholder(McpTransportSession<?> transportSession) {
		return transportSession.sessionId().orElse(MISSING_SESSION_ID);
	}

	private Flux<McpSchema.JSONRPCMessage> directResponseFlux(McpSchema.JSONRPCMessage sentMessage,
			ClientResponse response) {
		return response.bodyToMono(String.class).<Iterable<McpSchema.JSONRPCMessage>>handle((responseMessage, s) -> {
			try {
				if (sentMessage instanceof McpSchema.JSONRPCNotification) {
					if (logger.isWarnEnabled()) {
						logger.warn("Notification: " + sentMessage + " received non-compliant response: "
								+ (Utils.hasText(responseMessage) ? responseMessage : "[empty]"));
					}
					s.complete();
				}
				else {
					McpSchema.JSONRPCMessage jsonRpcResponse = McpSchema.deserializeJsonRpcMessage(this.jsonMapper,
							responseMessage);
					s.next(List.of(jsonRpcResponse));
				}
			}
			catch (IOException e) {
				s.error(new McpTransportException(e));
			}
		}).flatMapIterable(Function.identity());
	}

	private Flux<McpSchema.JSONRPCMessage> newEventStream(ClientResponse response, String sessionRepresentation) {
		McpTransportStream<Disposable> sessionStream = new DefaultMcpTransportStream<>(this.resumableStreams,

View on GitHub (pinned to 98a7beda4f)