Проблемы передачи utf-8 из формы в JAX-RS (REST)

    Введение


    Для передачи данных от интерфейса веб-приложения есть несколько методов, но, пожалуй, самый распространенный — отправка формы с MIME-type application/x-www-form-urlencoded. Ещё один вариант — multipart/form-data.

    В качестве серверной технологии для приема могут использоваться контроллеры в MVC-фреймворках (из основных Java-технологий следует упомянуть Spring MVC, Java Server Pages, Java Server Faces. Но эти фреймворки довольно сильно осложняют жизнь разработчику интерфейса, если он не знаком с Java или нужен шаг в сторону от того, что позволяет фреймворк. В случае же экспонирования REST-интерфейса бэкендом приложения разработкой фронта упрощается: ей может заниматься человек, знающий базовый javascript и jquery, независимый от разработки бэка. Кроме того, даже при использовании шаблонизатора, выбор сильно расширяется: Apache Velocity, FreeMarker (стоит упомянуть, что Spirng MVC неплохо интегрируется с последними). Тогда данные из формы на стороне сервера записываются в бины, с которыми связан данный view/controller. Правда, у JSF тоже наблюдалась генетическая проблема с кодировками, рассмотрение которой — тема для отдельной статьи.

    Краткое введение в JAX-RS было дано в предыдущей статье. И экспонированный через JAX-RS интерфейс может принимать GET и POST запросы с данными форм. О проблемах данного подхода при использовании не-latin-1 и пойдет речь в данной статье.


    Получение данных формы в JAX-RS


    Для инжекции параметра из формы в метод существует несколько аннотаций: @Form, @FormParam. Для POST-запроса @FormParam эквивалентен @QueryParam для GET по поведению. С маленьким исключением: поведение при использовании utf-8. В случае использования метода GET декодирование в строку из urlencoded происходит без проблем. Для POST-же в Content-Type требуется указать charset=utf-8, чтобы при декодировании из urlencoded преобразование байтового потока происходило в UTF-8, а не Latin-1 (поведение по умолчанию).

    Браузеры

    Браузеры (или jquery, например) не указывают кодировку, что приводит к неправильной интерпретации значений полей формы, если они содержат старшие CP UTF-8. Обходится данная проблема при использовании jquery явным указанием заголовка Content-Type: application/x-www-form-urlencoded; charset=utf-8. Для формы без js мне не удалось достичь успеха указанием enctype для формы.

    JBoss Resteasy

    Возникает, конечно, вопрос: надо ли использовать аннотацию Form, если данные будут приходить и от других приложений? В нашем случае её использование оправдано из принципов DRY: один и тот же метод может использоваться и формой клиента, и внешними приложениями, использующими это API.

    При использовании клиентского фрейморка Resteasy возможны несколько вариантов. Например, можно добавить в необходимые методы @HeaderParam("Content-Type"):

    @Path("/")
    public interface Rest {
      @POST
      @Path("test")
      public String test(@FormParam("q") String query, @HeaderParam("Content-Type") String contentType);
    }
    


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

    Rest client = ProxyFactory.create(Rest.class, url);
    client.test(query, "application/x-www-form-urlencode; charset=utf-8");
    


    Но более правильным и удобным будет использование клиентского интерсептора, добавляющего в заголовок Content-Type: application/x-www-form-urlencoded поле charset. Реализуется это следующим образом:

    @ClientInterceptor
    @HeaderDecoratorPrecedence
    public class RestInterceptor implements ClientExecutionInterceptor {
      public static final String FORM_CONTENT_TYPE = "application/x-www-form-urlencoded";
      public static final String FORM_CONTENT_TYPE_WITH_CHARSET = "application/x-www-form-urlencoded; charset=utf-8";
    
      @Override
      public ClientResponse execute(ClientExecutionContext context) throws Exception {
        String contentType = context.getRequest().getHeaders().getFirst(HttpHeaders.CONTENT_TYPE);
        if (formWithoutCharset(contentType)) {
          context.getRequest().header(HttpHeaders.CONTENT_TYPE, FORM_CONTENT_TYPE_WITH_CHARSET);
        }
        return context.proceed();
      }
    
      private boolean formWithoutCharset(String contentType) {
        return contentType != null
               && contentType.contains(FORM_CONTENT_TYPE)
               && ! contentType.contains("charset");
      }
    }
    


    Такой вариант интерсептора, конечно, не идеален. Возможно, что действительно хочется отправить форму в latin-1… Но это первый шаг к тому, чтобы упростить код клиента.

    Для его активации необходимо немного поправить процедуру инициализации фреймворка:

    public static void initResteasy() {
      ResteasyProviderFactory factory = ResteasyProviderFactory.getInstance();
      RegisterBuiltin.register(factory);
    
      InterceptorRegistry<ClientExecutionInterceptor> registry = factory.getClientExecutionInterceptorRegistry();
      registry.register(new RestInterceptor());
    }
    


    После этого и при использовании ClientRequest, и при использовании прокси-объектов в процессе отправки запроса при наличии заголовка Content-Type с application/x-www-form-urlencoded, но без charset, то проставляется заголовок, его содержащий.
    • +8
    • 16,1k
    • 6
    Поделиться публикацией

    Похожие публикации

    Комментарии 6

      0
      В своё время эту проблему решили с помощью глобального фильтра который устанавливал верную кодировку.
      Вот тут есть пример того как оно выглядит.
        +2
        В спринге стандартная тема org.springframework.web.filter.CharacterEncodingFilter
          0
          В Spring'е очень много хороших решений, которые сейчас спешно впиливают в EE.
          Например, Weld (JBoss'овская реализация CDI) послабее Spring'а в некоторых местах. Weld-OSGi они так и не доделали до нормального состояния (по ситуации на полгода назад). При этом Spring DM (ныне Eclipse Gemini Blueprint) работает уже давно и хорошо.
          0
          Аналогичная фильтрация была в JBoss AS 6 (тоже настраиваемая, со стандартным latin-1 по умолчанию). При переходе к 7 версии товарищи её протеряли… Не просто параметра нет в конфигах/админке, а в коде нет управления сходным фильтром для кодировки по умолчанию (относится к 7.0, в 7.1 проверю — напишу).
          0
          Спасибо за статью, именно сегодня нашли ошибку, что Chrome не выставляет charset в Content-Type: application/x-www-form-urlencoded; charset=utf-8, в то время когда в FireFox все установлено как надо.
          Думаю обратить внимание gui дева, и заодно добавить CharacterEncodingFilter.
            0
            Если UI использует jquery или prototype (или что-то ещё =) ), то можно проставить заголовок при отправке формы AJAX'ом.

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое