Я недавно писал про то, как мы используем заголовок Accept для определения, в каком виде присылать HTTP-ответ: структуру для ajax-функции или страницу целиком. Через некоторое время мы напали на некоторые баги, каковой историей и хочу поделиться.

Баги в порядке появления такие:

В итоге, победить эту мешанину получилось тем, что:

А в целом, это все оставляет некий неприятный осадок, потому что используй мы "некошерный" подход с ?ajax=1, проблем бы не возникло. Так что приходится оставить использование Accept только на те случаи, где клиентами выступают не браузеры, а специально написанные клиенты, которые точно контроллируют свои заголовки, и точно знают, что хотят получить. Как например у Джеймса Беннетта для определения запроса yadis-документа с главной страницы.

Комментарии: 15

  1. MiRacLe

    Тоже столкнулся с теми же проблемами, и практически так же решил - либо это собственный заголовок (x-requested-with), либо свой экзотичный mime-type(text/x-ajax) или крайний случай - та самая переменная в запросе:

    $respond_to_ajax = ((isset($_SERVER['HTTP_X_REQUESTED_WITH']) && ($_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest') || (isset($_SERVER['HTTP_ACCEPT']) && (false !== strpos($_SERVER['HTTP_ACCEPT'],'text/x-ajax'))) || (isset($_GET['ajax']) && $_GET['ajax'] == 1));

  2. Денис

    Моё личное ощущение - Accept немного не для этого создан. В описании протокола сказано The Accept request-header field can be used to specify certain media types which are acceptable for the response, т.е. что UA может понять, не так ли?

    А почему бы просто не добавлять свой header? jQuery и Prototype, например, добавляет X-Requested-With: XMLHttpRequest, а тогда достаточно проверить

     (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) && 'XMLHttpRequest' == $_SERVER['HTTP_X_REQUESTED_WITH']))
    

    А ?ajax=1 - это ужасный подход, о нём и речи быть не должно. Помимо этого, просто физически не всегда это возможно.

  3. Иван Сагалаев

    А почему бы просто не добавлять свой header?

    Основные мысли по этому поводу я описал в той статье, на которую в начале сослался. Никакой особо четкой причины в общем-то и нет. Но основная идея в том, что на серверной стороне есть функция, которая формирует какие-то данные и может выдавать их в разных видах: страница для браузера, XML для ajax-клиента, XML для клиента какой-нибудь другой природы. В этом смысле приложение, работающее через XmlHttpRequest ничем примечательным не выделяется, ему просто нужны данные в некотором приемлемом формате. Вот потому и хочется использовать Accept.

    Другое дело, что чисто с практических соображений, которые я тут и описал, все получается не так ровно, как хотелось бы.

  4. Денис Барушев

    А почему-бы не использовать для задания желаемого формата HTTP-ответа так называемое "расширение файла"? В кавычках, потому как в контексте веба оно изначально и использовалось как расширение файла, ведь путь в URL совпадал с физическим путем к html-файлу или скрипту от корня веб-сервера. Рассматривая же веб-сайт как набор ресурсов, а не html-страниц (чем он по сути и является), имеем, например:

    /users      # ресурс users в дефолтном формате
    /users.xml  # в формате xml
    /users.js   # в json
    /users.html # в формате html
    /users.ajax # название выбрано произвольно, отличие от предыдущего в том, что это кусок html-документа для вставки, сгенерированный без основного лейаута страницы
    

    Т.е. совершенно однозначно указываем желаемый формат, при этом не привязываясь к типу запроса (будь это синхронный или асинхронный GET или POST запрос). К тому же, такую ссылку без боязни можно положить в закладки. Настроить же роуты в контексте какого-то конкретного фреймворка/языка труда не составит чтоб все эти URL'ы обрабатывались одним контроллером.

  5. MiRacLe

    А ?ajax=1 - это ужасный подход, о нём и речи быть не должно. Помимо этого, просто физически не всегда это возможно.

    А почему бы просто не добавлять свой header?

    Вашими словами - это просто физически не всегда возможно. Пример:

    <script src="http://other.site.tld/do.load.some.data" type="text/javascript"></script>
    

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

  6. Иван Сагалаев

    К тому же, такую ссылку без боязни можно положить в закладки.

    Еще раз должен сказать, что как раз об этом и есть первая статья :-).

    Вкратце — я хочу, чтобы информация о формате не сохранялась в ссылке. Например я делаю ajax-запрос, во время которого выясняется, что юзер не авторизован. Он тогда редиректится на нашу яндексовый Паспорт для авторизации, куда передается также и исходный URL, на который пользователя потом оттуда вернут. И в этом контексте этот же самый URL уже должен вернуть полноценную страницу, а не какой-нибудь XML.

  7. ods

    Вопрос о правильности использования Accept в таких целях спорный. Этот заголовок должен выставляться агентом (браузером). Хотя агентом (с некоторой долей условности) можно считать client-side приложение на JavaScript-е, т.е. тот код, который делает запрос.

    Другое дело, стоит ли считать эти HTML и JSON/XML для получения через AJAX двумя представлениями одной сущности. В большинстве случаев это не так: в HTML вы показываете всю страницу, в то время как через AJAX получаете только какоё-то её фрагмент, причём таких фрагментов для одной страницы может быть множество. Другими словами, не только формат зависит от того, как вы представляете каждый объект, но и набор объектов меняется. Поэтому я не вижу смысла в поставленной задаче.

    Что касается выбора наилучшего формата на основе заголовка Accept, то типовым решением является использование разных значений весов по умолчанию (т.е. если вес не указан явно), например:

    • 1 для type/subtype
    • 0.1 для type/*
    • 0.01 для /
  8. Иван Сагалаев

    Другое дело, стоит ли считать эти HTML и JSON/XML для получения через AJAX двумя представлениями одной сущности.

    Хорошее замечание :-). Но в нашем случае это именно так: у нас GET ссылки через ajax целиком повторяет функциональность перехода на страницу с тем же URL'ом. А заодно и POST формы через ajax и не через ajax приводит к одинаковому эффекту, различающемуся только интерфейсно.

  9. ods

    Но в нашем случае это именно так: у нас GET ссылки через ajax целиком повторяет функциональность перехода на страницу с тем же URL’ом.

    Это так звучат требование? По-моему больше похоже на обещание, что требования никогда не выйдут за определённые рамки. Только требования - это самая изменчивая вещь в проекте, иначе бы мы никогда не услышали о таких вещах, как agile software development (extreme programming и пр.). Не понимаю, зачем связывать руки заказчику, накладывая такие жёсткие ограничения, даже если текущие требования в них вполне укладываются. Да, вы получаете некоторую дополнительную гибкость при поиске решения. Но пока что (использование Accept) это вам не даёт каких никаких преимуществ, кроме некоторого (весьма сомнительного!) ощущение правильности.

    Судя по вашему тексту, вы постулируете равенство URL (как идентификатор текущего состояния страницы в окне браузера) и URL последнего сделанного запроса. Но браузер не делает этого автоматически, то есть он не считает последний URL для AJAX запроса текущим URL (то есть то, что пользователь получит при обновлении страницы, поместив страницу в закладки, будет передано в заголовке referer при переходе по ссылке и т.д.). Значит вы делаете это вручную? Тогда почему этот новый URL должен быть обязательно совпадать с URL последнего запроса? Почему, например, его не получать из того же AJAX ответа в явном виде?

  10. Иван Сагалаев

    Почему требования? Это просто архитектурный стиль, который мы используем. Серверное приложение — это набор ресурсов, на которые можно воздействовать в REST-стиле. Соответственно, очень удобно не изобретать для ajax'а отдельный интерфейс, а привязывать его к HTML'ным ссылкам и формам. И хотя я действительно часто слышу, что "так трудно" или "так невозможно", на самом деле получается очень четкая и простая система. Как мелкий бонус — все работает с отключенным javascript'ом.

    Судя по вашему тексту, вы постулируете равенство URL (как идентификатор текущего состояния страницы в окне браузера) и URL последнего сделанного запроса.

    Не постулирую. Даже не очень понимаю, как это :-).

    URL — это адрес ресурса, состояние которого показывает загруженная в браузер страница. Ссылки на этой странице — это ссылки на другие ресурсы, при клике на которые:

    • в случае ajax с сервера подтягивается представление ресурса, которое показывается в виде каких-нибудь информеров или диалоговых окон
    • в случае не-ajax просто в браузер загружается новая страница

    Если состояние сервера со страницы можно менять, то на странице будет POST-форма, которая:

    • в случае ajax изменит состояние на сервере и получит какую-то структуру данных, отаражающих эти измненения, которые что-то поменяют в DOM'е страницы, если это нужно
    • в случае не-ajax форма обработается бразуером и перепокажет новое состояние ресурса в виде новой страницы

    Впрочем, это скорее тема для отдельного поста...

  11. ods

    Не постулирую. Даже не очень понимаю, как это :-).

    То есть если пользователь нажмёт "refresh" или поместит страницу в закладки, то это всегда будет первая страница, на которую он зашёл на данном ресурсе, но не то, что отображается в текущий момент в браузере (подразумевается, что браузер поддерживает AJAX)?

  12. Иван Сагалаев

    А, нет, такого конечно нет. Ajax навешивается только на некоторые избранные ссылки и формы, на которых это имеет смысл с интерфейсной точки зрения. Большая же часть навигации остается совершенно стандартной.

    (Черт, показать хочется, а NDA не дает пока :-) )

  13. alt.

    Пожалуйста, напишите баг-репорты разработчикам всех вышеуказанных браузеров

  14. Александр Пугачёв

    А вы используете джанговые тесты? TestClient ведь не умеет добавлять HTTP заголовки для get()?

  15. Иван Сагалаев

    Используем. TestClient вполне это умеет, хоть и не очень красивенько:

    self.client.get('/', defaults={'HTTP_ACCEPT': 'application/xml'})
    

Добавить комментарий