Ответ 401 Spring RestTemplate отредактирован (содержит [нет тела]), хотя у него есть тело

Справочная информация. В рамках цикла запроса API, скажем, для проверки OTP, мы вызываем внешний API (xxxx.com), который возвращает 401, если OTP недействителен, вместе с ответом JSON.

Мы используем Spring RestTemplate для внешних вызовов xxxx.com и хотим захватить ответ 401 как объект Java, но нам не удается это сделать. Мы видим, что ответ отредактирован.

Используемый нами RestTemplate взят из JAR org.springframework:spring-web:6.1.5

Линия, которая осуществляет вызовы:

this.restTemplate.exchange("https://xxxx.com", HttpMethod.PUT, entity, responseType);

Как мы захватываем не-2XX:

catch (HttpClientErrorException e) {
    if (successfulHttpStatuses.contains(e.getStatusCode())) {
        return e.getResponseBodyAs(responseType);
    } else {
        log.error("{} returned a {} status - {}", clientName, e.getStatusCode(), e.getMessage());
        throw e;
    }

Наш клиент отлично работает для 2XX, только во время 401 мы получаем журнал,

XXXClient returned a 401 status - [no body]

где, как мы ожидаем, ответ будет иметь тело.

Посмотрите это: Изображение: Показан запрос через Postman, возвращается ответ на номер 401

Кроме того, внешний сайт xxxx.com, о котором мы упоминали, использует аутентификацию по носителю и также выдает 401 в случае неверного JWT.

Заголовки ответов не содержат «WWW-Authenticate», мы предполагаем, что используемый нами JWT является действительным JWT

Итак, я считаю, что это намеренное дизайнерское решение, призванное замаскировать ответы 401 по соображениям безопасности. Есть ли обходной путь для этой ситуации?

Можете ли вы предоставить более подробную информацию о том, как вы устанавливаете токен носителя в заголовке запроса?

Oussama BASRY 12.04.2024 19:21

Кажется, это проблема не с вашим кодом входа, а скорее с ответом. Заглянув в код здесь org.springframework.web.client.DefaultResponseErrorHandler#g‌​etErrorMessage, вы увидите, что [ no body ] возвращается, когда тело ответа пусто.

LostKatana 13.04.2024 00:25
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
0
2
374
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Как уже упоминалось в моем комментарии, похоже, это проблема, поскольку ответ действительно пуст. Я собрал два теста, чтобы доказать свою точку зрения.

@Test
public void unauthorizedRequestWithBodyTest() {

    this.server
            .expect(ExpectedCount.once(), requestTo("/unauthorized"))
            .andExpect(method(HttpMethod.PUT))
            .andRespond(withStatus(HttpStatus.UNAUTHORIZED).body(
                    " { \"message\" : \"Something is broken\", \"status\": \"401\" } "));

    try {
        restTemplate.exchange("unauthorized", HttpMethod.PUT, null, String.class);
    } catch (HttpClientErrorException e) {
        System.out.println("Response with body content:");
        System.out.printf("%s returned a %s status - %s\n\n", "clientName", e.getStatusCode(), e.getResponseBodyAsString());
    }
}

@Test
public void unauthorizedRequestWithoutBodyTest() {

    this.server
            .expect(ExpectedCount.once(), requestTo("/unauthorized"))
            .andExpect(method(HttpMethod.PUT))
            .andRespond(withStatus(HttpStatus.UNAUTHORIZED));

    try {
        restTemplate.exchange("unauthorized", HttpMethod.PUT, null, String.class);
    } catch (HttpClientErrorException e) {
        System.out.println("Response without body content:");
        System.out.printf("%s returned a %s status - %s\n", "clientName", e.getStatusCode(), e.getMessage());
        System.out.printf("%s returned a %s status - %s\n\n", "clientName", e.getStatusCode(), e.getResponseBodyAsString());
    }
}

Результат следующий:

Response with body content:
clientName returned a 401 UNAUTHORIZED status -  { "message" : "Something is broken", "status": "401" } 

Response without body content:
clientName returned a 401 UNAUTHORIZED status - 401 Unauthorized: [no body]
clientName returned a 401 UNAUTHORIZED status - 

Из реализации вы можете видеть, что метод getErrorMessage() возвращает текст [no body], когда тело ответа пусто.

if (ObjectUtils.isEmpty(responseBody)) {
    return preface + "[no body]";
}

org.springframework.web.client.DefaultResponseErrorHandler#getErrorMessage

спасибо за это глубокое погружение и, указав на DefaultResponseErrorHandler, я попробовал протестировать аналогичные заметки. Не могли бы вы дать мне информацию о том, почему это дало ответ, если я повторил тот же запрос с почтальоном (изображения в вопросе выше)

thekinggpin 15.04.2024 17:26

Также @LostKatana Я попробовал реализовать тот же сценарий, развернув два приложения локально. Приложение A имело конечную точку, похожую на unauthorizedRequestWithBodyTest() Приложение B записывало ответ. В этом случае, что интересно, я получил [Нет тела]

thekinggpin 15.04.2024 17:30

Насчет вашего первого комментария со ссылкой на скриншот я не могу сказать наверняка, но возможно у почтальона другая реализация и поэтому он показывает тело. Что касается вашего второго комментария, я не уверен, правильно ли я понял.

LostKatana 16.04.2024 19:20
Ответ принят как подходящий

Я глубже изучил проблему 401 [нет тела],

Я видел, что реализация Spring по умолчанию RestTemplate инициализирует свой конструктор с помощью org.springframework.http.client.SimpleClientHttpRequestFactory, который основан на реализации JDK по умолчанию (java.net), которая выдает исключение java.net.HttpRetryException отсюда.

Смотрите java.net.HttpURLConnection#HTTP_UNAUTHORIZED.

Итак, метод org.springframework.web.client.DefaultResponseErrorHandler#getResponseBody через Spring игнорирует все исключения и, таким образом, возвращает пустой массив байтов, даже если у нас есть тело ответа.

В качестве исправления мне пришлось использовать реализацию Apache HttpClient, используя вместо этого org.springframework.http.client.HttpComponentsClientHttpRequestFactory, сделав это

restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory());

Это влияет на все операции потокового режима, такие как POST, PUT, где мы обычно не получаем длину содержимого в заголовке ответа.

Использованная литература: https://github.com/spring-projects/spring-boot/issues/40359

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