Правильно разобрать поле, содержащее '+' char - PullRequest
2 голосов
/ 22 марта 2019

Я сталкиваюсь со странной ситуацией, которую я воспроизвел в https://github.com/lgueye/uri-parameters-behavior

Поскольку мы перешли на spring-boot 2 ( spring framework 5 ), когдазапросив один из наших бэкэндов в методе GET, мы столкнулись со следующей ситуацией: все поля с символом + были изменены на (пробел) char, когда они достигли бэкэнда

Изменены следующие значения:

  • + 412386789 (номер телефона) в ** 412386789 **
  • 2019-03-22T17: 18: 39.621 + 02: 00 (java8 ZonedDateTime) в 2019-03-22T17: 18: 39.621 02: 00 (в результате org.springframework.validation.BindException

Я потратил довольно много времени на переполнение стека (https://github.com/spring-projects/spring-framework/issues/14464#issuecomment-453397378) и github (https://github.com/spring-projects/spring-framework/issues/21577)

IВнедрен и модульный тест mockMvc, и интеграционный тест

Модульный тест работает правильно Интеграционный тест не проходит (как в нашем производстве)

Может кто-нибудьили мне помочь исправить эту проблему?Очевидно, моя цель - пройти тест интеграции.

Спасибо за вашу помощь.

Луи

Ответы [ 2 ]

0 голосов
/ 25 марта 2019

Все смещение происходит из-за того, что существует нестандартная практика, как кодировать / декодировать пространство в "+".

Возможно, пространство может (сейчас) кодироваться в "+" или "%20".

Например, Google делает это со строками поиска:

https://www.google.com/search?q=test+my+space+delimited+entry

rfc1866, section-8.2.2 указывает, что часть запроса GET-запроса должна быть закодирована в 'application/x-www-form-urlencoded'.

Кодировка по умолчанию для всех форм - `application / x-www-form-
urlencoded '.Набор данных формы представлен в этом типе носителя следующим образом:
:

  1. Имена и значения полей формы экранируются: пробел символы заменяются на '+'.

С другой стороны, rfc3986 указывает, что пробелы в URL должны кодироваться с использованием "%20".

В основном это означает, что существуют разные стандарты для кодирования пробелов, в зависимости от того, где они находятся в компонентах синтаксиса URI .

     foo://example.com:8042/over/there?name=ferret#nose
     \_/   \______________/\_________/ \_________/ \__/
      |           |            |            |        |
   scheme     authority       path        query   fragment
      |   _____________________|__
     / \ /                        \
     urn:example:animal:ferret:nose

На основании этих замечаний мы можем утверждать, что в вызовах GET httpв URI:

  • пробелов до "?" необходимо кодировать в "%20"
  • пробелов после "?" в параметрах запроса нужно кодировать в "+"
  • , что означает, что "+" знаки должны быть закодированы в "%2B" в параметрах запроса

Реализация Spring соответствует спецификациям rfc, поэтому при отправке "+ 412386789« в параметрах запроса знак "+" интерпретируется как пробелchar и он получает к бэкэнду как "412386789" .

Глядя на:

final URI uri = UriComponentsBuilder.fromHttpUrl("http://localhost")
                                    .port(port)
                                    .path("/events")
                                    .queryParams(params)
                                    .build()
                                    .toUri();

Вы обнаружите, что:

"foo#bar@quizz+foo-bazz//quir."кодируется в "foo%23bar@quizz+foo-bazz//quir.", что соответствует спецификации (rfc3986).

Итак, если вы хотите, чтобы символ "+" в параметрах вашего запроса не интерпретировался как пробел, вынеобходимо закодировать его в "%2B".

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

   params.add("id", id);
   params.add("device", device);
   params.add("phoneNumber", "%2B225697845");
   params.add("timestamp", "2019-03-25T15%3A09%3A44.703088%2B02%3A00");
   params.add("value", "foo%23bar%40quizz%2Bfoo-bazz%2F%2Fquir.");

Для этого вы можете использовать UrlEncoder при передаче параметров на карту.Остерегайтесь двойного кодирования UriComponentsBuilder ваших вещей!

Вы можете получить правильный URL с помощью:

final MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("id", id);
params.add("device", device);
String uft8Charset = StandardCharsets.UTF_8.toString();
params.add("phoneNumber", URLEncoder.encode(phoneNumber, uft8Charset));
params.add("timestamp", URLEncoder.encode(timestamp.toString(), uft8Charset));
params.add("value", URLEncoder.encode(value, uft8Charset));

final URI uri = UriComponentsBuilder.fromHttpUrl("http://localhost")
                                    .port(port)
                                    .path("/events")
                                    .queryParams(params)
                                    .build(true)
                                    .toUri();

Обратите внимание, что передача "true" методу build() отключает кодировку, поэтому это означаетсхема, хост и т. д. из частей URI не будут правильно кодироваться как UriComponentsBuilder.

0 голосов
/ 25 марта 2019

После некоторой борьбы с этой проблемой я, наконец, заставил ее работать так, как мы ожидаем, в нашей компании.

Компонентом-нарушителем является не spring-boot , а скорее UriComponentsBuilder

Мой начальный провал тест выглядит следующим образом:

    @Test
public void get_should_properly_convert_query_parameters() {
    // Given
    final String device = UUID.randomUUID().toString();
    final String id = UUID.randomUUID().toString();
    final String phoneNumber = "+225697845";
    final String value = "foo#bar@quizz+foo-bazz//quir.";
    final Instant now = Instant.now();
    final ZonedDateTime timestamp = ZonedDateTime.ofInstant(now, ZoneId.of("+02:00"));

    final MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
    params.add("id", id);
    params.add("device", device);
    params.add("phoneNumber", phoneNumber);
    params.add("timestamp", timestamp.toString());
    params.add("value", value);

    final URI uri = UriComponentsBuilder.fromHttpUrl("http://localhost").port(port).path("/events").queryParams(params).build().toUri();
    final Event expected = Event.builder().device(device).id(id).phoneNumber(phoneNumber).value(value).timestamp(timestamp).build();

    // When
    final Event actual = restTemplate.exchange(uri, HttpMethod.GET, new HttpEntity<>(new HttpHeaders()), Event.class).getBody();

    // Then
    assertEquals(expected, actual);
}

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

    @Test
public void get_should_properly_convert_query_parameters() {
    // Given
    final String device = UUID.randomUUID().toString();
    final String id = UUID.randomUUID().toString();
    final String phoneNumber = "+225697845";
    final String value = "foo#bar@quizz+foo-bazz//quir.";
    final Instant now = Instant.now();
    final ZonedDateTime timestamp = ZonedDateTime.ofInstant(now, ZoneId.of("+02:00"));
    final Map<String, String> params = new HashMap<>();
    params.put("id", id);
    params.put("device", device);
    params.put("phoneNumber", phoneNumber);
    params.put("timestamp", timestamp.toString());
    params.put("value", value);
    final MultiValueMap<String, String> paramTemplates = new LinkedMultiValueMap<>();
    paramTemplates.add("id", "{id}");
    paramTemplates.add("device", "{device}");
    paramTemplates.add("phoneNumber", "{phoneNumber}");
    paramTemplates.add("timestamp", "{timestamp}");
    paramTemplates.add("value", "{value}");

    final URI uri = UriComponentsBuilder.fromHttpUrl("http://localhost").port(port).path("/events").queryParams(paramTemplates).encode().buildAndExpand(params).toUri();
    final Event expected = Event.builder().device(device).id(id).phoneNumber(phoneNumber).value(value).timestamp(ZonedDateTime.ofInstant(now, ZoneId.of("UTC"))).build();

    // When
    final Event actual = restTemplate.exchange(uri, HttpMethod.GET, new HttpEntity<>(new HttpHeaders()), Event.class).getBody();

    // Then
    assertEquals(expected, actual);
}

Примечание 4обязательные различия:

  • Требуются шаблоны параметров MultiValueMap
  • Требуется значение параметра карты
  • Требуется кодирование
  • Требуется buildAndExpand со значениями параметра

Мне немного грустно, потому что все это довольно подвержено ошибкам и обременительно (особенно часть Map / MultiValueMap).Я бы с удовольствием сгенерировал их из Java-бобов.

Это имеет большое влияние на наше решение, но, боюсь, у нас не будет выбора.Сейчас мы согласимся с этим решением.

Надеюсь, что это поможет другим, сталкивающимся с этой проблемой.

Best,

Louis

...