Асинхронные запросы иногда завершаются неудачно, поскольку 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с пустым (так как в данный момент мы не используем области видимости). Похоже, что дальнейших вызовов сохраненного запроса не происходит, так что это может нам подойти. Я буду исследовать дальше и/или улучшать решение «грязного взлома», которое у меня есть сейчас. Из-за длинных выходных здесь я вернусь только в понедельник.

0 ответов

Другие вопросы по тегам