Сегодня я покажу вам, как определить, был ли HTTP-запрос отправлен через форму HTML или с помощью JavaScript на сервер.

Примеры в этом посте основаны на Nuxt.js и включают ссылки на несколько глобальных функций (defineEventHandler, getRequestHeaders, sendRedirect). Не важно, что вы знаете, как они работают. Просто сосредоточьтесь на концепции. Я явно выделю важные моменты

Вот очень простой обработчик событий для сервера Nuxt JS. «Обработчик событий» в Nuxt.js представляет конечную точку HTTP.

Мы можем написать обработчик событий для создания конечной точки API, которая генерирует и возвращает объект.

export default defineEventHandler(async (event) => {
  // Do some work; Get some data
  const data = { goodBoy: 'Nugget' };

  return data;
});

Этот объект будет преобразован в строку JSON для ответа HTTP.

В чем проблема?

Опять же, синтаксис и базовая структура не важны. Важно знать, что запросы, сделанные с помощью JavaScript, могут прекрасно обрабатывать ответы JSON, но с запросами, сделанными с помощью HTML-форм, дело обстоит иначе. Когда форма отправлена, браузер отправляет запрос как навигацию по странице, в результате чего следующая страница отображает все, что отвечает сервер. Если ответом является JSON, он отобразит эту строку JSON на странице.

Не лучший пользовательский опыт.

Теперь вы можете спросить себя, если мы создаем JSON API, почему мы должны заботиться о HTML-формах? И ответ таков: даже если вы предпочитаете использовать JavaScript для отправки данных, существует множество сценариев, в которых JavaScript не работает. Поэтому хорошей идеей является реализовать прогрессивное улучшение с помощью JavaScript для улучшения HTML-форм таким образом, чтобы в случае сбоя JavaScript HTTP-запрос возвращался к обычной отправке формы. Пользовательский опыт не такой привлекательный, но, по крайней мере, он не сломан.

Итак, как мы можем исправить проблему с ответом JSON?

Идеальный вариант

Если бы наш обработчик запросов мог определить, был ли запрос отправлен с помощью HTML-формы, мы могли бы ответить перенаправлением HTTP, сообщающим браузеру о загрузке определенного URL-адреса. Если бы запрос был отправлен с помощью JavaScript, мы могли бы вернуться и вернуть JSON.

Существует HTTP-заголовок под названием Sec-Fetch-Mode, который браузеры автоматически включают в каждый HTTP-запрос (включая HTML-формы и JavaScript). Мы можем использовать этот заголовок, чтобы различать одно или другое.

Заголовок может иметь следующие директивы или значения:

  • cors
  • navigate
  • no-cors
  • same-origin
  • websocket

Нас интересует navigate, потому что это указывает на то, что HTTP-запрос был сделан через навигацию по HTML-странице. Значение navigate также отправляется во время HTML из представлений, и его нельзя использовать в запросах fetch.

Имея это в виду, мы можем изменить наш код, чтобы проверить, был ли запрос отправлен через HTML-форму. Если это так, мы можем перенаправить пользователя. Вот как это может выглядеть:

export default defineEventHandler(async (event) => {

  // Do some work; Get some data

  const data = { goodBoy: 'Nugget' };

  const headers = getRequestHeaders(event);
  const isHtml = headers['sec-fetch-mode'] === 'navigate';

  if (isHtml) {
    return sendRedirect(event, String(headers.referer), 303);
  }

  return data;
});

Для любого API, который вы создаете, вы, вероятно, начнете с некоторой бизнес-логики, которая в конечном итоге генерирует объект. Важная часть — это то, что будет дальше. С помощью заголовков HTTP-запроса мы можем проверить, установлен ли заголовок Sec-Fetch-Mode для навигации. Если это так, мы знаем, что это было отправлено с помощью HTML.

(ВАЖНО: Nuxt.js преобразует все заголовки в нижний регистр, но технически допустимо также отправлять заголовки с заглавными буквами. Обязательно учтите это в своем приложении.)

Если запрос был отправлен в формате HTML, нет смысла возвращать JSON. Поэтому вместо этого мы можем отправить ответ перенаправления 303 обратно в браузер.

Если вы знаете, куда перенаправить, все готово. Многие JSON API не знают. В этом случае имеет смысл отправить запрос обратно на страницу, с которой он пришел, используя заголовок referer.

Практический вариант (сегодня)

Теперь это здорово, но есть одна проблема. Safari в настоящее время не поддерживает Sec-Fetch-Mode (как и старые браузеры).

Однако мы можем достичь того же эффекта другим способом, пока поддержка браузера не улучшится. Это не совсем так удобно.

Поскольку мы не можем использовать Sec-Fetch-Mode, чтобы точно знать, что запрос пришел с HTML, давайте поменяем логику и определим, пришел ли запрос с JavaScript. В этом случае мы ответим JSON, а если нет, по умолчанию перенаправим.

У нас есть несколько вариантов.

  1. Проверьте, содержит ли заголовок Accept строку 'application/json'.
    Если это так, мы знаем, что клиент явно запрашивает ответ JSON.
  2. Проверьте, установлено ли в заголовке Content-Type значение 'application/json'.
    Это значение не является значением по умолчанию и не может быть установлено с помощью HTML-формы, поэтому можно с уверенностью предположить, что, поскольку запрос был отправлен в формате JSON и заголовки были настроены, мы можем ответить в формате JSON.
  3. Проверьте наличие пользовательского/проприетарного заголовка HTTP, такого как x-custom-fetch.
    HTML-формы очень ограничены в том, какие заголовки они могут изменять, и они не могут добавлять собственные заголовки, как это может делать JavaScript. Если мы найдем пользовательский заголовок, мы можем предположить, что запрос был сделан с помощью JavaScript.

Мы можем изменить наш код, включив в него эти проверки (вам, вероятно, не нужны все из них, но я все равно их включу).

export default defineEventHandler(async (event) => {
  // Do some work; Get some data
  const data = { goodBoy: 'Nugget' };

  const headers = getRequestHeaders(event);

  const isJson =
    headers.accept?.includes('application/json') ||
    headers['content-type']?.includes('application/json') ||
    headers['x-custom-fetch'];

  if (isJson) {
    return data;
  }

  return sendRedirect(event, String(headers.referer), 303);
});

Опять же, если какое-либо из этих условий выполнено, мы знаем, что запрос должен быть создан в JavaScript. Если это правда, мы отвечаем JSON. В противном случае мы перенаправляем.

Бонус за пограничные вычисления

Еще один небольшой совет, которым я хочу поделиться, заключается в том, что эта функция, которую мы создали, на самом деле является идеальным кандидатом для граничных вычислений, таких как EdgeWorkers.

Пограничные вычисления позволяют добавлять пользовательскую логику между клиентом и сервером и перехватывать или изменять запросы и/или ответы. Это означает, что вы можете получить ответ от JSON API и исправить эту функциональность поверх. Проверьте, был ли запрос отправлен с помощью HTML или JavaScript; для HTML перенаправьте на referer; для JS передать ответ.

Это не повлияет на задержку, которой славятся граничные вычисления, но это удобный способ добавления функций в API без изменения существующей базы кода API. Возможно, вы не работаете в компании или команде, отвечающей за API.

Предостережения

Теперь мне важно отметить, что все эти способы обнаружения запросов JavaScript требуют проверки заголовков, которые не включены по умолчанию в стандартный HTTP-запрос. Поэтому вы должны убедиться в том, что сообщаете разработчикам, что они должны включать необходимые заголовки при создании HTTP-запросов, чтобы получить ответ JSON.

По крайней мере, так должно быть только до тех пор, пока поддержка браузеров не улучшится, и мы сможем полагаться исключительно на Sec-Fetch-Mode.

Если вы хотите увидеть пример этого на практике, я сделал видео, которое охватывает эту же тему.

Что снова за суета?

Теперь вы можете подумать про себя, ну, это вроде как круто, но почему меня это должно волновать?

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

Однако разработчики на стороне клиента могут сделать очень немногое. Если API всегда отвечает в формате JSON, прогрессивное улучшение гарантирует, что запрос все еще работает, но ответ будет нарушен.

Создавая API, обеспечивающие прогрессивное улучшение, мы даем разработчикам возможность создавать прогрессивно улучшаемые приложения.

Большое спасибо за чтение. Если вам понравилась эта статья, пожалуйста, поделитесь ею. Это один из лучших способов поддержать меня. Вы также можете подписаться на мою рассылку или следить за мной в Твиттере, если хотите знать, когда публикуются новые статьи.

Первоначально опубликовано на austingil.com.