Skip to content

Commit 58cce01

Browse files
committed
Consistent Publisher error handling with SSE
Closes gh-1080
1 parent cb9afd0 commit 58cce01

File tree

4 files changed

+43
-20
lines changed

4 files changed

+43
-20
lines changed

spring-graphql/src/main/java/org/springframework/graphql/server/webflux/GraphQlSseHandler.java

+15-3
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import graphql.ErrorType;
2525
import graphql.ExecutionResult;
2626
import graphql.GraphQLError;
27+
import graphql.GraphqlErrorBuilder;
2728
import org.reactivestreams.Publisher;
2829
import reactor.core.publisher.Flux;
2930
import reactor.core.publisher.Mono;
@@ -86,7 +87,7 @@ protected Mono<ServerResponse> prepareResponse(ServerRequest request, WebGraphQl
8687
if (response.getData() instanceof Publisher) {
8788
resultFlux = Flux.from((Publisher<ExecutionResult>) response.getData())
8889
.map(ExecutionResult::toSpecification)
89-
.onErrorResume(SubscriptionPublisherException.class, (ex) -> Mono.just(ex.toMap()));
90+
.onErrorResume(this::exceptionToResultMap);
9091
}
9192
else {
9293
if (this.logger.isDebugEnabled()) {
@@ -102,14 +103,25 @@ protected Mono<ServerResponse> prepareResponse(ServerRequest request, WebGraphQl
102103
}
103104

104105
Flux<ServerSentEvent<Map<String, Object>>> sseFlux =
105-
resultFlux.map((event) -> ServerSentEvent.builder(event).event("next").build());
106+
resultFlux.map((event) -> ServerSentEvent.builder(event).event("next").build())
107+
.concatWith(COMPLETE_EVENT);
106108

107109
Mono<ServerResponse> responseMono = ServerResponse.ok()
108110
.contentType(MediaType.TEXT_EVENT_STREAM)
109-
.body(BodyInserters.fromServerSentEvents(sseFlux.concatWith(COMPLETE_EVENT)))
111+
.body(BodyInserters.fromServerSentEvents(sseFlux))
110112
.onErrorResume(Throwable.class, (ex) -> ServerResponse.badRequest().build());
111113

112114
return ((this.timeout != null) ? responseMono.timeout(this.timeout) : responseMono);
113115
}
114116

117+
private Mono<Map<String, Object>> exceptionToResultMap(Throwable ex) {
118+
return Mono.just((ex instanceof SubscriptionPublisherException spe) ?
119+
spe.toMap() :
120+
GraphqlErrorBuilder.newError()
121+
.message("Subscription error")
122+
.errorType(org.springframework.graphql.execution.ErrorType.INTERNAL_ERROR)
123+
.build()
124+
.toSpecification());
125+
}
126+
115127
}

spring-graphql/src/main/java/org/springframework/graphql/server/webmvc/GraphQlSseHandler.java

+21-12
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import graphql.ErrorType;
2525
import graphql.ExecutionResult;
2626
import graphql.GraphQLError;
27+
import graphql.GraphqlErrorBuilder;
2728
import org.reactivestreams.Publisher;
2829
import reactor.core.publisher.BaseSubscriber;
2930
import reactor.core.publisher.Flux;
@@ -119,10 +120,10 @@ private SseSubscriber(ServerResponse.SseBuilder sseBuilder) {
119120

120121
@Override
121122
protected void hookOnNext(Map<String, Object> value) {
122-
writeResult(value);
123+
sendNext(value);
123124
}
124125

125-
private void writeResult(Map<String, Object> value) {
126+
private void sendNext(Map<String, Object> value) {
126127
try {
127128
this.sseBuilder.event("next");
128129
this.sseBuilder.data(value);
@@ -139,18 +140,21 @@ private void cancelWithError(Throwable ex) {
139140

140141
@Override
141142
protected void hookOnError(Throwable ex) {
142-
if (ex instanceof SubscriptionPublisherException spe) {
143-
ExecutionResult result = ExecutionResult.newExecutionResult().errors(spe.getErrors()).build();
144-
writeResult(result.toSpecification());
145-
hookOnComplete();
146-
}
147-
else {
148-
this.sseBuilder.error(ex);
149-
}
143+
sendNext(exceptionToResultMap(ex));
144+
sendComplete();
150145
}
151146

152-
@Override
153-
protected void hookOnComplete() {
147+
private static Map<String, Object> exceptionToResultMap(Throwable ex) {
148+
return ((ex instanceof SubscriptionPublisherException spe) ?
149+
spe.toMap() :
150+
GraphqlErrorBuilder.newError()
151+
.message("Subscription error")
152+
.errorType(org.springframework.graphql.execution.ErrorType.INTERNAL_ERROR)
153+
.build()
154+
.toSpecification());
155+
}
156+
157+
private void sendComplete() {
154158
try {
155159
this.sseBuilder.event("complete").data("");
156160
}
@@ -160,6 +164,11 @@ protected void hookOnComplete() {
160164
this.sseBuilder.complete();
161165
}
162166

167+
@Override
168+
protected void hookOnComplete() {
169+
sendComplete();
170+
}
171+
163172
static Consumer<ServerResponse.SseBuilder> connect(Flux<Map<String, Object>> resultFlux) {
164173
return (sseBuilder) -> {
165174
SseSubscriber subscriber = new SseSubscriber(sseBuilder);

spring-graphql/src/test/java/org/springframework/graphql/server/webflux/GraphQlSseHandlerTests.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ class GraphQlSseHandlerTests {
5555

5656
private static final DataFetcher<?> SEARCH_DATA_FETCHER = env -> {
5757
String author = env.getArgument("author");
58-
return Flux.fromIterable(BookSource.books()).filter((book) -> book.getAuthor().getFullName().contains(author));
58+
return Flux.fromIterable(BookSource.books())
59+
.filter((book) -> book.getAuthor().getFullName().contains(author));
5960
};
6061

6162
private final MockServerHttpRequest httpRequest = MockServerHttpRequest.post("/graphql")

spring-graphql/src/test/java/org/springframework/graphql/server/webmvc/GraphQlSseHandlerTests.java

+5-4
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
*
5757
* @author Brian Clozel
5858
*/
59+
@SuppressWarnings("ReactiveStreamsUnusedPublisher")
5960
class GraphQlSseHandlerTests {
6061

6162
private static final List<HttpMessageConverter<?>> MESSAGE_READERS =
@@ -92,7 +93,7 @@ void shouldRejectQueryOperations() throws Exception {
9293
void shouldWriteMultipleEventsForSubscription() throws Exception {
9394
GraphQlSseHandler handler = createSseHandler(SEARCH_DATA_FETCHER);
9495
MockHttpServletRequest request = createServletRequest("""
95-
{ "query": "subscription TestSubscription { bookSearch(author:\\\"Orwell\\\") { id name } }" }
96+
{ "query": "subscription TestSubscription { bookSearch(author:\\"Orwell\\") { id name } }" }
9697
""");
9798
MockHttpServletResponse response = handleAndAwait(request, handler);
9899

@@ -118,7 +119,7 @@ void shouldWriteEventsAndTerminalError() throws Exception {
118119

119120
GraphQlSseHandler handler = createSseHandler(errorDataFetcher);
120121
MockHttpServletRequest request = createServletRequest("""
121-
{ "query": "subscription TestSubscription { bookSearch(author:\\\"Orwell\\\") { id name } }" }
122+
{ "query": "subscription TestSubscription { bookSearch(author:\\"Orwell\\") { id name } }" }
122123
""");
123124
MockHttpServletResponse response = handleAndAwait(request, handler);
124125

@@ -140,7 +141,7 @@ void shouldWriteEventsAndTerminalError() throws Exception {
140141
void shouldCancelDataFetcherPublisherWhenWritingFails() throws Exception {
141142
GraphQlSseHandler handler = createSseHandler(SEARCH_DATA_FETCHER);
142143
MockHttpServletRequest servletRequest = createServletRequest("""
143-
{ "query": "subscription TestSubscription { bookSearch(author:\\\"Orwell\\\") { id name } }" }
144+
{ "query": "subscription TestSubscription { bookSearch(author:\\"Orwell\\") { id name } }" }
144145
""");
145146
HttpServletResponse servletResponse = mock(HttpServletResponse.class);
146147
ServletOutputStream outputStream = mock(ServletOutputStream.class);
@@ -165,7 +166,7 @@ void shouldCancelDataFetcherWhenAsyncTimeout() throws Exception {
165166

166167
GraphQlSseHandler handler = createSseHandler(errorDataFetcher);
167168
MockHttpServletRequest servletRequest = createServletRequest("""
168-
{ "query": "subscription TestSubscription { bookSearch(author:\\\"Orwell\\\") { id name } }" }
169+
{ "query": "subscription TestSubscription { bookSearch(author:\\"Orwell\\") { id name } }" }
169170
""");
170171

171172
MockHttpServletResponse servletResponse = handleRequest(servletRequest, handler);

0 commit comments

Comments
 (0)