Problémy s kódováním v AJAXu a jak na ně

V prosinci jsem znovu řešil problém s kódováním českých znaků při AJAXové komunikaci se serverem. Znovu říkám proto, že jsem stejný problém řešil před pár měsíci, ale řešení jsem stihnul úplně zapomenout. Tentokrát jsem si ale poklepal na čelo a říkám si: "Furo tvá paměť se horší, zapiš to nebo nad tím budeš za měsíc trávit čas znovu".

Problém je poměrně obecného charakteru - kdysi jsem na to narazil při používání knihovny Prototype.js, nyní to byla jQuery s Tomcatem na straně serveru. Obě dvě knihovny mají pro AJAX (jQuery, Prototype.js) velmi pěknou podporu - ačkoliv bych řekl, že Prototype.js je v tomto ohledu o kousek dál.

V případě, že používáte AJAX pro odesílání obsahu formulářů padnou vám do oka metody, které v obou frameworcích nesou shodný název "serialize()". Např.:


//jQuery
//serializes all form fields into field1=value1&field2=value2 form
var params = $("#myForm").serialize();
//serializes all form fields into simple JSON object
var jsonData = $("#myForm").serializeArray();
//Prototype.js
//serializes all form fields into field1=value1&field2=value2 form
var params = $("#myForm").serialize();
//serializes all form fields into simple JSON object
var jsonData = $("#myForm").serialize(true);

Intuitivně potom člověk použije serializovanou formu jako součást GET url:


//jQuery
var params = $("#myForm").serialize();
$.get(url + ? + params,  function(result) {alert(result);});
var jsonData = $("#myForm").serializeArray();
$.get(url, jsonData, function(result) {alert(result);});
//Prototype.js
var params = $("#myForm").serialize();
new Ajax.Request(url + "?" + params, {
  onSuccess: function(result) {alert(result);}
});
var jsonData = $("#myForm").serialize(true);
new Ajax.Request(url, {
  parameters: params,
  onSuccess: function(result) {alert(result);}
});

Jenže právě tady padne kosa na kámen. Pokud formulář obsahuje data s národními znaky, ty se cestou na server pomrší - a to zcela spolehlivě. Otázka zní, kde se pomrší a proč?!

Problém je v tom, že HTTP protokol (snad s vyjímkou nefinalizované verze HTML 5) dodnes nezná způsob jak z klienta předat na server informaci o tom v jakém kódování je url requestu. Velmi zajímavá je věta z Jetty FAQ:

"Current standards (July 2002) provide no basis for the reliable transmission of international characters using the URL %HH escape mechanism."

Tento FAQ dokument vřele doporučuji k přečtení, protože je to asi nejdetailnější rozbor této nepříjemné situace, která nás pálí i v roce 2009 - tedy sedm let po vydání tohoto FAQ!

Aby nebylo celé legrace konec, dočteme se, že různé web kontejnery a dokonce různé verze web kontejnerů překládají obsah URL různě - jen autoři Jetty serveru přiznávají, že URL očekávají v kódování ISO-8859-1, KROMĚ verze 4.0, kde URL očekávali v UTF-8. Nechci ani domýšlet, jak si standard vykládají další typy web kontejnerů.

Když se podíváme na stranu klienta - tedy web browseru. Ani tady nepanuje při kódování URL jednotný přístup. Zajímavou tabulku nabízí článek Google Browser Security Handbook.

Co s tím

Jediným víceméně spolehlivým způsobem je pro posílání parametrů, které by mohly používat národní znaky NIKDY NEPOUŽÍVAT HTTP GET metodu, ale vždy se přidržet POST. Potom můžeme jednotně aplikovat Servlet Filtr, který zavolá metodu setCharsetEncoding na kódování, ve kterém byla vrácena HTML stránka s daným formulářem (viz. např. článek Tomcat/UTF-8).

V případě Tomcatu bychom za jistých podmínek mohli vyřešit i přenos národních znaků v rámci URL (GET metoda). Pokud budeme vracet HTML stránku s formulářem v UTF-8, měla by většina browserů encodovat URI v UTF-8 kódování (některé totiž berou pro kódování URI kódování UTF-8 jako standard, jiné používají po kódování URI kódování dané HTML stránky - viz. Google Browser Security Handbook).

Na straně Tomcatu potom můžeme v server.xml konfiguračním souboru na elementu <Connector> definovat jeden z následujících atributů:

  • URIEncoding="UTF-8"

    This specifies the character encoding used to decode the URI bytes, after %xx decoding the URL. If not specified, ISO-8859-1 will be used.

  • useBodyEncodingForURI="true"

    This specifies if the encoding specified in contentType should be used for URI query parameters, instead of using the URIEncoding. This setting is present for compatibility with Tomcat 4.1.x, where the encoding specified in the contentType, or explicitely set using Request.setCharacterEncoding method was also used for the parameters from the URL. The default value is false.

Nicméně toto nastavení bude potom platné pro všechny virtuální hosty daného Tomcatu, což může být pro využití této funkce limitující. Z tohoto důvodu jsem výše popsané řešení ani netestoval a spokojil jsem se s tím, že veškerá data obsahující národní znaky AJAXem vždy POSTuji.

Na závěr

Tento článek nepřináší v podstatě žádné nové informace. Tenhle problém je tu s námi již řadu let. Řadu let už používáme osvědčené workaroundy, které nám víceméně fungují a řekl bych, že ještě pěknou řádku let s nimi budeme muset vystačit.

Tuhle kapitolku jsem tu otevřel proto, že na tento problém můžeme narazit z odlišného úhlu - při odesílání dat pomocí AJAXu, kde nám tento problém nemusí přijít tak zřejný a je navíc velmi lákavé zůstat u defaultních hodnot javascriptových knihoven, které typicky znamenají GET požadavek na stranu serveru.

Přestože webové aplikace píšu už nějaký ten pátek, strávil jsem na analýze problému při AJAXovém volání poměrně dost času. A to dokonce dvakrát :-) ... ale potřetí už ne, protože až na si naběhnu znovu, tak si budu moci přečíst už jen závěry v tomhle článku.

Zdroje