Зарегистрируйтесь, чтобы продолжить обучение

Валидация HTTP-запросов Веб-разработка на Go

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

В этом уроке мы разберем, как проверять HTTP-запросы в Go. Это важно, потому что проверки позволяют избежать ошибок и обеспечить безопасность нашего приложения.

Ручная проверка запросов в Go

Процесс проверки запросов на корректность перед последующей обработкой называется валидацией:

set

У разработчиков есть несколько вариантов реализации валидации в Go. В некоторых проектах придерживаются идеологии простоты чтения кода и реализуют валидацию вручную.

Например, валидация запроса на сохранение поста может выглядеть следующим образом:

package main

import (
    "errors"
    "fmt"
    "github.com/gofiber/fiber/v2"
    "github.com/sirupsen/logrus"
)

type CreatePostRequest struct {
    UserID int64  `json:"user_id"`
    Text   string `json:"text"`
}

func (req *CreatePostRequest) Validate() error {
    if req.UserID < 0 {
        return errors.New("user ID cannot be less than 0")
    }
    if req.Text == "" {
        return errors.New("text is empty")
    }
    if len(req.Text) > 140 {
        return errors.New("text is too long")
    }

    return nil
}

func main() {
    webApp := fiber.New()

    webApp.Post("/posts", func(ctx *fiber.Ctx) error {
        // Парсинг JSON-строки из тела запроса в объект.
        var req CreatePostRequest
        if err := ctx.BodyParser(&req); err != nil {
            return fmt.Errorf("body parser: %w", err)
        }

        // Проверка запроса на корректность.
        err := req.Validate()
        if err != nil {
            return ctx.Status(fiber.StatusUnprocessableEntity).SendString(err.Error())
        }

        // @TODO Сохранение поста в хранилище.

        return ctx.SendStatus(fiber.StatusOK)
    })

    logrus.Fatal(webApp.Listen(":80"))
}

Запускаем веб-приложение и отправляем запрос на создание поста с некорректными данными:

curl --location --request POST 'http://localhost/posts' \
--header 'Content-Type: application/json' \
--data-raw '{"user_id": -1, "text": ""}'

В данном запросе мы указали некорректный идентификатор пользователя и пустой текст поста. В ответ получаем сообщение об ошибке:

 HTTP/1.1 422 Unprocessable Entity

user ID cannot be less than 0

Попробуем отправить корректные значения:

curl --location --request POST 'http://localhost/posts' \
--header 'Content-Type: application/json' \
--data-raw '{"user_id": 100, "text": "Hello, World!"}'

В ответ приходит статус 200 OK, что означает успешное прохождение проверок.

Таким образом, мы настроили проверку запросов, которые приходят в наше веб-приложение. Если запрос на создание поста содержит некорректные данные, то мы возвращаем ошибку. В случае успешной валидации запроса мы возвращаем ответ со статусом 200 OK.

Со стороны кода в данной реализации все проверки описаны явно и легко читаются. Когда новый разработчик присоединится к проекту, он быстро поймет логику приложения, и как проверяются запросы.

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

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

Чтобы решить эти недочеты, следует использовать готовую библиотеку для валидации запросов. Мы рассмотрим самую часто используемую библиотеку в Go — go-playground/validator. Далее будем ее называть Validator.

Валидация запросов с помощью Validator

Библиотека Validator позволяет реализовать валидацию запросов с помощью аннотаций полей структур. Для каждого поля структуры мы описываем список правил проверок, которые необходимо осуществить. Например, валидация запроса на публикацию поста может выглядеть следующим образом:

package main

import (
    "fmt"
    "github.com/go-playground/validator/v10"
    "github.com/gofiber/fiber/v2"
    "github.com/sirupsen/logrus"
)

type CreatePostRequest struct {
    // Описываем правила валидации в аннотациях полей структуры.
    UserID int64  `json:"user_id" validate:"required,min=0"`
    Text   string `json:"text" validate:"required,max=140"`
}

func main() {
    webApp := fiber.New()

    validate := validator.New()

    webApp.Post("/posts", func(ctx *fiber.Ctx) error {
        // Парсинг JSON-строки из тела запроса в объект.
        var req CreatePostRequest
        if err := ctx.BodyParser(&req); err != nil {
            return fmt.Errorf("body parser: %w", err)
        }

        // Проверка запроса на корректность.
        err := validate.Struct(req)
        if err != nil {
            return ctx.Status(fiber.StatusUnprocessableEntity).SendString(err.Error())
        }

        // @TODO Сохранение поста в хранилище.

        return ctx.SendStatus(fiber.StatusOK)
    })

    logrus.Fatal(webApp.Listen(":80"))
}

Запускаем веб-приложение и отправляем запрос на создание поста с некорректными данными:

curl --location --request POST 'http://localhost/posts' \
--header 'Content-Type: application/json' \
--data-raw '{"user_id": -1, "text": ""}'

В данном запросе мы указали некорректный идентификатор пользователя и пустой текст поста. В ответ получаем сообщение об ошибке:

 HTTP/1.1 422 Unprocessable Entity

Key: 'CreatePostRequest.UserID' Error:Field validation for 'UserID' failed on the 'min' tag
Key: 'CreatePostRequest.Text' Error:Field validation for 'Text' failed on the 'required' tag

Попробуем отправить корректные значения:

curl --location --request POST 'http://localhost/posts' \
--header 'Content-Type: application/json' \
--data-raw '{"user_id": 100, "text": "Hello, World!"}'

В ответ приходит статус 200 OK, что означает успешное прохождение проверок.

Так мы реализовали валидацию запросов с помощью библиотеки Validator. Когда мы передаем некорректные данные, то в ответ получаем сообщение об ошибке. Оно позволяет понять, какие данные необходимо исправить. Если все данные заполнены правильно, то мы получаем статус 200 OK.

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

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

package main

import (
    "fmt"
    "github.com/go-playground/validator/v10"
)

type User struct {
    Email string `validate:"required,email"`
}

func main() {
    v := validator.New()

    // Вывод: Key: 'User.Email' Error:Field validation for 'Email' failed on the 'required' tag
    fmt.Println(v.Struct(&User{}))
    // Вывод: Key: 'User.Email' Error:Field validation for 'Email' failed on the 'email' tag
    fmt.Println(v.Struct(&User{Email: "test"}))
    // Пустой вывод, так как ошибки нет.
    fmt.Println(v.Struct(&User{Email: "test@gmail.com"}))
}

Полный список правил проверок можно смотреть в документации.

Пользовательские валидаторы

Готовые правила обычно покрывают большинство нужд в валидации запросов. Но иногда нужно добавить свои правила с пользовательской логикой. Для этого можно использовать функцию validate.RegisterValidation().

Например, мы хотим проверить, что в публикуемом посте отсутствуют слова-фильтры. Для этого мы напишем следующий код:

package main

import (
    "fmt"
    "github.com/go-playground/validator/v10"
    "github.com/gofiber/fiber/v2"
    "github.com/sirupsen/logrus"
    "log"
    "strings"
)

type CreatePostRequest struct {
    // Описываем правила валидации в аннотациях полей структуры.
    UserID int64  `json:"user_id" validate:"required,min=0"`
    Text   string `json:"text" validate:"required,max=140,allowable_text"`
}

var forbiddenWords = []string{
    "umbrella",
    "shinra",
}

func main() {
    webApp := fiber.New()

    validate := validator.New()
    vErr := validate.RegisterValidation("allowable_text", func(fl validator.FieldLevel) bool {
        // Проверяем, что текст не содержит запрещенных слов.
        text := fl.Field().String()
        for _, word := range forbiddenWords {
            if strings.Contains(strings.ToLower(text), word) {
                return false
            }
        }

        return true
    })
    if vErr != nil {
        log.Fatal("register validation ", vErr)
    }

    webApp.Post("/posts", func(ctx *fiber.Ctx) error {
        // Парсинг JSON-строки из тела запроса в объект.
        var req CreatePostRequest
        if err := ctx.BodyParser(&req); err != nil {
            return fmt.Errorf("body parser: %w", err)
        }

        // Проверка запроса на корректность.
        err := validate.Struct(req)
        if err != nil {
            return ctx.Status(fiber.StatusUnprocessableEntity).SendString(err.Error())
        }

        // @TODO Сохранение поста в хранилище.

        return ctx.SendStatus(fiber.StatusOK)
    })

    logrus.Fatal(webApp.Listen(":80"))
}

Запускаем веб-приложение и отправляем запрос на создание поста с текстом, который содержит слово-фильтр:

curl --location --request POST 'http://localhost/posts' \
--header 'Content-Type: application/json' \
--data-raw '{"user_id": 100, "text": "Hello Umbrella corp!"}'

В ответ получаем валидационную ошибку:

HTTP/1.1 422 Unprocessable Entity

Key: 'CreatePostRequest.Text' Error:Field validation for 'Text' failed on the 'allowable_text' tag

Если же отправить запрос с текстом без запрещенных слов, то получим успешный ответ:

curl --location --request POST 'http://localhost/posts' \
--header 'Content-Type: application/json' \
--data-raw '{"user_id": 100, "text": "Hello Good corp!"}'
HTTP/1.1 200 OK

В итоге мы описали пользовательское правило валидации по тегу allowable_text. Оно проверяет, что текстовое поле не содержит запрещенных слов.

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

Выводы

  • В веб-приложениях следует проверять все запросы на корректность, чтобы избежать ошибок и уязвимостей
  • В Go можно описывать правила валидации явно или с помощью готовых библиотек
  • Библиотека Validator позволяет описывать правила валидации с помощью аннотаций полей структур. Это позволяет сократить код и упростить его чтение и поддержку
  • Функция validate.RegisterValidation() позволяет описывать пользовательские правила валидации

Дополнительные материалы

  1. Go Validator Package
  2. Fiber Validation

Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»
Изображение Тото

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