проблема с Spring и асинхронным контроллером + HandlerInterceptor + IE / Edge - PullRequest
0 голосов
/ 10 октября 2018

Я работаю над приложением Spring, которое обслуживает конечные точки REST.Одна из конечных точек, по сути, действует как прокси-сервер между клиентом HTML и сторонним поставщиком облачного хранилища.Эта конечная точка извлекает файлы из провайдера хранилища и передает их обратно клиенту.Что-то вроде следующего (обратите внимание, что существует синхронная и асинхронная версия одной и той же конечной точки):

@Controller
public class CloudStorageController {

  ...    

  @RequestMapping(value = "/fetch-image/{id}", method = RequestMethod.GET, produces = MediaType.IMAGE_JPEG_VALUE)
  public ResponseEntity<byte[]> fetchImageSynchronous(@PathVariable final Long id) {
    final byte[] imageFileContents = this.fetchImage(id);
    return ResponseEntity.ok().body(imageFileContents);
  }

  @RequestMapping(value = "/fetch-image-async/{id}", method = RequestMethod.GET, produces = MediaType.IMAGE_JPEG_VALUE)
  public Callable<ResponseEntity<byte[]>> fetchImageAsynchronous(@PathVariable final Long id) {
    return () -> {
      final byte[] imageFileContents = this.fetchImage(id);
      return ResponseEntity.ok().body(imageFileContents);
    };
  }

  private byte[] fetchImage(final long id) {
    // fetch the file from cloud storage and return as byte array
    ...
  }

  ...

}

Из-за характера клиентского приложения (HTML5 + ajax) и способа использования этой конечной точки, аутентификация пользователяподается на эту конечную точку иначе, чем на другие конечные точки.Для этого был разработан HandlerInterceptor для проверки подлинности для этой конечной точки:

@Component("cloudStorageAuthenticationInterceptor")
public class CloudStorageAuthenticationInterceptor extends HandlerInterceptorAdapter {

  @Override
  public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler) {
    // examine the request for the authentication information and verify it
    final Authentication authenticated = ...
    if (authenticated == null) {
      try {
        pResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
      } catch (IOException e) {
        throw new RuntimeException(e);
      }
      return false;
    }
    else {
      try {
        request.login(authenticated.getName(), (String) authenticated.getCredentials());
      } catch (final ServletException e) {
        throw new BadCredentialsException("Bad credentials");
      }
    }
    return true;
  }

}

Перехватчик регистрируется следующим образом:

@Configuration
@EnableWebMvc
public class ApiConfig extends WebMvcConfigurerAdapter {

  @Autowired
  @Qualifier("cloudStorageAuthenticationInterceptor")
  private HandlerInterceptor cloudStorageAuthenticationInterceptor;

  @Override
  public void addInterceptors(final InterceptorRegistry registry) {
    registry.addInterceptor(this.cloudStorageAuthenticationInterceptor)
        .addPathPatterns(
            "/fetch-image/**",
            "/fetch-image-async/**"
        );
  }

  @Override
  public void configureAsyncSupport(final AsyncSupportConfigurer configurer) {
    final ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(this.asyncThreadPoolCoreSize);
    executor.setMaxPoolSize(this.asyncThreadPoolMaxSize);
    executor.setQueueCapacity(this.asyncThreadPoolQueueCapacity);
    executor.setThreadNamePrefix(this.asyncThreadPoolPrefix);
    executor.initialize();
    configurer.setTaskExecutor(executor);
    super.configureAsyncSupport(configurer);
  }

}

В идеале выборка изображения должна выполняться асинхронно(используя конечную точку / fetch-image-asyc / {id}), поскольку она должна вызывать стороннюю веб-службу, которая может иметь некоторую задержку.

Синхронная конечная точка (/ fetch-image / {id}) работает правильно для всех браузеров.Однако, если используется асинхронная конечная точка (/ fetch-image-async / {id}), Chrome и Firefox работают как положено.

Однако, если клиент является Microsoft IE или Microsoft Edge, нам кажется странным поведение,Конечная точка вызывается правильно, и ответ успешно отправлен (по крайней мере, с точки зрения сервера).Однако, похоже, что браузер ждет чего-то дополнительного.В окне IE / Edge DevTools сетевой запрос изображения отображается как ожидающий в течение 30 секунд, затем кажется, что время ожидания истекло, обновления успешно выполнены, и изображение успешно отображается.Также кажется, что соединение с сервером все еще открыто, поскольку ресурсы на стороне сервера, такие как соединения с базой данных, не освобождаются.В других браузерах асинхронный ответ принимается и обрабатывается в секунду или меньше.

Если я удаляю HandlerInterceptor и просто подключаю некоторые учетные данные для отладки, поведение исчезает.Так что, похоже, это как-то связано с взаимодействием между HandlerInterceptor и методом асинхронного контроллера и демонстрируется только для некоторых клиентов.

У кого-нибудь есть предложение о том, почему семантика IE / Edge вызывает такое поведение?

Ответы [ 2 ]

0 голосов
/ 12 октября 2018

После долгой трассировки на сервере и чтения комментариев JavaDocs для AsyncHandlerInterceptor я смог решить эту проблему.Для запросов к асинхронным методам контроллера метод preHandle любого перехватчика вызывается дважды.Он вызывается до того, как запрос передается сервлету, обрабатывающему запрос, и снова после того, как сервлет обработал запрос.В моем случае перехватчик пытался аутентифицировать запрос для обоих сценариев (до и после обработки запроса).Поставщик проверки подлинности приложения проверяет учетные данные в базе данных.По какой-то причине, если клиентом является IE или Edge, провайдеру аутентификации не удалось получить соединение с базой данных при вызове из preHandle в перехватчике после того, как сервлет обработал запрос.Будет выдано следующее исключение:

ERROR o.a.c.c.C.[.[.[.[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.dao.DataAccessResourceFailureException: Could not open connection; nested exception is org.hibernate.exception.JDBCConnectionException: Could not open connection] with root cause
java.sql.SQLTransientConnectionException: HikariPool-0 - Connection is not available, request timed out after 30001ms.

Таким образом, сервлет успешно обработает запрос и отправит ответ, но фильтр зависнет на 30 секунд, ожидая истечения времени ожидания соединения с базой данных при постобработкеcall to preHandle.

Поэтому для меня простое решение было добавить проверку в preHandle, если она вызывается после того, как сервлет уже обработал запрос.Я обновил метод preHandle следующим образом:

@Override
public boolean preHandle(final HttpServletRequest pRequest, final HttpServletResponse pResponse, final Object pHandler) {
  if (pRequest.getDispatcherType().equals(DispatcherType.REQUEST)) {
    ... perform authentication ...
  }
  return true;
}

Это решило проблему для меня.Это не объясняет все (то есть, почему только IE / Edge может вызвать проблему), но кажется, что preHandle должен выполнять работу только до того, как сервлет все равно обработает запрос.

0 голосов
/ 12 октября 2018

Судя по вашему описанию, при использовании IE или Edge существуют некоторые другие способы поведения

  • , кажется, что браузер ожидает чего-то дополнительного
  • соединение кажется открытым
  • работает нормально, если удалить HandlerInterceptor и использовать жесткий код в логике аутентификации

Для первого поведения я бы предложил вам использовать fiddler для трассировки всех httpзапросы .Было бы лучше, если бы вы могли сравнить два разных действия с помощью fiddler (1) запустить на Chrome, 2) запустить на краю).Внимательно проверьте все заголовки http в запросах и ответах, чтобы увидеть, есть ли какая-то другая часть.Что касается других вариантов поведения, я бы посоветовал вам написать логи, чтобы определить, какая часть тратит больше всего времени.Он предоставит вам полезную информацию для устранения неполадок.

...