Auto-configure SSE support for GraphQL endpoints
This commit auto-configures the newly supported SSE transport for GraphQL endpoints in both Spring MVC and WebFlux. Closes gh-39651
This commit is contained in:
@@ -48,6 +48,7 @@ import org.springframework.graphql.server.WebGraphQlHandler;
|
||||
import org.springframework.graphql.server.WebGraphQlInterceptor;
|
||||
import org.springframework.graphql.server.webflux.GraphQlHttpHandler;
|
||||
import org.springframework.graphql.server.webflux.GraphQlRequestPredicates;
|
||||
import org.springframework.graphql.server.webflux.GraphQlSseHandler;
|
||||
import org.springframework.graphql.server.webflux.GraphQlWebSocketHandler;
|
||||
import org.springframework.graphql.server.webflux.GraphiQlHandler;
|
||||
import org.springframework.graphql.server.webflux.SchemaHandler;
|
||||
@@ -83,12 +84,6 @@ public class GraphQlWebFluxAutoConfiguration {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(GraphQlWebFluxAutoConfiguration.class);
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public GraphQlHttpHandler graphQlHttpHandler(WebGraphQlHandler webGraphQlHandler) {
|
||||
return new GraphQlHttpHandler(webGraphQlHandler);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public WebGraphQlHandler webGraphQlHandler(ExecutionGraphQlService service,
|
||||
@@ -96,14 +91,27 @@ public class GraphQlWebFluxAutoConfiguration {
|
||||
return WebGraphQlHandler.builder(service).interceptors(interceptors.orderedStream().toList()).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public GraphQlHttpHandler graphQlHttpHandler(WebGraphQlHandler webGraphQlHandler) {
|
||||
return new GraphQlHttpHandler(webGraphQlHandler);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public GraphQlSseHandler graphQlSseHandler(WebGraphQlHandler webGraphQlHandler) {
|
||||
return new GraphQlSseHandler(webGraphQlHandler);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order(0)
|
||||
public RouterFunction<ServerResponse> graphQlRouterFunction(GraphQlHttpHandler httpHandler,
|
||||
GraphQlSource graphQlSource, GraphQlProperties properties) {
|
||||
GraphQlSseHandler sseHandler, GraphQlSource graphQlSource, GraphQlProperties properties) {
|
||||
String path = properties.getPath();
|
||||
logger.info(LogMessage.format("GraphQL endpoint HTTP POST %s", path));
|
||||
RouterFunctions.Builder builder = RouterFunctions.route();
|
||||
builder.route(GraphQlRequestPredicates.graphQlHttp(path), httpHandler::handleRequest);
|
||||
builder.route(GraphQlRequestPredicates.graphQlSse(path), sseHandler::handleRequest);
|
||||
builder = builder.GET(path, this::onlyAllowPost);
|
||||
if (properties.getGraphiql().isEnabled()) {
|
||||
GraphiQlHandler graphQlHandler = new GraphiQlHandler(path, properties.getWebsocket().getPath());
|
||||
|
||||
@@ -50,6 +50,7 @@ import org.springframework.graphql.server.WebGraphQlHandler;
|
||||
import org.springframework.graphql.server.WebGraphQlInterceptor;
|
||||
import org.springframework.graphql.server.webmvc.GraphQlHttpHandler;
|
||||
import org.springframework.graphql.server.webmvc.GraphQlRequestPredicates;
|
||||
import org.springframework.graphql.server.webmvc.GraphQlSseHandler;
|
||||
import org.springframework.graphql.server.webmvc.GraphQlWebSocketHandler;
|
||||
import org.springframework.graphql.server.webmvc.GraphiQlHandler;
|
||||
import org.springframework.graphql.server.webmvc.SchemaHandler;
|
||||
@@ -94,6 +95,12 @@ public class GraphQlWebMvcAutoConfiguration {
|
||||
return new GraphQlHttpHandler(webGraphQlHandler);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public GraphQlSseHandler graphQlSseHandler(WebGraphQlHandler webGraphQlHandler) {
|
||||
return new GraphQlSseHandler(webGraphQlHandler);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public WebGraphQlHandler webGraphQlHandler(ExecutionGraphQlService service,
|
||||
@@ -104,11 +111,12 @@ public class GraphQlWebMvcAutoConfiguration {
|
||||
@Bean
|
||||
@Order(0)
|
||||
public RouterFunction<ServerResponse> graphQlRouterFunction(GraphQlHttpHandler httpHandler,
|
||||
GraphQlSource graphQlSource, GraphQlProperties properties) {
|
||||
GraphQlSseHandler sseHandler, GraphQlSource graphQlSource, GraphQlProperties properties) {
|
||||
String path = properties.getPath();
|
||||
logger.info(LogMessage.format("GraphQL endpoint HTTP POST %s", path));
|
||||
RouterFunctions.Builder builder = RouterFunctions.route();
|
||||
builder.route(GraphQlRequestPredicates.graphQlHttp(path), httpHandler::handleRequest);
|
||||
builder.route(GraphQlRequestPredicates.graphQlSse(path), sseHandler::handleRequest);
|
||||
builder = builder.GET(path, this::onlyAllowPost);
|
||||
if (properties.getGraphiql().isEnabled()) {
|
||||
GraphiQlHandler graphiQLHandler = new GraphiQlHandler(path, properties.getWebsocket().getPath());
|
||||
|
||||
@@ -44,6 +44,7 @@ import org.springframework.graphql.server.webflux.GraphQlWebSocketHandler;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.reactive.server.EntityExchangeResult;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
|
||||
@@ -93,6 +94,30 @@ class GraphQlWebFluxAutoConfigurationTests {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void SseSubscriptionShouldWork() {
|
||||
testWithWebClient((client) -> {
|
||||
String query = "{ booksOnSale(minPages: 50){ id name pageCount author } }";
|
||||
EntityExchangeResult<String> result = client.post()
|
||||
.uri("/graphql")
|
||||
.accept(MediaType.TEXT_EVENT_STREAM)
|
||||
.bodyValue("{ \"query\": \"subscription TestSubscription " + query + "\"}")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
.expectHeader()
|
||||
.contentTypeCompatibleWith(MediaType.TEXT_EVENT_STREAM)
|
||||
.expectBody(String.class)
|
||||
.returnResult();
|
||||
|
||||
assertThat(result.getResponseBody()).contains("event:next",
|
||||
"data:{\"data\":{\"booksOnSale\":{\"id\":\"book-1\",\"name\":\"GraphQL for beginners\",\"pageCount\":100,\"author\":\"John GraphQL\"}}}",
|
||||
"event:next",
|
||||
"data:{\"data\":{\"booksOnSale\":{\"id\":\"book-2\",\"name\":\"Harry Potter and the Philosopher's Stone\",\"pageCount\":223,\"author\":\"Joanne Rowling\"}}}",
|
||||
"event:complete");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void httpGetQueryShouldBeSupported() {
|
||||
testWithWebClient((client) -> {
|
||||
@@ -233,8 +258,12 @@ class GraphQlWebFluxAutoConfigurationTests {
|
||||
|
||||
@Bean
|
||||
RuntimeWiringConfigurer bookDataFetcher() {
|
||||
return (builder) -> builder.type(TypeRuntimeWiring.newTypeWiring("Query")
|
||||
.dataFetcher("bookById", GraphQlTestDataFetchers.getBookByIdDataFetcher()));
|
||||
return (builder) -> {
|
||||
builder.type(TypeRuntimeWiring.newTypeWiring("Query")
|
||||
.dataFetcher("bookById", GraphQlTestDataFetchers.getBookByIdDataFetcher()));
|
||||
builder.type(TypeRuntimeWiring.newTypeWiring("Subscription")
|
||||
.dataFetcher("booksOnSale", GraphQlTestDataFetchers.getBooksOnSaleDataFetcher()));
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -93,6 +93,22 @@ class GraphQlWebMvcAutoConfigurationTests {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void SseSubscriptionShouldWork() {
|
||||
testWith((mockMvc) -> {
|
||||
String query = "{ booksOnSale(minPages: 50){ id name pageCount author } }";
|
||||
mockMvc
|
||||
.perform(post("/graphql").accept(MediaType.TEXT_EVENT_STREAM)
|
||||
.content("{\"query\": \"subscription TestSubscription " + query + "\"}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_EVENT_STREAM))
|
||||
.andExpect(content().string(Matchers.stringContainsInOrder("event:next",
|
||||
"data:{\"data\":{\"booksOnSale\":{\"id\":\"book-1\",\"name\":\"GraphQL for beginners\",\"pageCount\":100,\"author\":\"John GraphQL\"}}}",
|
||||
"event:next",
|
||||
"data:{\"data\":{\"booksOnSale\":{\"id\":\"book-2\",\"name\":\"Harry Potter and the Philosopher's Stone\",\"pageCount\":223,\"author\":\"Joanne Rowling\"}}}")));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void httpGetQueryShouldBeSupported() {
|
||||
testWith((mockMvc) -> {
|
||||
@@ -207,8 +223,12 @@ class GraphQlWebMvcAutoConfigurationTests {
|
||||
|
||||
@Bean
|
||||
RuntimeWiringConfigurer bookDataFetcher() {
|
||||
return (builder) -> builder.type(TypeRuntimeWiring.newTypeWiring("Query")
|
||||
.dataFetcher("bookById", GraphQlTestDataFetchers.getBookByIdDataFetcher()));
|
||||
return (builder) -> {
|
||||
builder.type(TypeRuntimeWiring.newTypeWiring("Query")
|
||||
.dataFetcher("bookById", GraphQlTestDataFetchers.getBookByIdDataFetcher()));
|
||||
builder.type(TypeRuntimeWiring.newTypeWiring("Subscription")
|
||||
.dataFetcher("booksOnSale", GraphQlTestDataFetchers.getBooksOnSaleDataFetcher()));
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,4 +2,8 @@ type Query {
|
||||
greeting(name: String! = "Spring"): String!
|
||||
bookById(id: ID): Book
|
||||
books: BookConnection
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
booksOnSale(minPages: Int) : Book!
|
||||
}
|
||||
@@ -87,6 +87,7 @@ are detected by Spring Boot and considered as candidates for `DataFetcher` for m
|
||||
[[web.graphql.transports.http-websocket]]
|
||||
==== HTTP and WebSocket
|
||||
The GraphQL HTTP endpoint is at HTTP POST `/graphql` by default.
|
||||
It also supports the `"text/event-stream"` media type over Server Sent Events for subscriptions only.
|
||||
The path can be customized with configprop:spring.graphql.path[].
|
||||
|
||||
TIP: The HTTP endpoint for both Spring MVC and Spring WebFlux is provided by a `RouterFunction` bean with an `@Order` of `0`.
|
||||
|
||||
Reference in New Issue
Block a user