Skip to content

Commit 028dd64

Browse files
authored
fix: update subs callback to SpringBoot 3.3 (#404)
1 parent 2b17eaf commit 028dd64

File tree

12 files changed

+217
-390
lines changed

12 files changed

+217
-390
lines changed

gradle.properties

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ version = 3.0-SNAPSHOT
33

44
# dependencies
55
annotationsVersion = 24.1.0
6-
graphQLJavaVersion = 22.0
6+
graphQLJavaVersion = 22.1
77
mockWebServerVersion = 4.12.0
88
protobufVersion = 4.27.0
99
slf4jVersion = 2.0.13
10-
springBootVersion = 3.3.0-RC1
11-
springGraphQLVersion = 1.3.0-RC1
10+
springBootVersion = 3.3.0
11+
springGraphQLVersion = 1.3.0
1212
reactorVersion = 3.6.6
1313

1414
# test dependencies

spring-subscription-callback/README.md

Lines changed: 26 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,10 @@ implementation("com.apollographql.federation:federation-spring-subscription-call
5959

6060
## Usage
6161

62-
In order to enable HTTP subscription callback protocol support you need to configure `CallbackGraphQlHttpHandler` bean in your
63-
application context.
62+
In order to enable HTTP subscription callback protocol support you need to configure `SubscriptionCallbackHandler` and
63+
`CallbackWebGraphQLInterceptor` beans in your application context.
6464

65-
This library provides support for both WebMVC and WebFlux applications so make sure to instantiate correct flavor of protocol.
66-
67-
* `com.apollographql.subscription.webmvc.CallbackGraphQlHttpHandler` for WebMVC applications
68-
* `com.apollographql.subscription.webflux.CallbackGraphQlHttpHandler` for WebFlux applications
65+
`CallbackWebGraphQLInterceptor` works with both WebMVC and WebFlux applications.
6966

7067
Given a subscription
7168

@@ -88,32 +85,30 @@ We can enable subscription HTTP callback support using following configuration
8885
public class GraphQLConfiguration {
8986

9087
@Bean
91-
public GraphQlHttpHandler graphQlHttpHandler(WebGraphQlHandler webGraphQlHandler) {
92-
return new CallbackGraphQlHttpHandler(webGraphQlHandler);
88+
public SubscriptionCallbackHandler callbackHandler(ExecutionGraphQlService graphQlService) {
89+
return new SubscriptionCallbackHandler(graphQlService);
90+
}
91+
92+
// This interceptor defaults to Ordered#LOWEST_PRECEDENCE order as it should run last in chain
93+
// to allow users to still apply other interceptors that handle common stuff (e.g. extracting
94+
// auth headers, etc).
95+
// You can override this behavior by specifying custom order.
96+
@Bean
97+
public CallbackWebGraphQLInterceptor callbackGraphQlInterceptor(
98+
SubscriptionCallbackHandler callbackHandler) {
99+
return new CallbackWebGraphQLInterceptor(callbackHandler);
100+
}
101+
102+
// regular federation transforms
103+
// see https://docs.spring.io/spring-graphql/reference/federation.html
104+
@Bean
105+
public GraphQlSourceBuilderCustomizer customizer(FederationSchemaFactory factory) {
106+
return builder -> builder.schemaFactory(factory::createGraphQLSchema);
93107
}
94108

95-
// regular federation transform
96109
@Bean
97-
public GraphQlSourceBuilderCustomizer federationTransform() {
98-
DataFetcher<?> entityDataFetcher =
99-
env -> {
100-
List<Map<String, Object>> representations = env.getArgument(_Entity.argumentName);
101-
return representations.stream()
102-
.map(
103-
representation -> {
104-
// TODO implement entity data fetcher logic here
105-
return null;
106-
})
107-
.collect(Collectors.toList());
108-
};
109-
110-
return builder ->
111-
builder.schemaFactory(
112-
(registry, wiring) ->
113-
Federation.transform(registry, wiring)
114-
.fetchEntities(entityDataFetcher)
115-
.resolveEntityType(new ClassNameTypeResolver())
116-
.build());
110+
FederationSchemaFactory federationSchemaFactory() {
111+
return new FederationSchemaFactory();
117112
}
118113
}
119114
```
@@ -124,9 +119,8 @@ your provided scheduler.
124119

125120
```java
126121
@Bean
127-
public GraphQlHttpHandler graphQlHttpHandler(WebGraphQlHandler webGraphQlHandler) {
122+
public SubscriptionCallbackHandler callbackHandler(ExecutionGraphQlService graphQlService) {
128123
Scheduler customScheduler = <provide your custom scheduler>;
129-
SubscriptionCallbackHandler subscriptionHandler = new SubscriptionCallbackHandler(webGraphQlHandler, customScheduler);
130-
return new CallbackGraphQlHttpHandler(webGraphQlHandler, subscriptionHandler);
124+
return new SubscriptionCallbackHandler(graphQlService, customScheduler);
131125
}
132126
```

spring-subscription-callback/build.gradle.kts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,6 @@ plugins {
44
id("com.apollographql.federation.java-conventions")
55
}
66

7-
repositories {
8-
mavenCentral()
9-
maven {
10-
url = uri("https://repo.spring.io/milestone")
11-
}
12-
}
13-
147
val annotationsVersion: String by project
158
val graphQLJavaVersion: String by project
169
val mockWebServerVersion: String by project
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package com.apollographql.subscription;
2+
3+
import static com.apollographql.subscription.callback.SubscriptionCallback.parseSubscriptionCallbackExtension;
4+
import static com.apollographql.subscription.callback.SubscriptionCallbackHandler.SUBSCRIPTION_PROTOCOL_HEADER;
5+
import static com.apollographql.subscription.callback.SubscriptionCallbackHandler.SUBSCRIPTION_PROTOCOL_HEADER_VALUE;
6+
7+
import com.apollographql.subscription.callback.SubscriptionCallbackHandler;
8+
import graphql.ExecutionResult;
9+
import graphql.GraphQLError;
10+
import org.apache.commons.logging.Log;
11+
import org.apache.commons.logging.LogFactory;
12+
import org.springframework.core.Ordered;
13+
import org.springframework.graphql.server.WebGraphQlInterceptor;
14+
import org.springframework.graphql.server.WebGraphQlRequest;
15+
import org.springframework.graphql.server.WebGraphQlResponse;
16+
import org.springframework.graphql.server.WebSocketGraphQlRequest;
17+
import org.springframework.graphql.support.DefaultExecutionGraphQlResponse;
18+
import org.springframework.http.HttpStatus;
19+
import org.springframework.web.server.ResponseStatusException;
20+
import reactor.core.publisher.Mono;
21+
22+
/**
23+
* Interceptor that provides support for Apollo Subscription Callback Protocol. This interceptor
24+
* defaults to {@link Ordered#LOWEST_PRECEDENCE} order as it should run last in chain to allow users
25+
* to still apply other interceptors that handle common stuff (e.g. extracting auth headers, etc).
26+
* You can override this behavior by specifying custom order.
27+
*
28+
* @see <a
29+
* href="https://www.apollographql.com/docs/router/executing-operations/subscription-callback-protocol">Subscription
30+
* Callback Protocol</a>
31+
*/
32+
public class CallbackWebGraphQLInterceptor implements WebGraphQlInterceptor, Ordered {
33+
34+
private static final Log logger = LogFactory.getLog(CallbackWebGraphQLInterceptor.class);
35+
36+
private final SubscriptionCallbackHandler subscriptionCallbackHandler;
37+
private final int order;
38+
39+
public CallbackWebGraphQLInterceptor(SubscriptionCallbackHandler subscriptionCallbackHandler) {
40+
this(subscriptionCallbackHandler, LOWEST_PRECEDENCE);
41+
}
42+
43+
public CallbackWebGraphQLInterceptor(
44+
SubscriptionCallbackHandler subscriptionCallbackHandler, int order) {
45+
this.subscriptionCallbackHandler = subscriptionCallbackHandler;
46+
this.order = order;
47+
}
48+
49+
@Override
50+
public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest request, Chain chain) {
51+
// in order to correctly handle parsing of ANY requests (i.e. it is valid to define a
52+
// document with query fragments first) we would need to parse it which is a much heavier
53+
// operation, we may opt to do it in the future releases
54+
if (!isWebSocketRequest(request) && request.getDocument().startsWith("subscription")) {
55+
return parseSubscriptionCallbackExtension(request.getExtensions())
56+
.flatMap(
57+
callback -> {
58+
if (logger.isDebugEnabled()) {
59+
logger.debug("Starting subscription using callback: " + callback);
60+
}
61+
return this.subscriptionCallbackHandler
62+
.handleSubscriptionUsingCallback(request, callback)
63+
.map(response -> callbackResponse(request, response));
64+
})
65+
.onErrorResume(
66+
(error) -> {
67+
if (logger.isErrorEnabled()) {
68+
logger.error("Unable to start subscription using callback protocol", error);
69+
}
70+
return Mono.error(new ResponseStatusException(HttpStatus.BAD_REQUEST));
71+
});
72+
} else {
73+
return chain.next(request);
74+
}
75+
}
76+
77+
private boolean isWebSocketRequest(WebGraphQlRequest request) {
78+
return request instanceof WebSocketGraphQlRequest;
79+
}
80+
81+
private WebGraphQlResponse callbackResponse(
82+
WebGraphQlRequest request, ExecutionResult callbackResult) {
83+
var callbackExecutionResponse =
84+
new DefaultExecutionGraphQlResponse(request.toExecutionInput(), callbackResult);
85+
var callbackGraphQLResponse = new WebGraphQlResponse(callbackExecutionResponse);
86+
callbackGraphQLResponse
87+
.getResponseHeaders()
88+
.add(SUBSCRIPTION_PROTOCOL_HEADER, SUBSCRIPTION_PROTOCOL_HEADER_VALUE);
89+
return callbackGraphQLResponse;
90+
}
91+
92+
private WebGraphQlResponse errorCallbackResponse(WebGraphQlRequest request) {
93+
var errorCallbackResult =
94+
ExecutionResult.newExecutionResult()
95+
.addError(
96+
GraphQLError.newError()
97+
.message("Unable to start subscription using callback protocol")
98+
.build())
99+
.build();
100+
return callbackResponse(request, errorCallbackResult);
101+
}
102+
103+
@Override
104+
public int getOrder() {
105+
return order;
106+
}
107+
}

spring-subscription-callback/src/main/java/com/apollographql/subscription/callback/SubscriptionCallbackHandler.java

Lines changed: 22 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.apollographql.subscription.callback;
22

3+
import com.apollographql.subscription.exception.CallbackInitializationFailedException;
34
import com.apollographql.subscription.exception.InactiveSubscriptionException;
45
import com.apollographql.subscription.message.CallbackMessageCheck;
56
import com.apollographql.subscription.message.CallbackMessageComplete;
@@ -14,7 +15,7 @@
1415
import org.apache.commons.logging.LogFactory;
1516
import org.jetbrains.annotations.NotNull;
1617
import org.reactivestreams.Publisher;
17-
import org.springframework.graphql.server.WebGraphQlHandler;
18+
import org.springframework.graphql.ExecutionGraphQlService;
1819
import org.springframework.graphql.server.WebGraphQlRequest;
1920
import org.springframework.http.MediaType;
2021
import org.springframework.web.reactive.function.client.WebClient;
@@ -30,20 +31,20 @@ public class SubscriptionCallbackHandler {
3031
public static final String SUBSCRIPTION_PROTOCOL_HEADER = "subscription-protocol";
3132
public static final String SUBSCRIPTION_PROTOCOL_HEADER_VALUE = "callback/1.0";
3233

33-
private final WebGraphQlHandler graphQlHandler;
34+
private final ExecutionGraphQlService graphQlService;
3435
private final Scheduler scheduler;
3536

36-
public SubscriptionCallbackHandler(WebGraphQlHandler graphQlHandler) {
37-
this(graphQlHandler, Schedulers.boundedElastic());
37+
public SubscriptionCallbackHandler(ExecutionGraphQlService graphQlService) {
38+
this(graphQlService, Schedulers.boundedElastic());
3839
}
3940

40-
public SubscriptionCallbackHandler(WebGraphQlHandler graphQlHandler, Scheduler scheduler) {
41-
this.graphQlHandler = graphQlHandler;
41+
public SubscriptionCallbackHandler(ExecutionGraphQlService graphQlService, Scheduler scheduler) {
42+
this.graphQlService = graphQlService;
4243
this.scheduler = scheduler;
4344
}
4445

4546
@NotNull
46-
public Mono<Boolean> handleSubscriptionUsingCallback(
47+
public Mono<ExecutionResult> handleSubscriptionUsingCallback(
4748
@NotNull WebGraphQlRequest graphQlRequest, @NotNull SubscriptionCallback callback) {
4849
if (logger.isDebugEnabled()) {
4950
logger.debug("Starting subscription callback: " + callback);
@@ -62,33 +63,31 @@ public Mono<Boolean> handleSubscriptionUsingCallback(
6263
.exchangeToMono(
6364
checkResponse -> {
6465
var responseStatusCode = checkResponse.statusCode();
65-
var subscriptionProtocol =
66-
checkResponse.headers().header(SUBSCRIPTION_PROTOCOL_HEADER);
66+
// var subscriptionProtocol =
67+
// checkResponse.headers().header(SUBSCRIPTION_PROTOCOL_HEADER);
6768

6869
if (responseStatusCode.is2xxSuccessful()) {
69-
// && !subscriptionProtocol.isEmpty() &&
70+
// && !subscriptionProtocol.isEmpty() &&
7071
// "callback".equals(subscriptionProtocol.get(0)))
7172
if (logger.isDebugEnabled()) {
7273
logger.debug("Subscription callback init successful: " + callback);
7374
}
7475

7576
Flux<SubscritionCallbackMessage> subscription =
7677
startSubscription(client, graphQlRequest, callback);
77-
return Mono.just(true)
78+
return Mono.just(emptyResult())
7879
.publishOn(scheduler)
7980
.doOnSubscribe((subscribed) -> subscription.subscribe());
8081
} else {
81-
if (logger.isWarnEnabled()) {
82-
logger.warn(
83-
"Subscription callback failed initialization: "
84-
+ callback
85-
+ ", server responded with: "
86-
+ responseStatusCode.value());
87-
}
88-
return Mono.just(false);
82+
return Mono.error(
83+
new CallbackInitializationFailedException(
84+
callback, responseStatusCode.value()));
8985
}
90-
})
91-
.onErrorReturn(false);
86+
});
87+
}
88+
89+
private ExecutionResult emptyResult() {
90+
return ExecutionResult.newExecutionResult().data(null).build();
9291
}
9392

9493
@NotNull
@@ -107,8 +106,8 @@ protected Flux<SubscritionCallbackMessage> startSubscription(
107106

108107
// subscription data flux
109108
Flux<SubscritionCallbackMessage> subscriptionFlux =
110-
this.graphQlHandler
111-
.handleRequest(graphQlRequest)
109+
this.graphQlService
110+
.execute(graphQlRequest)
112111
.flatMapMany(
113112
(subscriptionData) -> {
114113
Flux<Map<String, Object>> responseFlux;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.apollographql.subscription.exception;
2+
3+
import com.apollographql.subscription.callback.SubscriptionCallback;
4+
5+
/** Exception thrown when callback initialization fails. */
6+
public class CallbackInitializationFailedException extends RuntimeException {
7+
8+
public CallbackInitializationFailedException(SubscriptionCallback callback, int statusCode) {
9+
super(
10+
"Subscription callback failed initialization: "
11+
+ callback
12+
+ ", server responded with: "
13+
+ statusCode);
14+
}
15+
}

0 commit comments

Comments
 (0)