Асинхронные запросы иногда завершаются неудачно, поскольку RequestFacade перерабатывается.
У меня есть следующее представление, которое асинхронно загружает данные:
@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);
});
}
}
Сервис просто передает правильный запрос моему клиенту GraphQL:
@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);
}
}
И GraphQlService наконец отправляет запрос через Spring HttpGraphQlClient. Этот клиент необходимо настроить для аутентификации OAuth2, поскольку конечная точка GraphQL защищена как сервер ресурсов.
@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);
}
}
Локально это работает в 100% случаев, но на наших тестовых серверах случайным образом, может быть, примерно в 50% случаев, выдается это исключение:
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]
Я подозреваю, что между отправкой запроса GraphQL и очисткой HTTP-запроса возникает какая-то гонка. Но я не могу понять, как предотвратить повторную обработку HTTP-запроса.
Я сравнил свой код с этим вебинаром на YouTube: https://www.youtube.com/watch?v=7Mr_pQc_rxA и соответствующей базой кода здесь: https://github.com/eriklumme/reactive-vaadin-demo. Он немного старше, но кажется в основном идентичным (без аутентификации OAuth2 и использования GraphQL, но основан на реактивном Monos).
Я также попробовал GraphQlService, который не полагается на HTTP-запросы, который работает при этой ошибке, но предотвращает автоматическое обновление токенов доступа OAuth2:
@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);
}
}
Я бы предпочел версию на основе HTTP-запроса (поскольку это реальная среда, в которой работает этот код), но я бы согласился на обновление токена доступа, работающее с моим обходным решением.
РЕДАКТИРОВАТЬ: после дальнейшего изучения этого вопроса я считаю, что процесс авторизации OAuth2 запрашивает параметр области OAuth2 из устаревшего запроса. Я заменил реализацию по умолчаниюDefaultOAuth2AuthorizedClientManager.DefaultContextAttributesMapper
с пустым (так как в данный момент мы не используем области видимости). Похоже, что дальнейших вызовов сохраненного запроса не происходит, так что это может нам подойти. Я буду исследовать дальше и/или улучшать решение «грязного взлома», которое у меня есть сейчас. Из-за длинных выходных здесь я вернусь только в понедельник.