Skip to content

Commit

Permalink
Separate transports in GraphQL auto-configurations
Browse files Browse the repository at this point in the history
This commit revisits the existing GraphQL configuration properties to
better reflect which ones belong to specific transports.
This also relaxes the Web auto-configurations to only require the
`ExecutionGraphQlService` as a bean. The `GraphQlSource` is now an
optional bean dependency.

Closes gh-44495
  • Loading branch information
bclozel committed Feb 28, 2025
1 parent e886785 commit 83f678a
Show file tree
Hide file tree
Showing 7 changed files with 84 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.util.Arrays;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.DeprecatedConfigurationProperty;
import org.springframework.core.io.Resource;

/**
Expand All @@ -31,31 +32,35 @@
@ConfigurationProperties("spring.graphql")
public class GraphQlProperties {

/**
* Path at which to expose a GraphQL request HTTP endpoint.
*/
private String path = "/graphql";
private final Http http = new Http();

private final Graphiql graphiql = new Graphiql();

private final Rsocket rsocket = new Rsocket();

private final Schema schema = new Schema();

private final Websocket websocket = new Websocket();
private final DeprecatedSse sse = new DeprecatedSse(this.http.getSse());

private final Rsocket rsocket = new Rsocket();
private final Websocket websocket = new Websocket();

private final Sse sse = new Sse();
public Http getHttp() {
return this.http;
}

public Graphiql getGraphiql() {
return this.graphiql;
}

@DeprecatedConfigurationProperty(replacement = "spring.graphql.http.path", since = "3.5.0")
@Deprecated(since = "3.5.0", forRemoval = true)
public String getPath() {
return this.path;
return getHttp().getPath();
}

@Deprecated(since = "3.5.0", forRemoval = true)
public void setPath(String path) {
this.path = path;
getHttp().setPath(path);
}

public Schema getSchema() {
Expand All @@ -70,10 +75,33 @@ public Rsocket getRsocket() {
return this.rsocket;
}

public Sse getSse() {
public DeprecatedSse getSse() {
return this.sse;
}

public static class Http {

/**
* Path at which to expose a GraphQL request HTTP endpoint.
*/
private String path = "/graphql";

private final Sse sse = new Sse();

public String getPath() {
return this.path;
}

public void setPath(String path) {
this.path = path;
}

public Sse getSse() {
return this.sse;
}

}

public static class Schema {

/**
Expand Down Expand Up @@ -178,7 +206,7 @@ public static class Printer {

/**
* Whether the endpoint that prints the schema is enabled. Schema is available
* under spring.graphql.path + "/schema".
* under spring.graphql.http.path + "/schema".
*/
private boolean enabled = false;

Expand Down Expand Up @@ -302,4 +330,25 @@ public void setTimeout(Duration timeout) {

}

public static class DeprecatedSse {

private final Sse sse;

public DeprecatedSse(Sse sse) {
this.sse = sse;
}

@DeprecatedConfigurationProperty(replacement = "spring.graphql.http.sse.timeout", since = "3.5.0")
@Deprecated(since = "3.5.0", forRemoval = true)
public Duration getTimeout() {
return this.sse.getTimeout();
}

@Deprecated(since = "3.5.0", forRemoval = true)
public void setTimeout(Duration timeout) {
this.sse.setTimeout(timeout);
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -87,28 +87,29 @@ public class GraphQlWebFluxAutoConfiguration {

@Bean
@ConditionalOnMissingBean
public WebGraphQlHandler webGraphQlHandler(ExecutionGraphQlService service,
ObjectProvider<WebGraphQlInterceptor> interceptors) {
return WebGraphQlHandler.builder(service).interceptors(interceptors.orderedStream().toList()).build();
public GraphQlHttpHandler graphQlHttpHandler(WebGraphQlHandler webGraphQlHandler) {
return new GraphQlHttpHandler(webGraphQlHandler);
}

@Bean
@ConditionalOnMissingBean
public GraphQlHttpHandler graphQlHttpHandler(WebGraphQlHandler webGraphQlHandler) {
return new GraphQlHttpHandler(webGraphQlHandler);
public GraphQlSseHandler graphQlSseHandler(WebGraphQlHandler webGraphQlHandler, GraphQlProperties properties) {
return new GraphQlSseHandler(webGraphQlHandler, properties.getHttp().getSse().getTimeout());
}

@Bean
@ConditionalOnMissingBean
public GraphQlSseHandler graphQlSseHandler(WebGraphQlHandler webGraphQlHandler) {
return new GraphQlSseHandler(webGraphQlHandler);
public WebGraphQlHandler webGraphQlHandler(ExecutionGraphQlService service,
ObjectProvider<WebGraphQlInterceptor> interceptors) {
return WebGraphQlHandler.builder(service).interceptors(interceptors.orderedStream().toList()).build();
}

@Bean
@Order(0)
public RouterFunction<ServerResponse> graphQlRouterFunction(GraphQlHttpHandler httpHandler,
GraphQlSseHandler sseHandler, GraphQlSource graphQlSource, GraphQlProperties properties) {
String path = properties.getPath();
GraphQlSseHandler sseHandler, ObjectProvider<GraphQlSource> graphQlSourceProvider,
GraphQlProperties properties) {
String path = properties.getHttp().getPath();
logger.info(LogMessage.format("GraphQL endpoint HTTP POST %s", path));
RouterFunctions.Builder builder = RouterFunctions.route();
builder.route(GraphQlRequestPredicates.graphQlHttp(path), httpHandler::handleRequest);
Expand All @@ -119,7 +120,8 @@ public RouterFunction<ServerResponse> graphQlRouterFunction(GraphQlHttpHandler h
GraphiQlHandler graphQlHandler = new GraphiQlHandler(path, properties.getWebsocket().getPath());
builder.GET(properties.getGraphiql().getPath(), graphQlHandler::handleRequest);
}
if (properties.getSchema().getPrinter().isEnabled()) {
GraphQlSource graphQlSource = graphQlSourceProvider.getIfAvailable();
if (properties.getSchema().getPrinter().isEnabled() && graphQlSource != null) {
SchemaHandler schemaHandler = new SchemaHandler(graphQlSource);
builder.GET(path + "/schema", schemaHandler::handleRequest);
}
Expand Down Expand Up @@ -158,7 +160,7 @@ public GraphQlEndpointCorsConfiguration(GraphQlProperties graphQlProps, GraphQlC
public void addCorsMappings(CorsRegistry registry) {
CorsConfiguration configuration = this.corsProperties.toCorsConfiguration();
if (configuration != null) {
registry.addMapping(this.graphQlProperties.getPath()).combine(configuration);
registry.addMapping(this.graphQlProperties.getHttp().getPath()).combine(configuration);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration;
import org.springframework.boot.autoconfigure.graphql.GraphQlCorsProperties;
import org.springframework.boot.autoconfigure.graphql.GraphQlProperties;
import org.springframework.boot.autoconfigure.graphql.GraphQlProperties.Sse;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
Expand Down Expand Up @@ -99,8 +98,7 @@ public GraphQlHttpHandler graphQlHttpHandler(WebGraphQlHandler webGraphQlHandler
@Bean
@ConditionalOnMissingBean
public GraphQlSseHandler graphQlSseHandler(WebGraphQlHandler webGraphQlHandler, GraphQlProperties properties) {
Sse sse = properties.getSse();
return new GraphQlSseHandler(webGraphQlHandler, sse.getTimeout());
return new GraphQlSseHandler(webGraphQlHandler, properties.getHttp().getSse().getTimeout());
}

@Bean
Expand All @@ -113,8 +111,9 @@ public WebGraphQlHandler webGraphQlHandler(ExecutionGraphQlService service,
@Bean
@Order(0)
public RouterFunction<ServerResponse> graphQlRouterFunction(GraphQlHttpHandler httpHandler,
GraphQlSseHandler sseHandler, GraphQlSource graphQlSource, GraphQlProperties properties) {
String path = properties.getPath();
GraphQlSseHandler sseHandler, ObjectProvider<GraphQlSource> graphQlSourceProvider,
GraphQlProperties properties) {
String path = properties.getHttp().getPath();
logger.info(LogMessage.format("GraphQL endpoint HTTP POST %s", path));
RouterFunctions.Builder builder = RouterFunctions.route();
builder.route(GraphQlRequestPredicates.graphQlHttp(path), httpHandler::handleRequest);
Expand All @@ -125,7 +124,8 @@ public RouterFunction<ServerResponse> graphQlRouterFunction(GraphQlHttpHandler h
GraphiQlHandler graphiQLHandler = new GraphiQlHandler(path, properties.getWebsocket().getPath());
builder.GET(properties.getGraphiql().getPath(), graphiQLHandler::handleRequest);
}
if (properties.getSchema().getPrinter().isEnabled()) {
GraphQlSource graphQlSource = graphQlSourceProvider.getIfAvailable();
if (properties.getSchema().getPrinter().isEnabled() && graphQlSource != null) {
SchemaHandler schemaHandler = new SchemaHandler(graphQlSource);
builder.GET(path + "/schema", schemaHandler::handleRequest);
}
Expand Down Expand Up @@ -164,7 +164,7 @@ public GraphQlEndpointCorsConfiguration(GraphQlProperties graphQlProps, GraphQlC
public void addCorsMappings(CorsRegistry registry) {
CorsConfiguration configuration = this.corsProperties.toCorsConfiguration();
if (configuration != null) {
registry.addMapping(this.graphQlProperties.getPath()).combine(configuration);
registry.addMapping(this.graphQlProperties.getHttp().getPath()).combine(configuration);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ void shouldContributeDefaultBeans() {

@Test
void shouldConfigureSseTimeout() {
this.contextRunner.withPropertyValues("spring.graphql.sse.timeout=10s").run((context) -> {
this.contextRunner.withPropertyValues("spring.graphql.http.sse.timeout=10s").run((context) -> {
assertThat(context).hasSingleBean(GraphQlSseHandler.class);
GraphQlSseHandler handler = context.getBean(GraphQlSseHandler.class);
assertThat(handler).hasFieldOrPropertyWithValue("timeout", Duration.ofSeconds(10));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ are detected by Spring Boot and considered as candidates for javadoc:graphql.sch

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[].
The path can be customized with configprop:spring.graphql.http.path[].

TIP: The HTTP endpoint for both Spring MVC and Spring WebFlux is provided by a `RouterFunction` bean with an javadoc:org.springframework.core.annotation.Order[format=annotation] of `0`.
If you define your own `RouterFunction` beans, you may want to add appropriate javadoc:org.springframework.core.annotation.Order[format=annotation] annotations to ensure that they are sorted correctly.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public class HttpGraphQlTesterAutoConfiguration {
@ConditionalOnBean(WebTestClient.class)
@ConditionalOnMissingBean
public HttpGraphQlTester webTestClientGraphQlTester(WebTestClient webTestClient, GraphQlProperties properties) {
WebTestClient mutatedWebTestClient = webTestClient.mutate().baseUrl(properties.getPath()).build();
WebTestClient mutatedWebTestClient = webTestClient.mutate().baseUrl(properties.getHttp().getPath()).build();
return HttpGraphQlTester.create(mutatedWebTestClient);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ private String deduceBasePath() {
}

private String findConfiguredGraphQlPath() {
String configuredPath = this.applicationContext.getEnvironment().getProperty("spring.graphql.path");
String configuredPath = this.applicationContext.getEnvironment().getProperty("spring.graphql.http.path");
return StringUtils.hasText(configuredPath) ? configuredPath : "/graphql";
}

Expand Down

0 comments on commit 83f678a

Please sign in to comment.