XMLHttpRequest POST, формы и кодировка

Во время обычной отправки формы <form> браузер собирает значения её полей, делает из них строку и составляет тело GET/POST-запроса для посылки на сервер.

При отправке данных через XMLHttpRequest, это нужно делать самим, в JS-коде. Большинство проблем и вопросов здесь связано с непониманием, где и какое кодирование нужно осуществлять.

Кодировка urlencoded

Основной способ кодировки запросов – это urlencoded, то есть – стандартное кодирование URL.

Например, форма:

<form action="/submit" method="GET">
  <input name="name" value="Ivan">
  <input name="surname" value="Ivanov">
</form>

Здесь есть два поля: name=Ivan и surname=Ivanov.

Браузер перечисляет такие пары «имя=значение» через символ амперсанда & и, так как метод GET, итоговый запрос выглядит как /submit?name=Ivan&surname=Ivanov.

Все символы, кроме английских букв, цифр и - _ . ! ~ * ' ( ) заменяются на их цифровой код в UTF-8 со знаком %.

Например, пробел заменяется на %20, символ / на %2F, русские буквы кодируются двумя байтами в UTF-8, поэтому, к примеру, Ц заменится на %D0%A6.

Например, форма:

<form action="/submit" method="GET">
  <input name="name" value="Виктор">
  <input name="surname" value="Цой">
</form>

Будет отправлена так: /submit?name=%D0%92%D0%B8%D0%BA%D1%82%D0%BE%D1%80&surname=%D0%A6%D0%BE%D0%B9.

в JavaScript есть функция encodeURIComponent для получения такой кодировки «вручную»:

alert( encodeURIComponent(' ') ); // %20
alert( encodeURIComponent('/') ); // %2F
alert( encodeURIComponent('В') ); // %D0%92
alert( encodeURIComponent('Виктор') ); // %D0%92%D0%B8%D0%BA%D1%82%D0%BE%D1%80

Эта кодировка используется в основном для метода GET, то есть для передачи параметра в строке запроса. По стандарту строка запроса не может содержать произвольные Unicode-символы, поэтому они кодируются как показано выше.

GET-запрос

Формируя XMLHttpRequest, мы должны формировать запрос «руками», кодируя поля функцией encodeURIComponent.

Например, для посылки GET-запроса с параметрами name и surname, аналогично форме выше, их необходимо закодировать так:

// Передаём name и surname в параметрах запроса

var xhr = new XMLHttpRequest();

var params = 'name=' + encodeURIComponent(name) +
  '&surname=' + encodeURIComponent(surname);

xhr.open("GET", '/submit?' + params, true);

xhr.onreadystatechange = ...;

xhr.send();
Прочие заголовки

Браузер автоматически добавит к запросу важнейшие HTTP-заголовки, такие как Content-Length и Connection.

По спецификации браузер запрещает их явную установку, как и некоторых других низкоуровневых HTTP-заголовков, которые могли бы ввести в заблуждение сервер относительно того, кто и сколько данных ему прислал, например Referer. Это сделано в целях контроля правильности запроса и для безопасности.

Сообщаем про AJAX

Запрос, отправленный кодом выше через XMLHttpRequest, никак не отличается от обычной отправки формы. Сервер не в состоянии их отличить.

Поэтому в некоторых фреймворках, чтобы сказать серверу, что это AJAX, добавляют специальный заголовок, например такой:

xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");

POST с urlencoded

В методе POST параметры передаются не в URL, а в теле запроса. Оно указывается в вызове send(body).

В стандартных HTTP-формах для метода POST доступны три кодировки, задаваемые через атрибут enctype:

  • application/x-www-form-urlencoded
  • multipart/form-data
  • text-plain

В зависимости от enctype браузер кодирует данные соответствующим способом перед отправкой на сервер.

В случае с XMLHttpRequest мы, вообще говоря, не обязаны использовать ни один из этих способов. Главное, чтобы сервер наш запрос понял. Но обычно проще всего выбрать какой-то из стандартных.

В частности, при POST обязателен заголовок Content-Type, содержащий кодировку. Это указание для сервера – как обрабатывать (раскодировать) пришедший запрос.

Для примера отправим запрос в кодировке application/x-www-form-urlencoded:

var xhr = new XMLHttpRequest();

var body = 'name=' + encodeURIComponent(name) +
  '&surname=' + encodeURIComponent(surname);

xhr.open("POST", '/submit', true)
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')

xhr.onreadystatechange = ...;

xhr.send(body);
Только UTF-8

Всегда используется только кодировка UTF-8, независимо от языка и кодировки страницы.

Если сервер вдруг ожидает данные в другой кодировке, к примеру windows-1251, то их нужно будет перекодировать.

Кодировка multipart/form-data

Кодировка urlencoded за счёт замены символов на %код может сильно «раздуть» общий объём пересылаемых данных. Поэтому для пересылки файлов используется другая кодировка: multipart/form-data.

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

Чтобы использовать этот способ, нужно указать его в атрибуте enctype и метод должен быть POST:

<form action="/submit" method="POST" enctype="multipart/form-data">
  <input name="name" value="Виктор">
  <input name="surname" value="Цой">
</form>

Форма при такой кодировке будет выглядеть примерно так:

...Заголовки...
Content-Type: multipart/form-data; boundary=RaNdOmDeLiMiTeR

--RaNdOmDeLiMiTeR
Content-Disposition: form-data; name="name"

Виктор
--RaNdOmDeLiMiTeR
Content-Disposition: form-data; name="surname"

Цой
--RaNdOmDeLiMiTeR--

…То есть, поля передаются одно за другим, значения не кодируются, а чтобы было чётко понятно, какое значение где – поля разделены случайно сгенерированной строкой, которую называют «boundary» (англ. граница), в примере выше это RaNdOmDeLiMiTeR:

Сервер видит заголовок Content-Type: multipart/form-data, читает из него границу и раскодирует поля формы.

Такой способ используется в первую очередь при пересылке файлов, так перекодировка мегабайтов через urlencoded существенно загрузила бы браузер. Да и объём данных после неё сильно вырос бы.

Однако, никто не мешает использовать эту кодировку всегда для POST запросов. Для GET доступна только urlencoded.

POST с multipart/form-data

Сделать POST-запрос в кодировке multipart/form-data можно и через XMLHttpRequest.

Достаточно указать в заголовке Content-Type кодировку и границу, и далее сформировать тело запроса, удовлетворяющее требованиям кодировки.

Пример кода для того же запроса, что и раньше, теперь в кодировке multipart/form-data:

var data = {
  name: 'Виктор',
  surname: 'Цой'
};

var boundary = String(Math.random()).slice(2);
var boundaryMiddle = '--' + boundary + '\r\n';
var boundaryLast = '--' + boundary + '--\r\n'

var body = ['\r\n'];
for (var key in data) {
  // добавление поля
  body.push('Content-Disposition: form-data; name="' + key + '"\r\n\r\n' + data[key] + '\r\n');
}

body = body.join(boundaryMiddle) + boundaryLast;

// Тело запроса готово, отправляем

var xhr = new XMLHttpRequest();
xhr.open('POST', '/submit', true);

xhr.setRequestHeader('Content-Type', 'multipart/form-data; boundary=' + boundary);

xhr.onreadystatechange = function() {
  if (this.readyState != 4) return;

  alert( this.responseText );
}

xhr.send(body);

Тело запроса будет иметь вид, описанный выше, то есть поля через разделитель.

Отправка файла

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

Для добавления файла нужно использовать тот же код, что выше, модифицировав заголовки перед полем, которое является файлом, так:

Content-Disposition: form-data; name="myfile"; filename="pic.jpg"
Content-Type: image/jpeg
(пустая строка)
содержимое файла

FormData

Современные браузеры, исключая IE9- (впрочем, есть полифилл), поддерживают встроенный объект FormData, который кодирует формы для отправки на сервер.

Это очень удобно. Например:

<form name="person">
  <input name="name" value="Виктор">
  <input name="surname" value="Цой">
</form>

<script>
  // создать объект для формы
  var formData = new FormData(document.forms.person);

  // добавить к пересылке ещё пару ключ - значение
  formData.append("patronym", "Робертович");

  // отослать
  var xhr = new XMLHttpRequest();
  xhr.open("POST", "/url");
  xhr.send(formData);
</script>

Этот код отправит на сервер форму с полями name, surname и patronym.

Интерфейс:

  • Конструктор new FormData([form]) вызывается либо без аргументов, либо с DOM-элементом формы.
  • Метод formData.append(name, value) добавляет данные к форме.

Объект formData можно сразу отсылать, интеграция FormData с XMLHttpRequest встроена в браузер. Кодировка при этом будет multipart/form-data.

Другие кодировки

XMLHttpRequest сам по себе не ограничивает кодировку и формат пересылаемых данных.

Поэтому для обмена данными часто используется формат JSON:

var xhr = new XMLHttpRequest();

var json = JSON.stringify({
  name: "Виктор",
  surname: "Цой"
});

xhr.open("POST", '/submit', true)
xhr.setRequestHeader('Content-type', 'application/json; charset=utf-8');

xhr.onreadystatechange = ...;

// Отсылаем объект в формате JSON и с Content-Type application/json
// Сервер должен уметь такой Content-Type принимать и раскодировать
xhr.send(json);

Итого

  • У форм есть две основные кодировки: application/x-www-form-urlencoded – по умолчанию и multipart/form-data – для POST запросов, если явно указана в enctype. Вторая кодировка обычно используется для больших данных и только для тела запроса.
  • Для составления запроса в application/x-www-form-urlencoded используется функция encodeURIComponent.
  • Для отправки запроса в multipart/form-data – объект FormData.
  • Для обмена данными JS ↔ сервер можно использовать и просто JSON, желательно с указанием кодировки в заголовке Content-Type.

В XMLHttpRequest можно использовать и другие HTTP-методы, например PUT, DELETE, TRACE. К ним применимы все те же принципы, что описаны выше.

Карта учебника

Комментарии

перед тем как писать…
  • Приветствуются комментарии, содержащие дополнения и вопросы по статье, и ответы на них.
  • Для одной строки кода используйте тег <code>, для нескольких строк кода — тег <pre>, если больше 10 строк — ссылку на песочницу (plnkr, JSBin, codepen…)
  • Если что-то непонятно в статье — пишите, что именно и с какого места.