Давайте обновим наше приложение, чтобы маршрут /snippet/create
отвечал только на HTTP запросы, в которых используется метод POST
.
Содержание статьи
- Коды состояния HTTP в веб-приложениях
- Настройка HTTP-Заголовка
- Использование http.Error
- Управление HTTP заголовками в Go
- HTTP заголовки по умолчанию от веб-сервера
Архитектура HTTP-маршрутов веб-приложения для создания новой заметки.
Рекомендуем вам супер TELEGRAM канал по Golang где собраны все материалы для качественного изучения языка. Удивите всех своими знаниями на собеседовании! 😎
Мы публикуем в паблике ВК и Telegram качественные обучающие материалы для быстрого изучения Go. Подпишитесь на нас в ВК и в Telegram. Поддержите сообщество Go программистов.
Метод | Шаблон | Обработчик | Действие |
ANY (любой метод) | / | home | Отображает домашнюю страницу |
ANY (любой метод) | /snippet | showSnippet | Отображает определенную заметку |
POST | /snippet/create | createSnippet | Создает новую заметку |
Очень важно выполнить данное изменение, потому что POST запрос к маршруту /snippet/create
должен создавать новую запись в базе данных, но если будет выполнен GET запрос, то результатом будет ошибка 405 — запрещенный метод.
Создание новой заметки в базе данных является не идемпотентным действием, которое изменяет состояние нашего сервера. Поэтому мы должны придерживаться хорошей HTTP практики и ограничить маршрут, чтобы он отвечал только на POST-запросы.
Однако, главная причина, почему я хочу осветить данную тему заключается в HTTP заголовках и то, как они настраиваются.
Коды состояния HTTP в веб-приложениях
Давайте начнем с обновлением кода для функции-обработчика createSnippet()
. Функция должна возвращать код состояния HTTP 405 (метод запрещен), но только в том случае, если это GET запрос. Для этого можно использовать метод w.WriteHeader()
следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
package main ... // Обработчик для создания новой заметки. func createSnippet(w http.ResponseWriter, r *http.Request) { // Используем r.Method для проверки, использует ли запрос метод POST или нет. Обратите внимание, // что http.MethodPost является строкой и содержит текст "POST". if r.Method != http.MethodPost { // Если это не так, то вызывается метод w.WriteHeader() для возвращения статус-кода 405 // и вызывается метод w.Write() для возвращения тела-ответа с текстом "Метод запрещен". // Затем мы завершаем работу функции вызвав "return", чтобы // последующий код не выполнялся. w.WriteHeader(405) w.Write([]byte("GET-Метод запрещен!")) return } w.Write([]byte("Создание новой заметки...")) } ... |
Хотя данное изменение выглядит простым, в нем есть несколько нюансов, которые стоит пояснить:
- Вызвать метод
w.WriteHeader()
в обработчике можно только один раз, и после возвращения кода состояния HTTP, изменить его нельзя. Если попытаться вызватьw.WriteHeader()
во второй раз, Go выдаст сообщение об ошибке; - Если не вызывать метод
w.WriteHeader()
напрямую, тогда первый вызовw.Write()
автоматически отправит пользователю код состояния200 OK
. Поэтому, если вы хотите вернуть другой код состояния, вызовите один раз методw.WriteHeader()
перед любым вызовомw.Write()
.
Давайте рассмотрим все это в действии.
Перезапустите веб-сервер, откройте второе окно терминала и выполните следующую curl-команду для создания POST-запроса на адрес http://127.0.0.1:4000/snippet/create
. Вы должны получить HTTP-ответ с кодом состояния 200 OK
.
1 |
curl -i -X POST http://127.0.0.1:4000/snippet/create |
Вы получите такой результат:
1 2 3 4 5 6 |
HTTP/1.1 200 OK Date: Tue, 15 Dec 2020 17:34:40 GMT Content-Length: 45 Content-Type: text/plain; charset=utf-8 Создание новой заметки... |
Однако, если вы выполните другой метод запроса: GET
, PUT
или DELETE
— вы получите ответ с кодом состояния HTTP 405 Method Not Allowed
.
К примеру:
1 |
curl -i -X PUT http://127.0.0.1:4000/snippet/create |
Результатом будет:
1 2 3 4 5 6 |
HTTP/1.1 405 Method Not Allowed Date: Tue, 15 Dec 2020 17:36:52 GMT Content-Length: 32 Content-Type: text/plain; charset=utf-8 GET-Метод запрещен! |
Настройка HTTP-Заголовка
Другое улучшение, которое мы можем добавить, это заголовок Allow: POST
для каждого ответа 405 «метод запрещен», чтобы пользователь знал, какие HTTP-методы поддерживаются для определенного URL.
Это можно сделать, используя метод w.Header().Set()
для добавления нового заголовка к карте HTTP-заголовков. Например:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
package main ... // Обработчик для создания новой заметки. func createSnippet(w http.ResponseWriter, r *http.Request) { // Используем r.Method для проверки, использует ли запрос метод POST или нет. Обратите внимание, // что http.MethodPost является строкой и содержит текст "POST". if r.Method != http.MethodPost { // Используем метод Header().Set() для добавления заголовка 'Allow: POST' в // карту HTTP-заголовков. Первый параметр - название заголовка, а // второй параметр - значение заголовка. w.Header().Set("Allow", http.MethodPost) // Вызываем метод w.WriteHeader() для возвращения статус-кода 405 // и вызывается метод w.Write() для возвращения тела-ответа с текстом "Метод запрещен". w.WriteHeader(405) w.Write([]byte("GET-Метод запрещен!")) // Затем мы завершаем работу функции вызвав "return", чтобы // последующий код не выполнялся. return } w.Write([]byte("Создание новой заметки...")) } ... |
Внимание: Вы должны сперва вызвать метод
w.Header().Set()
и уже потом остальные методыw.WriteHeader()
илиw.Write()
иначе ваши изменения не будут учтены.
Давайте повторим наш PUT или GET запрос на адрес /snippet/create
и проверим если наш новый заголовок Allow появится в списке:
1 |
curl -i -X PUT http://127.0.0.1:4000/snippet/create |
Результат:
1 2 3 4 5 6 7 |
HTTP/1.1 405 Method Not Allowed Allow: POST Date: Tue, 15 Dec 2020 18:14:10 GMT Content-Length: 32 Content-Type: text/plain; charset=utf-8 GET-Метод запрещен! |
Как вы видите, теперь ответ включает в себя строку Allow: POST
.
Использование http.Error
Если требуется отправить какой-то код состояния, кроме 200 с текстом ответа (как мы это делали в коде выше), можно использовать http.Error(). Это вспомогательная функция, которая принимает текст сообщения и код состояния, а затем «за кулисами» сама вызывает методы w.WriteHeader()
и w.Write()
.
Обновим код следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
package main ... // Обработчик для создания новой заметки. func createSnippet(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.Header().Set("Allow", http.MethodPost) // Используем функцию http.Error() для отправки кода состояния 405 с соответствующим сообщением. http.Error(w, "Метод запрещен!", 405) return } w.Write([]byte("Создание новой заметки...")) } ... |
С точки зрения функциональности, она осталась прежней. Разница в том, что теперь мы передаем ответственность вызова http.ResponseWriter
другой функции, которая отправляет ответ пользователю за нас.
В Go, практика вызова http.ResponseWriter
в коде других вспомогательных функций очень распространена, и в будущих уроках мы будем часто использовать такой подход. Важно, чтобы вы понимали, что лежит «под капотом» в других более продвинутых (и интересных!) способах отправки ответов пользователям. Ранее, мы использовали методы w.Write()
и w.WriteHeader()
напрямую, но на практике так делают довольно редко.
Управление HTTP заголовками в Go
В приведенном выше коде мы использовали w.Header().Set()
, чтобы добавить новый заголовок в карту HTTP заголовка. Существуют такие методы как Add()
, Del()
и Get()
, которые тоже можно использовать для добавления, удаления и чтения заголовков из карты.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// Устанавливаем новый заголовок управления кешем. Если заголовок "Cache-Control" уже указан // то он будет переписан. w.Header().Set("Cache-Control", "public, max-age=31536000") // Метод Add() добавляет новый заголовок "Cache-Control" и может // вызываться несколько раз. w.Header().Add("Cache-Control", "public") w.Header().Add("Cache-Control", "max-age=31536000") // Удаляем все значения из заголовка "Cache-Control". w.Header().Del("Cache-Control") // Получаем первое значение из заголовка "Cache-Control". w.Header().Get("Cache-Control") |
HTTP заголовки по умолчанию от веб-сервера
При отправке ответа, Go автоматически установит для вас три сгенерированных системой HTTP заголовка: Date
, Content-Length
и Content-Type
.
Заголовок Content-Type
(тип контента) весьма интересен. Go попытается автоматически узнать тип контента проанализировав содержимое тела ответа с помощью функции http.DetectContentType()
. Если эта функция не сможет определить тип контента, Go укажет следующее значение для заголовка Content-Type: application/octet-stream
.
Функция http.DetectContentType()
обычно работает довольно неплохо. Однако главная проблема веб-разработчиков, плохо знакомыми с Go, заключается в том, что данная функция не может отличить JSON от обычного текста. По умолчанию, ответы содержащие JSON будут отправляться с заголовком Content-Type: text/plain; charset=utf-8
. Вы можете предотвратить это, установив правильный заголовок вручную следующим образом:
1 2 |
w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{"name":"Alex"}`)) |
Обработка HTTP заголовка
Название заголовка не чувствительно к регистру. Когда вы используете методы Add()
, Get()
, Set()
и Del()
в карте заголовков, название заголовка всегда будет обработано с помощью функции textproto.CanonicalMIMEHeaderKey()
. Эта функция преобразует первую букву и любую букву после дефиса в верхний регистр, а остальные буквы в нижний.
Если требуется избежать такого поведения, можно напрямую отредактировать базовую карту заголовков (у нее тип map[string][]string
). Например:
1 |
w.Header()["X-XSS-Protection"] = []string{"1; mode=block"} |
На заметку: Если используется соединение HTTP/2, Go всегда автоматически конвертирует названия заголовков в нижний регистр в соответствии со спецификациями HTTP/2.
Удаление системных HTTP заголовков
Метод Del()
не удаляет заголовки, сгенерированные системой по умолчанию. Чтобы подавить их, требуется напрямую получить доступ к базовой карте заголовков и указать значение nil для нужного вам заголовка. К примеру, если нужно подавить заголовок Date
, нужно использовать следующий код:
1 |
w.Header()["Date"] = nil |
Полный исходный код веб-приложения
Ниже опубликован код нашего веб приложения из цикла уроков по созданию сайта на go на текущий момент.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
package main import ( "log" "net/http" ) // Обработчик главной странице. func home(w http.ResponseWriter, r *http.Request) { // Проверяется, если текущий путь URL запроса точно совпадает с шаблоном "/". Если нет, вызывается // функция http.NotFound() для возвращения клиенту ошибки 404. // Важно, чтобы мы завершили работу обработчика через return. Если мы забудем про "return", то обработчик // продолжит работу и выведет сообщение "Привет из SnippetBox" как ни в чем не бывало. if r.URL.Path != "/" { http.NotFound(w, r) return } w.Write([]byte("Привет из Snippetbox")) } // Обработчик для отображения содержимого заметки. func showSnippet(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Отображение заметки...")) } // Обработчик для создания новой заметки. func createSnippet(w http.ResponseWriter, r *http.Request) { // Используем r.Method для проверки, использует ли запрос метод POST или нет. Обратите внимание, // что http.MethodPost является строкой и содержит текст "POST". if r.Method != http.MethodPost { // Используем метод Header().Set() для добавления заголовка 'Allow: POST' в // карту HTTP-заголовков. Первый параметр - название заголовка, а // второй параметр - значение заголовка. w.Header().Set("Allow", http.MethodPost) // Используем функцию http.Error() для отправки кода состояния 405 с соответствующим сообщением. http.Error(w, "Метод запрещен!", 405) // Затем мы завершаем работу функции вызвав "return", чтобы // последующий код не выполнялся. return } w.Write([]byte("Создание новой заметки...")) } func main() { // Регистрируем два новых обработчика и соответствующие URL-шаблоны в // маршрутизаторе servemux mux := http.NewServeMux() mux.HandleFunc("/", home) mux.HandleFunc("/snippet", showSnippet) mux.HandleFunc("/snippet/create", createSnippet) log.Println("Запуск веб-сервера на http://127.0.0.1:4000") err := http.ListenAndServe(":4000", mux) log.Fatal(err) } |
Администрирую данный сайт с целью распространения как можно большего объема обучающего материала для языка программирования Go. В IT с 2008 года, с тех пор изучаю и применяю интересующие меня технологии. Проявляю огромный интерес к машинному обучению и анализу данных.
E-mail: vasile.buldumac@ati.utm.md
Образование
Технический Университет Молдовы (utm.md), Факультет Вычислительной Техники, Информатики и Микроэлектроники
- 2014 — 2018 Universitatea Tehnică a Moldovei, ИТ-Инженер. Тема дипломной работы «Автоматизация покупки и продажи криптовалюты используя технический анализ»
- 2018 — 2020 Universitatea Tehnică a Moldovei, Магистр, Магистерская диссертация «Идентификация человека в киберпространстве по фотографии лица»