Skip to content

Commit 63cbc7a

Browse files
committed
fix: race condition in ResponseSubscribers
1 parent 61d1987 commit 63cbc7a

2 files changed

Lines changed: 57 additions & 11 deletions

File tree

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

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,7 @@ static class AggregateSubscriber extends BaseSubscriber<String> {
320320
* The response information from the HTTP response. Send with each event to
321321
* provide context.
322322
*/
323-
private ResponseInfo responseInfo;
323+
private final ResponseInfo responseInfo;
324324

325325
volatile boolean hasRequestedDemand = false;
326326

@@ -348,16 +348,15 @@ public AggregateSubscriber(ResponseInfo responseInfo, FluxSink<ResponseEvent> si
348348

349349
@Override
350350
protected void hookOnSubscribe(Subscription subscription) {
351+
// Register disposal callback to cancel subscription when Flux is disposed
352+
sink.onDispose(subscription::cancel);
351353

352354
sink.onRequest(n -> {
353355
if (!hasRequestedDemand) {
356+
hasRequestedDemand = true;
354357
subscription.request(Long.MAX_VALUE);
355358
}
356-
hasRequestedDemand = true;
357359
});
358-
359-
// Register disposal callback to cancel subscription when Flux is disposed
360-
sink.onDispose(subscription::cancel);
361360
}
362361

363362
@Override
@@ -410,17 +409,14 @@ public BodilessResponseLineSubscriber(ResponseInfo responseInfo, FluxSink<Respon
410409

411410
@Override
412411
protected void hookOnSubscribe(Subscription subscription) {
412+
// Register disposal callback to cancel subscription when Flux is disposed
413+
sink.onDispose(subscription::cancel);
413414

414415
sink.onRequest(n -> {
415416
if (!hasRequestedDemand) {
417+
hasRequestedDemand = true;
416418
subscription.request(Long.MAX_VALUE);
417419
}
418-
hasRequestedDemand = true;
419-
});
420-
421-
// Register disposal callback to cancel subscription when Flux is disposed
422-
sink.onDispose(() -> {
423-
subscription.cancel();
424420
});
425421
}
426422

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2024 - 2024 the original author or authors.
3+
*/
4+
5+
package io.modelcontextprotocol.client.transport;
6+
7+
import java.net.http.HttpResponse.ResponseInfo;
8+
9+
import org.junit.jupiter.api.Test;
10+
11+
import reactor.core.publisher.Flux;
12+
import reactor.test.StepVerifier;
13+
14+
import static org.assertj.core.api.Assertions.assertThat;
15+
import static org.mockito.Mockito.mock;
16+
17+
class ResponseSubscribersTests {
18+
19+
@Test
20+
void aggregateSubscriberEmitsResponseWhenRequestCompletesSynchronously() {
21+
ResponseInfo responseInfo = mock(ResponseInfo.class);
22+
23+
Flux<ResponseSubscribers.ResponseEvent> response = Flux.create(sink -> {
24+
var subscriber = new ResponseSubscribers.AggregateSubscriber(responseInfo, sink, Integer.MAX_VALUE);
25+
Flux.just("payload").subscribe(subscriber);
26+
});
27+
28+
StepVerifier.create(response).assertNext(event -> {
29+
var aggregate = (ResponseSubscribers.AggregateResponseEvent) event;
30+
assertThat(aggregate.responseInfo()).isSameAs(responseInfo);
31+
assertThat(aggregate.data()).isEqualTo("payload\n");
32+
}).verifyComplete();
33+
}
34+
35+
@Test
36+
void bodilessSubscriberEmitsResponseWhenRequestCompletesSynchronously() {
37+
ResponseInfo responseInfo = mock(ResponseInfo.class);
38+
39+
Flux<ResponseSubscribers.ResponseEvent> response = Flux.create(sink -> {
40+
var subscriber = new ResponseSubscribers.BodilessResponseLineSubscriber(responseInfo, sink);
41+
Flux.<String>empty().subscribe(subscriber);
42+
});
43+
44+
StepVerifier.create(response).assertNext(event -> {
45+
var dummy = (ResponseSubscribers.DummyEvent) event;
46+
assertThat(dummy.responseInfo()).isSameAs(responseInfo);
47+
}).verifyComplete();
48+
}
49+
50+
}

0 commit comments

Comments
 (0)