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

в 15:37, , рубрики: java, JAX-RS, jboss, rest, Resteasy, метки: , , , ,

Введение

Для передачи данных от интерфейса веб-приложения есть несколько методов, но, пожалуй, самый распространенный — отправка формы с 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, то проставляется заголовок, его содержащий.

Автор: grossws

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js