Reputation: 45
I have the following view, which loads its data asynchronously:
@Route(value = NewsAdminView.ROUTE)
@RequiredArgsConstructor
public class NewsAdminView extends VerticalLayout {
public static final String ROUTE = "";
private final Grid<News> grid = new Grid<>(News.class);
private final NewsService service;
@Override
protected void onAttach(final AttachEvent attachEvent) {
if (attachEvent.isInitialAttach()) {
addComponents();
final UI ui = attachEvent.getUI();
service.getNews().subscribe(items -> updateItems(ui, items), t -> GenericNotifications.graphQlError(ui, t));
}
}
private void updateItems(final UI ui, final List<News> news) {
ui.access(() -> {
grid.setItems(this.newsList);
grid.setEnabled(true);
});
}
}
The service simply passes the correct query to my GraphQL client:
@Service
@RequiredArgsConstructor
public class NewsService {
private final GraphQlService graphQlService;
public Mono<List<News>> getNews() {
final String query = """
query NewsQuery {
allNews {
author
body
title
}
}""";
return graphQlService.queryList(query, "allNews", News.class);
}
}
And the GraphQlService finally sends the request via a Spring HttpGraphQlClient. This client needs to be configured for OAuth2 authentication, as the GraphQL endpoint is protected as a resource server.
@Service
public class GraphQlService {
private final HttpGraphQlClient client;
public GraphQlService(final WebClient.Builder webClientBuilder,
final ClientRegistrationRepository clientRegistrationRepository,
final OAuth2AuthorizedClientRepository oAuth2AuthorizedClientRepository,
final @Value("${graphql.baseUrl}") String baseUrl) {
final var oauth2Client = new ServletOAuth2AuthorizedClientExchangeFilterFunction(
clientRegistrationRepository, oAuth2AuthorizedClientRepository);
webClientBuilder.apply(oauth2Client.oauth2Configuration())
.defaultRequest(request -> request.attributes(clientRegistrationId("custom")));
client = HttpGraphQlClient.builder(webClientBuilder.build()).url(baseUrl).build();
}
@NotNull
public <T> Mono<List<T>> queryList(final String query, final String jsonPath, final Class<T> typeRef) {
log.debug("Running GraphQL list query for allNews, expecting News objects");
return client.document(query).variables(arguments).retrieve(jsonPath).toEntityList(typeRef);
}
}
Locally this works 100% of the time, but on our test servers it randomly, maybe about 50% of the time, produces this exception:
DEBUG 13997 --- [io-10112-exec-2] service.GraphQlService : Running GraphQL list query for allNews, expecting News objects
ERROR 13997 --- [oundedElastic-2] reactor.core.publisher.Operators: Operator called default onErrorDropped
reactor.core.Exceptions$ErrorCallbackNotImplemented: org.springframework.graphql.client.GraphQlTransportException: GraphQlTransport error: The request object has been recycled and is no longer associated wi
th this facade
Caused by: org.springframework.graphql.client.GraphQlTransportException: GraphQlTransport error: The request object has been recycled and is no longer associated with this facade
at org.springframework.graphql.client.DefaultGraphQlClient$DefaultRequestSpec.lambda$execute$1(DefaultGraphQlClient.java:161) ~[spring-graphql-1.1.3.jar!/:1.1.3]
at reactor.core.publisher.Mono.lambda$onErrorResume$29(Mono.java:3849) ~[reactor-core-3.5.5.jar!/:3.5.5]
at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onError(FluxOnErrorResume.java:94) ~[reactor-core-3.5.5.jar!/:3.5.5]
at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onError(FluxMapFuseable.java:142) ~[reactor-core-3.5.5.jar!/:3.5.5]
at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onError(FluxMapFuseable.java:142) ~[reactor-core-3.5.5.jar!/:3.5.5]
at reactor.core.publisher.MonoFlatMap$FlatMapMain.onError(MonoFlatMap.java:180) ~[reactor-core-3.5.5.jar!/:3.5.5]
at reactor.core.publisher.FluxContextWrite$ContextWriteSubscriber.onError(FluxContextWrite.java:121) ~[reactor-core-3.5.5.jar!/:3.5.5]
at reactor.core.publisher.FluxDoFinally$DoFinallySubscriber.onError(FluxDoFinally.java:119) ~[reactor-core-3.5.5.jar!/:3.5.5]
at reactor.core.publisher.MonoPeekTerminal$MonoTerminalPeekSubscriber.onError(MonoPeekTerminal.java:258) ~[reactor-core-3.5.5.jar!/:3.5.5]
at reactor.core.publisher.FluxPeekFuseable$PeekConditionalSubscriber.onError(FluxPeekFuseable.java:903) ~[reactor-core-3.5.5.jar!/:3.5.5]
at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.onError(Operators.java:2210) ~[reactor-core-3.5.5.jar!/:3.5.5]
at reactor.core.publisher.FluxOnAssembly$OnAssemblySubscriber.onError(FluxOnAssembly.java:544) ~[reactor-core-3.5.5.jar!/:3.5.5]
at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.onError(Operators.java:2210) ~[reactor-core-3.5.5.jar!/:3.5.5]
at reactor.core.publisher.MonoFlatMap$FlatMapMain.onError(MonoFlatMap.java:180) ~[reactor-core-3.5.5.jar!/:3.5.5]
at reactor.core.publisher.FluxMap$MapSubscriber.onError(FluxMap.java:134) ~[reactor-core-3.5.5.jar!/:3.5.5]
at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.onError(Operators.java:2210) ~[reactor-core-3.5.5.jar!/:3.5.5]
at reactor.core.publisher.MonoFlatMap$FlatMapMain.secondError(MonoFlatMap.java:241) ~[reactor-core-3.5.5.jar!/:3.5.5]
at reactor.core.publisher.MonoFlatMap$FlatMapInner.onError(MonoFlatMap.java:315) ~[reactor-core-3.5.5.jar!/:3.5.5]
at reactor.core.publisher.FluxSubscribeOnCallable$CallableSubscribeOnSubscription.run(FluxSubscribeOnCallable.java:230) ~[reactor-core-3.5.5.jar!/:3.5.5]
at reactor.core.scheduler.SchedulerTask.call(SchedulerTask.java:68) ~[reactor-core-3.5.5.jar!/:3.5.5]
at reactor.core.scheduler.SchedulerTask.call(SchedulerTask.java:28) ~[reactor-core-3.5.5.jar!/:3.5.5]
at java.base/java.util.concurrent.FutureTask.run(Unknown Source) ~[na:na]
at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(Unknown Source) ~[na:na]
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source) ~[na:na]
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source) ~[na:na]
at java.base/java.lang.Thread.run(Unknown Source) ~[na:na]
Caused by: java.lang.IllegalStateException: The request object has been recycled and is no longer associated with this facade
at org.apache.catalina.connector.RequestFacade.checkFacade(RequestFacade.java:856) ~[tomcat-embed-core-10.1.8.jar!/:na]
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
*__checkpoint ⇢ Request to POST null [DefaultWebClient]
Original Stack Trace:
at org.apache.catalina.connector.RequestFacade.checkFacade(RequestFacade.java:856) ~[tomcat-embed-core-10.1.8.jar!/:na]
at org.apache.catalina.connector.RequestFacade.getParameter(RequestFacade.java:304) ~[tomcat-embed-core-10.1.8.jar!/:na]
at jakarta.servlet.ServletRequestWrapper.getParameter(ServletRequestWrapper.java:149) ~[tomcat-embed-core-10.1.8.jar!/:na]
at org.springframework.security.web.firewall.StrictHttpFirewall$StrictFirewalledRequest.getParameter(StrictHttpFirewall.java:769) ~[spring-security-web-6.0.3.jar!/:6.0.3]
at jakarta.servlet.ServletRequestWrapper.getParameter(ServletRequestWrapper.java:149) ~[tomcat-embed-core-10.1.8.jar!/:na]
at jakarta.servlet.ServletRequestWrapper.getParameter(ServletRequestWrapper.java:149) ~[tomcat-embed-core-10.1.8.jar!/:na]
at org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager$DefaultContextAttributesMapper.apply(DefaultOAuth2AuthorizedClientManager.java:298) ~[spring-security-oauth2-client-6.0.3.jar!/:6.0.3]
at org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager$DefaultContextAttributesMapper.apply(DefaultOAuth2AuthorizedClientManager.java:291) ~[spring-security-oauth2-client-6.0.3.jar!/:6.0.3]
at org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager.lambda$authorize$2(DefaultOAuth2AuthorizedClientManager.java:168) ~[spring-security-oauth2-client-6.0.3.jar!/:6.0.3]
at org.springframework.security.oauth2.client.OAuth2AuthorizationContext$Builder.attributes(OAuth2AuthorizationContext.java:185) ~[spring-security-oauth2-client-6.0.3.jar!/:6.0.3]
at org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager.authorize(DefaultOAuth2AuthorizedClientManager.java:167) ~[spring-security-oauth2-client-6.0.3.jar!/:6.0.3]
at org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.lambda$authorizeClient$22(ServletOAuth2AuthorizedClientExchangeFilterFunction.java:485) ~[spring-security-oauth2-client-6.0.3.jar!/:6.0.3]
at reactor.core.publisher.MonoSupplier.call(MonoSupplier.java:67) ~[reactor-core-3.5.5.jar!/:3.5.5]
at reactor.core.publisher.FluxSubscribeOnCallable$CallableSubscribeOnSubscription.run(FluxSubscribeOnCallable.java:227) ~[reactor-core-3.5.5.jar!/:3.5.5]
at reactor.core.scheduler.SchedulerTask.call(SchedulerTask.java:68) ~[reactor-core-3.5.5.jar!/:3.5.5]
at reactor.core.scheduler.SchedulerTask.call(SchedulerTask.java:28) ~[reactor-core-3.5.5.jar!/:3.5.5]
at java.base/java.util.concurrent.FutureTask.run(Unknown Source) ~[na:na]
at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(Unknown Source) ~[na:na]
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source) ~[na:na]
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source) ~[na:na]
at java.base/java.lang.Thread.run(Unknown Source) ~[na:na]
I suspect some kind of race condition between the GraphQL query being sent and the HTTP request being cleaned up. But I can't figure out how to prevent the HTTP request from being recycled.
I compared my code to this Webinar on YouTube: https://www.youtube.com/watch?v=7Mr_pQc_rxA and its corresponding code base here: https://github.com/eriklumme/reactive-vaadin-demo. It is a little bit older, but seems to be basically identical (sans the OAuth2 authentication and usage of GraphQL, but which is based on reactive Monos).
I have aslo tried a GraphQlService that does not rely on HTTP requests, which works for this error, but prevents the automatic renewal of OAuth2 access tokens:
@Service
public class NoHttpRequestGraphQlService {
private final HttpGraphQlClient client;
public NoHttpRequestGraphQlService(final WebClient.Builder webClientBuilder,
final ClientRegistrationRepository clientRegistrationRepository,
final OAuth2AuthorizedClientService oAuth2AuthorizedClientService,
final @Value("${graphql.baseUrl}") String baseUrl) {
final var authorizedClientManager = new AuthorizedClientServiceOAuth2AuthorizedClientManager(clientRegistrationRepository, oAuth2AuthorizedClientService);
final var oauth2Client = new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
final var authorizationFailureHandler = new RemoveAuthorizedClientOAuth2AuthorizationFailureHandler(
(clientRegistrationId, principal, attributes) -> oAuth2AuthorizedClientService.removeAuthorizedClient(clientRegistrationId, principal.getName()));
authorizedClientManager.setAuthorizationFailureHandler(authorizationFailureHandler);
oauth2Client.setAuthorizationFailureHandler(authorizationFailureHandler);
webClientBuilder.apply(oauth2Client.oauth2Configuration())
.defaultRequest(request -> request.attributes(clientRegistrationId("custom")));
client = HttpGraphQlClient.builder(webClientBuilder.build()).url(baseUrl).build();
}
@NotNull
public <T> Mono<List<T>> queryList(final String query, final String jsonPath, final Class<T> typeRef) {
log.debug("Running GraphQL list query for allNews, expecting News objects");
return client.document(query).variables(arguments).retrieve(jsonPath).toEntityList(typeRef);
}
}
I would prefer the HTTP request based version (as that is the actual environment where this code runs), but I would settle for the access token renewal working with my workaround.
EDIT: After looking into this further, I believe the OAuth2 authorization process is requesting the OAuth2 scope parameter from a stale request. I replaced the default implementation DefaultOAuth2AuthorizedClientManager.DefaultContextAttributesMapper
with an empty one (as we are not using scopes at the moment). There seem to be no further calls to the stored request, so this might work for us. I'll investigate further and/or improve the 'dirty hack' solution I have now. Due to the long weekend here I'll only be back on monday.
Upvotes: 2
Views: 1981