- Пишем устойчивое веб-приложение
- Восстановление после паники
- Обработка ошибок
- Таймаут при обработке HTTP-запроса
- Выводы
Представим, что мы запустили проект, в котором серверная часть написана на Golang. Все идет хорошо: количество пользователей растет и бизнес развивается. Но в какой-то момент мы замечаем, что наше веб-приложение выключается без причины. Пользователи сталкиваются с ошибками, спрашивают о проблеме, но мы не можем определить причину.
В этом уроке мы рассмотрим самые популярные проблемы Go веб-приложений и способы их решения. Также узнаем, можно ли заранее обезопасить себя от неожиданных сбоев.
Пишем устойчивое веб-приложение
Допустим, у нас есть веб-приложение, которое позволяет рассчитать время между двумя датами. Код приложения выглядит следующим образом:
package main
import (
"github.com/gofiber/fiber/v2"
"time"
)
type (
// Структура HTTP-запроса на расчет диапазона дат
DateRangeRequest struct {
From Date `json:"from"`
To Date `json:"to"`
}
// Структура даты, которая хранит формат и значение
Date struct {
Value string `json:"value"`
Format string `json:"format"`
}
// Структура HTTP-ответа на расчет диапазона дат
// Хранит значение в секундах
DateRangeResponse struct {
SecondsRange int64 `json:"seconds_range"`
}
)
func main() {
webApp := fiber.New()
webApp.Post("/daterange", func(c *fiber.Ctx) error {
var req *DateRangeRequest
c.BodyParser(req)
from, _ := time.Parse(req.From.Format, req.From.Value)
to, _ := time.Parse(req.To.Format, req.To.Value)
return c.JSON(DateRangeResponse{
SecondsRange: int64(to.Sub(from).Seconds()),
})
})
webApp.Listen(":80")
}
Все работает отлично, пока в один день мы не внесли изменение в код приложения, которое отправляет HTTP-запросы. Из-за небольшой ошибки в коде, приложение стало отправлять некорректный JSON-формат в теле HTTP-запроса.
Воспроизведем такую ситуацию. Запускаем веб-приложение и отправляем HTTP-запрос с некорректным JSON-форматом в теле:
curl --location --request POST 'http://localhost/daterange' \
--header 'Content-Type: application/json' \
--data-raw '{'
Получаем сообщение, что ответа не было:
curl: (52) Empty reply from server
Ошибка в формате тела HTTP-запроса привела к тому, что наше веб-приложение перестало работать. В логах веб-приложения мы видим сообщение о панике:
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x2 addr=0x10 pc=0x104470750]
goroutine 34 [running]:
main.main.func1(0x10453dc40?)
/Users/john.doe/go/src/error-handling/main.go:36 +0x30
github.com/gofiber/fiber/v2.(*App).next(0x1400010ec80, 0x140002b0000)
/Users/john.doe/go/src/error-handling/vendor/github.com/gofiber/fiber/v2/router.go:132 +0x184
github.com/gofiber/fiber/v2.(*App).handler(0x1400010ec80, 0x104429338?)
/Users/john.doe/go/src/error-handling/vendor/github.com/gofiber/fiber/v2/router.go:159 +0x3c
github.com/valyala/fasthttp.(*Server).serveConn(0x140001126c0, {0x10458e998?, 0x14000286000})
...
Восстановление после паники
Паника — это критическая ошибка, которая останавливает работу всего Go-приложения. Она возникает в запущенном приложении, поэтому ее нельзя обнаружить во время компиляции.
Из-за того, что паника возникает в работающем приложении и приводит к полной остановке, следует максимально подготовить наше приложение к таким ситуациям. Для этого в Go есть механизм восстановление от паники.
В веб-приложениях принято устанавливать посредника для восстановления после паник. Добавим такого посредника в наше веб-приложение:
package main
import (
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/recover"
"time"
)
type (
// Структура HTTP-запроса на расчет диапазона дат
DateRangeRequest struct {
From Date `json:"from"`
To Date `json:"to"`
}
// Структура даты, которая хранит формат и значение
Date struct {
Value string `json:"value"`
Format string `json:"format"`
}
// Структура HTTP-ответа на расчет диапазона дат
// Хранит значение в секундах
DateRangeResponse struct {
SecondsRange int64 `json:"seconds_range"`
}
)
func main() {
webApp := fiber.New()
// Устанавливаем посредника, который будет
// восстанавливать веб-приложение после паники
webApp.Use(recover.New())
webApp.Post("/daterange", func(c *fiber.Ctx) error {
var req *DateRangeRequest
c.BodyParser(req)
from, _ := time.Parse(req.From.Format, req.From.Value)
to, _ := time.Parse(req.To.Format, req.To.Value)
return c.JSON(DateRangeResponse{
SecondsRange: int64(to.Sub(from).Seconds()),
})
})
webApp.Listen(":80")
}
Запускаем веб-приложение и пытаемся снова воспроизвести ситуацию с некорректным JSON-телом в HTTP-запросе. На этот раз получаем следующий ответ:
runtime error: invalid memory address or nil pointer dereference
Мы получили сообщение об ошибке, но веб-приложение продолжило работу. Это произошло благодаря посреднику recover.New()
, который восстановил веб-приложение после паники. Если мы попробуем отправить еще десять таких запросов, то веб-приложение все равно продолжит работать.
Наше веб-приложение стало устойчиво к паникам в коде, однако пользователи до сих пор получают ошибки с непонятным текстом. При этом мы ничего не видим в своих логах. Исправим обработку ошибок в приложении.
Обработка ошибок
В любом работающем веб-приложении всегда могут возникнуть ошибки. Это обусловлено тем, что веб-приложения — это сложные системы, в которых невозможно учесть все возможные сценарии. Поэтому Go-разработчикам важно предусмотреть возникновение как можно большего количества ошибок.
В Go ошибки представлены явным типом error
, и золотое правило Go-разработчика звучит как: Обрабатываем абсолютно все ошибки в коде приложения. Даже если кажется, что ошибка в какой-то части кода никогда не возникнет, мы все равно должны ее обработать. Код приложения будет часто меняться, и то, что невозможно сегодня, может произойти завтра.
Добавим обработку всех ошибок в нашем веб-приложении:
package main
import (
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/recover"
"github.com/sirupsen/logrus"
"time"
)
type (
// Структура HTTP-запроса на расчет диапазона дат
DateRangeRequest struct {
From Date `json:"from"`
To Date `json:"to"`
}
// Структура даты, которая хранит формат и значение
Date struct {
Value string `json:"value"`
Format string `json:"format"`
}
// Структура HTTP-ответа на расчет диапазона дат
// Хранит значение в секундах
DateRangeResponse struct {
SecondsRange int64 `json:"seconds_range"`
}
)
func main() {
webApp := fiber.New()
// Устанавливаем посредника, который будет
// восстанавливать веб-приложение после паники
webApp.Use(recover.New())
webApp.Post("/daterange", func(c *fiber.Ctx) error {
var req *DateRangeRequest
if err := c.BodyParser(&req); err != nil {
logrus.WithError(err).Info("body parser")
return c.Status(fiber.StatusBadRequest).SendString("bad JSON")
}
from, err := time.Parse(req.From.Format, req.From.Value)
if err != nil {
logrus.WithError(err).Info("parse 'from' date")
return c.Status(fiber.StatusUnprocessableEntity).SendString("bad 'from' date")
}
to, err := time.Parse(req.To.Format, req.To.Value)
if err != nil {
logrus.WithError(err).Info("parse 'to' date")
return c.Status(fiber.StatusUnprocessableEntity).SendString("bad 'to' date")
}
return c.JSON(DateRangeResponse{
SecondsRange: int64(to.Sub(from).Seconds()),
})
})
lErr := webApp.Listen(":80")
if lErr != nil {
logrus.WithError(lErr).Fatal("listen port")
}
}
Запускаем веб-приложение и отправляем HTTP-запрос с невалидным JSON в теле:
curl --location --request POST 'http://localhost/daterange' \
--header 'Content-Type: application/json' \
--data-raw '{'
В ответ получаем сообщение об ошибке:
HTTP/1.1 400 Bad Request
bad JSON
Теперь отправителю HTTP-запроса понятно, в чем заключается ошибка.
Попробуем отправить корректный JSON в теле HTTP-запроса, но передать невалидные даты:
curl --location --request POST 'http://localhost/daterange' \
--header 'Content-Type: application/json' \
--data-raw '{
"from": {
"format": "test",
"value": "25"
},
"to": {
"format": "test",
"value": "25"
}
}'
Получаем в ответ сообщение:
HTTP/1.1 422 Unprocessable Entity
bad 'from' date
Отправитель HTTP-запроса поймет, что неправильно передал значение в поле from.
Проверим, что наше веб-приложение работает при корректном запросе:
curl --location --request POST 'http://localhost/daterange' \
--header 'Content-Type: application/json' \
--data-raw '{
"from": {
"format": "Jan 2, 2006 at 3:04pm (MST)",
"value": "Nov 1, 2022 at 12:59pm (PST)"
},
"to": {
"format": "Jan 2, 2006 at 3:04pm (MST)",
"value": "Nov 2, 2022 at 12:59pm (PST)"
}
}'
В ответ приходит:
HTTP/1.1 200 OK
{"seconds_range":86400}
Мы сделали веб-приложение более устойчивым к ошибкам в коде и неправильным запросам. В случае паники, веб-приложение будет само восстанавливаться и продолжать работу. При неправильном HTTP-запросе отправитель получит понятное сообщение об ошибке, а мы увидим в логах подробную информацию о том, что произошло.
Мы уже сделали много работы, но осталась еще одна потенциальная проблема, которую стоит учитывать при разработке веб-приложений. У нас отсутствуют таймауты при обработке HTTP-запросов.
Таймаут при обработке HTTP-запроса
Представим, что наш конкурент решил воспользоваться противозаконным методом и нанял команду хакеров, чтобы они остановили наше веб-приложение. Допустим, они узнали, что у нас нет таймаутов на обработку HTTP-запросов. Тогда они написали программу, которая отправляла в веб-приложение HTTP-запрос, но делало это очень медленно — по одному байту в секунду.
Получилась ситуация, когда количество соединений к нашему веб-приложению бесконечно растет, а обработчики не могут сработать, потому что тело HTTP-запроса получено не полностью. Спустя какое-то время, наше приложение перестает отвечать на HTTP-запросы, и весь сервер может выйти из строя.
Чтобы избежать такой ситуации, нам стоит настроить таймауты при обработке HTTP-запросов. Эта настройка укажет веб-приложению закрывать соединения спустя заданное время, если оно не получило полностью тело HTTP-запроса.
Добавим настройку таймаутов в наш код:
func main() {
// Создаем новый экземпляр Fiber веб-приложения
// Указываем таймаут на чтение HTTP-запросов в 3 секунды
// Указываем таймаут на запись HTTP-запросов в 3 секунды
webApp := fiber.New(fiber.Config{
ReadTimeout: 3 * time.Second,
WriteTimeout: 3 * time.Second,
})
...
}
Таймауты настраиваются через передачу параметров в конструктор fiber.New()
. Мы указали таймаут на чтение HTTP-запросов и запись HTTP-ответов в три секунды. Если спустя заданное время не произойдет ни одного действия, то соединение будет закрыто.
С помощью настройки таймаутов на обработку HTTP-запросов мы обезопасили веб-приложение от еще одной уязвимости, которая может вывести веб-приложение из строя.
Выводы
- В Go веб-приложениях может возникать состояние критической ошибки — паника, и важно всегда иметь посредника, который будет восстанавливать веб-приложение
- Все ошибки, которые возвращаются из функций, должны быть обработаны. Так разработчики и пользователи будут понимать, что пошло не так
- В Go веб-приложениях нужно настроить таймауты на обработку HTTP-запросов. Это поможет избежать уязвимости, которая может вывести веб-приложение из строя
Дополнительные материалы
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.