Давайте обновим наше приложение, чтобы маршрут /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, Магистр, Магистерская диссертация «Идентификация человека в киберпространстве по фотографии лица»