Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 8 из 25 ~20 мин +50 XP

Кастомные middleware в Echo: фабрика, Bearer-аутентификация и метрики

echo кастомный middleware — это звено, которое вы пишете сами, когда каталога из [прошлого урока](/courses/echo/lessons/07-vstroennye-middleware/) перестаёт хватать: токены проверяются по вашей базе, метрики нужны с бизнес-размерностью, а правило проекта обязано исполняться на каждом маршруте без исключений. Анатомия та же, что у самодельного логгера из [урока про цепочки](/courses/echo/lessons/06-middleware/): func(next echo.HandlerFunc) echo.HandlerFunc, возврат замыкания func(c echo.Context) error и вызов next(c) — никакого c.Next в Echo нет. Соберём пять звеньев: фабрику метрик с состоянием, echo auth middleware на Bearer-токене, передачу данных через c.Set/c.Get, сквозную валидацию и скипер по образцу штатных конфигов.

Содержание урока · 12 разделов

echo кастомный middleware: когда каталога мало#

Каталог встроенных слоёв закрывает то, что одинаково у всех проектов: логи, recovery, сжатие, идентификатор запроса. Собственный слой нужен там, где логика привязана к вашему домену: проверка токена по таблице сессий в базе, лимиты на пользователя из вашего хранилища, счётчики с бизнес-размерностью вроде «запросов на тенианта», сквозные правила API. Три надёжных признака того, что код пора оформить звеном. Первый: один и тот же фрагмент повторяется в трёх и более обработчиках — и уже начал расходиться между копиями. Второй: правило должно исполняться ДО обработчика — отказ на границе дешевле, чем отказ посреди транзакции. Третий: данные, посчитанные один раз на запрос, нужны нескольким звеньям сразу — классический пример это пользователь, выясненный аутентификацией.

Свой echo кастомный middleware проходит ту же анатомию, что loggerTimer из урока про цепочки: внешняя функция принимает следующий обработчик и возвращает замыкание, замыкание исполняется на каждый запрос, вызов следующего звена — next(c). Напомним одной строкой и миф из того же урока: метода c.Next() в Echo не существует — это API Gin и Fiber, и в рецептах про echo он всегда признак скопированного кода. План урока такой: сначала фабрика с конфигом — почему без неё не обойтись и где в ней жить состоянию; затем Bearer-аутентификация с отказом 401, передача пользователя через c.Set/c.Get, сквозная валидация, снятая с домашнего задания, и скипер по стандартам каталога.

Фабрика с конфигом: три ступени звена#

Голую функцию func(next echo.HandlerFunc) echo.HandlerFunc вы уже писали: loggerTimer в уроке про цепочки был именно ей. Для звена с настройками и состоянием оформляйте фабрику — функцию, которая принимает конфиг и возвращает echo.MiddlewareFunc, ровно как штатные фабрики каталога: Logger() и LoggerWithConfig(config) из урока про встроенные слои. Структура повторяет XConfig-паттерн: своя структура конфига, резолв дефолтов, возврат слоя.

Смотрите внимательно: у фабрики три ступени, и живут они по-разному. Первая ступень — тело самой фабрики: она исполняется один раз, в момент e.Use(metricsTimer(...)), потому что вызываете её вы сами. Здесь резолвятся дефолты, проверяется конфиг и объявляется состояние. Вторая ступень — возвращённый слой func(next echo.HandlerFunc) echo.HandlerFunc: его на каждый запрос вызывает applyMiddleware из echo.go, оборачивая найденный обработчик, — там нельзя заводить переменные, которыми хочет жить слой между запросами. Третья ступень — замыкание func(c echo.Context) error: само звено, тело которого исполняется один раз на запрос. Это важный контраст с Gin, где цепочка склеивается в момент регистрации маршрута: в Echo обёртка строится на каждый запрос — цена копеечная, пара замыканий, но правило размещения состояния другое. Свежая деталь из исходников v4: часть фабрик каталога на невалидном конфиге паникует прямо на старте — KeyAuth без Validator падает с текстом echo: key-auth middleware requires a validator function. Берите приём на вооружение: ошибка конфигурации должна стрелять при подключении, а не на первом запросе.

metricsTimer: метрики со счётчиками в фабрике

GO
// метрики: состояние объявлено в теле фабрики —
// она исполняется один раз при e.Use
type MetricsConfig struct {
        Prefix string // метка в строке вывода
}

func metricsTimer(cfg MetricsConfig) echo.MiddlewareFunc {
        // ступень 1: один раз при подключении — здесь живёт память слоя
        var (
                mu      sync.Mutex // запросы обслуживаются конкурентно
                total   int64
                errored int64
                elapsed time.Duration
        )
        if cfg.Prefix == "" {
                cfg.Prefix = "metrics" // дефолт резолвим на старте
        }

        // ступень 2: слой — applyMiddleware зовёт его на каждый запрос
        return func(next echo.HandlerFunc) echo.HandlerFunc {
                // ступень 3: само звено — один раз на запрос
                return func(c echo.Context) error {
                        start := time.Now() // фаза запроса

                        err := next(c) // дальше по цепочке — к обработчику

                        // фаза ответа: статус и время уже известны
                        latency := time.Since(start)
                        mu.Lock()
                        total++
                        elapsed += latency
                        if err != nil {
                                errored++
                        }
                        mu.Unlock()
                        fmt.Printf("%s: total=%d err=%d avg=%s last=%s\n",
                                cfg.Prefix, total, errored,
                                elapsed/time.Duration(total), latency)

                        if err != nil {
                                c.Error(err) // глобальный обработчик оформит ответ
                        }
                        return err
                }
        }
}

// подключение в main():
e.Use(metricsTimer(MetricsConfig{Prefix: "api"}))

Три ступени помечены комментариями — сверьтесь, где объявлено состояние: в теле фабрики, а не в слое. mu защищает счётчики: сервер обслуживает соединения конкурентно, у каждого соединения своя горутина, и две одновременные записи в int64 без мьютекса — гонка данных (проверяется go run -race). avg считается от накопленной суммы — средняя задержка за жизнь процесса. c.Error(err) до печати — канон loggerTimer из урока 06: обработчик ошибок фиксирует ответ, и строка метрик видит честный статус. Про песочницу: как и весь курс Echo, этот урок не исполняется браузерным движком сайта — runner-ов здесь нет и не будет, код запускается локально: go mod init, go get github.com/labstack/echo/v4, затем go run и curl.

Латентность и статус: что видно после next(c)#

Метрики живут по обе стороны next(c), и у каждой стороны свои возможности. До next(c) доступна фаза запроса: метод, путь, заголовки — можно поставить метку start := time.Now(). После next(c) наступает фаза ответа: время считается time.Since(start), ошибка внутренних слоёв лежит в err, а c.Response().Status содержит статус ответа — но только если ответ к этому моменту оформлен. Вот тонкость, на которой ломаются самодельные метрики. Когда нижнее звено, например аутентификация, возвращает echo.NewHTTPError и не вызывает next(c), ответа ещё нет: файл response.go при сбросе контекста ставит Status в http.StatusOK, и до оформления ответа там будет дефолтный 200. Оформлением занимается глобальный HTTPErrorHandler — строка if err := h(c); err != nil { e.HTTPErrorHandler(err, c) } из ServeHTTP, — но она срабатывает ПОСЛЕ выхода из вашего слоя. Поэтому loggerTimer из урока про цепочки и штатный Logger зовут c.Error(err) до печати: этот вызов тут же проводит ошибку через обработчик, ответ фиксируется, и статус в c.Response().Status становится настоящим — 401, 400, 500. Пропустите c.Error — и метрика молча запишет отказ аутентификации как двухсотый ответ.

Слой без c.Error: какой статус напечатается

Прочитайте код и выберите, что он выведет:

// метрики-слой, но c.Error(err) кто-то удалил:
func metricsTimer(next echo.HandlerFunc) echo.HandlerFunc {
        return func(c echo.Context) error {
                start := time.Now()

                err := next(c) // bearerAuth ниже вернёт 401-ошибку

                fmt.Printf("%s %s -> %d %s\n",
                        c.Request().Method, c.Path(),
                        c.Response().Status, time.Since(start))
                return err
        }
}

// e.Use(metricsTimer)
// api := e.Group("/api", bearerAuth(AuthConfig{Token: "dev-token-42", Username: "alice"}))
// curl -i localhost:1323/api/profile   (без заголовка Authorization)

echo auth middleware: Bearer-токен из заголовка Authorization#

Запрос «echo auth middleware» в поиске — это почти всегда Bearer-схема: заголовок Authorization вида Bearer <token>, стандарт RFC 6750, так ходят токены OAuth 2.0 и JWT. Соберём заглушку своими руками — а заменой настоящей проверке она станет после того, как сравнение токена подменится походом в базу сессий. Механика фазы запроса: достать заголовок через c.Request().Header.Get(echo.HeaderAuthorization) — Echo не нужна своя обёртка, заголовок живёт в обычном *http.Request; проверить префикс strings.HasPrefix(auth, "Bearer ") и срезать его strings.TrimPrefix-ом; сравнить токен с ожидаемым. Отказ — return echo.NewHTTPError(http.StatusUnauthorized, ...): ошибка без вызова next(c), клиент получает 401 с телом {"message":...} от глобального обработчика. Подробная анатомия HTTPError — Message как interface{}, поле Internal, собственный ErrorHandler с единым форматом — тема десятого урока, там и продолжим. Две честные оговорки наперёд. Первая: RFC 6750 советует при отказе ставить заголовок WWW-Authenticate: Bearer — штатный KeyAuth этого не делает, а ваша фабрика может, это одна строка перед return. Вторая: у каталога есть готовый middleware.KeyAuth с AuthScheme "Bearer" по умолчанию — своё звено оправдано, когда проверка ходит в вашу базу или кладёт в контекст больше одного значения.

bearerAuth: фабрика аутентификации

GO
// ключи контекста — константы, а не строковые литералы
const ctxUserKey = "auth.user"

// конфиг фабрики: по образцу XConfig из каталога
type AuthConfig struct {
        Token    string // «пароль» заглушки
        Username string // кем считаем владельца токена
}

func bearerAuth(cfg AuthConfig) echo.MiddlewareFunc {
        // ступень 1: конфиг проверяется один раз при подключении
        return func(next echo.HandlerFunc) echo.HandlerFunc {
                // ступень 2: слой — здесь состояния быть не должно
                return func(c echo.Context) error {
                        // фаза запроса: разбор заголовка Authorization
                        auth := c.Request().Header.Get(echo.HeaderAuthorization)
                        if !strings.HasPrefix(auth, "Bearer ") {
                                return echo.NewHTTPError(
                                        http.StatusUnauthorized,
                                        "нужен bearer-токен в заголовке Authorization",
                                )
                        }
                        token := strings.TrimPrefix(auth, "Bearer ")
                        if token != cfg.Token {
                                return echo.NewHTTPError(
                                        http.StatusUnauthorized,
                                        "неизвестный токен",
                                )
                        }

                        // успех: данные для звеньев ниже — и дальше по цепочке
                        c.Set(ctxUserKey, cfg.Username)
                        return next(c)
                }
        }
}

// подключение: защищённая группа из урока про цепочки
api := e.Group("/api", bearerAuth(AuthConfig{
        Token:    "dev-token-42",
        Username: "alice",
}))

Префикс проверяется с хвостовым пробелом — «Bearer » именно так: TrimPrefix срежет ровно схему, и токен останется без ведущего пробела. Имя заголовка в HTTP регистронезависимо: Header.Get приводит его к канонической форме, поэтому Authorization, AUTHORIZATION и authorization читаются одинаково — а вот само значение схемы сверяется строкой в точности. Отказ построен на echo.NewHTTPError без next(c): это прерывание цепочки из урока 06, обработчик не увидит запрос. При успехе в контекст кладётся имя пользователя под константой ctxUserKey — сразу после этого return next(c): success-ветка фабрики всегда заканчивается проходом дальше, иначе звено останется тупиком.

Проверка: где объявлять состояние звена

Вы хотите, чтобы middleware считал запросы на протяжении всей жизни процесса. Где объявить счётчик?

c.Set и c.Get: константы ключей и свежие нюансы#

Пользователь выяснен — как о нём узнают звенья ниже? Канал тот же, что в уроке про цепочки: c.Set(key, val) и c.Get(key) any поверх ленивой карты Map в контексте запроса, создание при первом c.Set. Там же разобрана и ok-форма чтения — v, ok := c.Get(key).(string), — повторять её не будем, но закрепим дисциплину, которая в файле с тремя и более звеньями становится обязательной. Ключи — только константы: const ctxUserKey = "auth.user" объявлен рядом с bearerAuth, и опечатка теперь ловится компилятором, а не молчаливой пустой строкой в обработчике. Префикс auth. — не магия, а способ видеть в автодополнении все ключи одного звена; validateCreate ниже добавит свой ctxTodoInputKey.

Два нюанса из актуальных исходников v4.15, которых не было в прошлом разговоре. Первый: доступ к store обёрнут в sync.RWMutex — c.Set берёт блокировку на запись, c.Get на чтение, так что чтение пользователя из параллельных запросов безопасно само по себе. Второй: store — поле контекста, а контексты Echo переиспользует через пул; Reset обнуляет store в nil, поэтому данные живут ровно один запрос и не перетекают в следующий. Читают положенное три потребителя: обработчик, сквозная валидация ниже по цепочке и — если вы помните урок про встроенные слои — CustomTagFunc штатного Logger, умеющая докинуть имя пользователя в строку лога.

Проверка: штатный KeyAuth на запросе без заголовка

Замените своё звено на штатное: e.Use(middleware.KeyAuth(validator)) с настройками по умолчанию. Прилетает запрос вовсе без заголовка Authorization. Что получит клиент?

Сквозная валидация: конвейер до обработчика#

В домашнем задании урока про цепочки был каркас: middleware, который прогоняет структуру через c.Validate и на ошибку возвращает 400, не доходя до обработчика. Развернём его в законченный сквозной конвейер привязки и проверки — тот самый bind → normalize → validate, который собирался в уроке про привязку и валидацию, переезжает из каждого обработчика в одно звено. Идея: звено привязывает тело запроса в TodoInput, нормализует и проверяет, кладёт готовую структуру в контекст и лишь потом зовёт next(c); обработчик достаёт результат через c.Get и занимается единственным своим делом — хранилищем и ответом. Три обработчика создания сущностей — и три копии конвейера превращаются в один e.POST(path, handler, validateCreate).

У дизайна есть цена, и о ней честно. Звено, которое делает c.Bind, обязано знать структуру — значит, оно прикрепляется к конкретным маршрутам (route-level, самый близкий к обработчику уровень из таблицы урока 06), а не вешается глобально. Маршрут перестаёт нести знание о теле запроса — это плюс к единообразию и минус к локальности: читая main, вы видите validateCreate в списке middleware, а не в теле обработчика. И помните правило урока 04: c.Bind читает тело для POST/PUT/PATCH, а для GET/DELETE — query; сквозное звено валидации создания живёт только на создающих маршрутах, оставляя query-разбор обработчикам чтения.

validateCreate: bind и validate в звене

GO
const ctxTodoInputKey = "validate.todoInput"

// сквозная валидация: подключается на создающих маршрутах
func validateCreate(next echo.HandlerFunc) echo.HandlerFunc {
        return func(c echo.Context) error {
                var in TodoInput // структура с validate-тегами из урока 05

                // POST: тело по Content-Type; битый JSON — 400
                if err := c.Bind(&in); err != nil {
                        return echo.NewHTTPError(
                                http.StatusBadRequest,
                                "тело должно быть корректным JSON",
                        )
                }
                in.Normalize() // TrimSpace заголовка до проверки правил

                // правила из e.Validator: customValidator уже упаковал
                // ValidationErrors в 400 с картой поле -> правило
                if err := c.Validate(&in); err != nil {
                        return err
                }

                // результат конвейера — звеньям ниже через контекст
                c.Set(ctxTodoInputKey, in)
                return next(c)
        }
}

func createTodo(c echo.Context) error {
        // ok-форма: звено выше могло не отработать
        in, ok := c.Get(ctxTodoInputKey).(TodoInput)
        if !ok {
                return echo.NewHTTPError(
                        http.StatusInternalServerError,
                        "validateCreate не положил TodoInput в контекст",
                )
        }

        // единственная забота обработчика — хранилище и ответ
        t := Todo{ID: nextID, Title: in.Title, Priority: in.Priority}
        nextID++
        todos = append(todos, t)

        return c.JSON(http.StatusCreated, t)
}

// в main(): e.POST("/todos", createTodo, validateCreate)

Ошибку c.Validate возвращаем без обёртки: customValidator из урока про привязку уже упаковал ValidationErrors в echo.NewHTTPError с картой деталей, и дефолтный обработчик доставит её клиенту как 400 {"message":{"Title":"required"}}. Ошибка c.Bind остаётся нашей — фиксированное сообщение, как в уроке 04. createTodo читает контекст ok-формой и отвечает 201: три строки bind-normalize-validate исчезли из тела, правила живут на struct-тегах, а обработчик не может получить невалидную структуру физически — до него она не доедет. Напоминание из урока 05: e.Validator должен быть назначен до регистрации маршрутов, иначе первый же c.Validate вернёт ErrValidatorNotRegistered и уедет в 500.

Валидный токен, невалидное тело

Прочитайте код и выберите, что он выведет:

// main():
e.Use(metricsTimer(MetricsConfig{})) // с c.Error(err) до печати

api := e.Group("/api", bearerAuth(AuthConfig{
        Token:    "dev-token-42",
        Username: "alice",
}))
api.POST("/todos", createTodo, validateCreate)

// curl -i -H "Authorization: Bearer dev-token-42"
//      -H "Content-Type: application/json"
//      -d '{"title":"   "}' localhost:1323/api/todos

Skipper в своём звене: паттерн каталога#

У каждого штатного конфига из урока про встроенные слои первое поле — Skipper: func(c echo.Context) bool, true — слой молча делает return next(c). Дефолт DefaultSkipper всегда возвращает false. Ваше звено обязано уметь то же самое: слой, который нельзя выключить, быстро начинают обходить стороной — health-чеки не должны требовать токен, метрики не должны попадать в свои же счётчики, публичные маршруты не должны знать про аутентификацию.

Интеграция в три действия. Первое: поле Skipper middleware.Skipper в своей структуре конфига — тип переиспользуем из пакета middleware, свой не заводим. Второе: резолв nil-а в теле фабрики — если скипер не задан, ставим middleware.DefaultSkipper; это та же ступень «один раз при подключении», что и для дефолтов конфига. Третье: первая строка замыкания — if cfg.Skipper(c) { return next(c) }: проверка дешёвая, выполняется на каждый запрос, и при true звено исчезает из цепочки, не потратив ни одной операции на разбор заголовков. Для сравнения путей берите c.Path() — шаблон маршрута вида /api/public/*, тот же, что у тега ${route} в штатном Logger, — либо c.Request().URL.Path с реальным путём запроса; разница проявляется на path-параметрах, где шаблон стабильнее.

bearerAuthWithConfig: дефолты, паника и скипер

GO
// конфиг дорос до скипера — тип из пакета middleware
type AuthConfig struct {
        Token    string
        Username string
        Skipper  middleware.Skipper // func(c echo.Context) bool
}

func bearerAuthWithConfig(cfg AuthConfig) echo.MiddlewareFunc {
        // ступень 1: резолв дефолтов — один раз при подключении
        if cfg.Skipper == nil {
                cfg.Skipper = middleware.DefaultSkipper // всегда false
        }
        if cfg.Token == "" {
                // ошибка конфигурации должна стрелять на старте,
                // как у штатного KeyAuth без Validator
                panic("bearer-auth: пустой токен в конфиге")
        }

        return func(next echo.HandlerFunc) echo.HandlerFunc {
                return func(c echo.Context) error {
                        // первая строка замыкания: путь пропущен —
                        // слой не работает вовсе
                        if cfg.Skipper(c) {
                                return next(c)
                        }
                        // ...далее тело bearerAuth без изменений:
                        // разбор Authorization, проверка токена,
                        // c.Set и next(c)
                        return next(c)
                }
        }
}

// подключение: health и публичные пути мимо аутентификации
e.Use(bearerAuthWithConfig(AuthConfig{
        Token: "dev-token-42",
        Skipper: func(c echo.Context) bool {
                p := c.Path() // шаблон маршрута, как ${route} у Logger
                return p == "/healthz" || strings.HasPrefix(p, "/api/public")
        },
}))

Это та же фабрика bearerAuth, доведённая до стандарта каталога: Skipper-поле, nil-резолв, паника на бессмысленной конфигурации — пустой токен означал бы 401 на каждый запрос до первого curl. Скипер зовётся на каждый запрос из замыкания — держите его линейным сравнением строк, без походов в базу. Глобальное подключение со скипером публичных путей — альтернатива группе из урока 06: один слой закрывает всё API, а исключения перечисляет в одном месте, вместо того чтобы разрастаться списком групп.

Проверка: что делает слой при Skipper true

В AuthConfig задан скипер, возвращающий true для /healthz. Запрос пришёл на GET /healthz. Что происходит?

Скелеты звеньев урока: две фазы next(c)#

ЗвеноДо next(c)После next(c)Что кладёт в контекст
metricsTimerstart := time.Now()latency := time.Since(start); счётчики под мьютексом; c.Error(err) при ошибке; печать строкиничего
bearerAuthразбор Authorization, сравнение токена; отказ — 401 без next(c)ничего — success-ветка заканчивается next(c)ctxUserKey → имя пользователя
validateCreatec.Bind, Normalize(), c.Validate; отказ — 400 без next(c)ничегоctxTodoInputKey → TodoInput
штатный Logger (07)метка времени, буфер из sync.Pool, Skipperстрока лога с тегами ${...}; c.Error(err) при ошибкенет, но видит чужие: ${id}, ${custom}

Соберите bearerAuth заново

Заполните пропуски: типы MiddlewareFunc, скипер, константа заголовка, статус отказа и кладка в контекст.

func bearerAuth(cfg AuthConfig) echo.MiddlewareFunc { return func(next echo.) echo. { return func(c echo.Context) error { if cfg.(c) { return next(c) // путь пропущен: слой не работает } auth := c.Request().Header.Get(echo.) if !strings.HasPrefix(auth, "Bearer ") { return echo.NewHTTPError(http., "нужен bearer-токен") } if strings.TrimPrefix(auth, "Bearer ") != cfg.Token { return echo.NewHTTPError(http.StatusUnauthorized, "неизвестный токен") } c.(ctxUserKey, cfg.Username) // данные для звеньев ниже return next(c) } } }

Соберите main() с тремя уровнями подключения

Расставьте строки в правильном порядке: фабрика метрик — глобально и снаружи, аутентификация — на группе, валидация — на маршруте. Помните правило урока 06: тот, кто должен видеть всё, добавляется первым.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Что дальше: логи и ошибки#

Мы собрали три своих звена и ни разу не вышли за пределы модели «до next(c) — после»: фабрика с конфигом и состоянием на первой ступени, Bearer-аутентификация с отказом 401 и кладкой в контекст, сквозная валидация, снявшая конвейер с обработчиков. Порядок сборки из order-задания — репетиция реального main: сначала то, что видит всё, потом то, что отклоняет, потом то, что готовит данные. Единственный шаг от заглушек к бою — подмена сравнения токена походом в базу и fmt.Printf на настоящий логгер: куда именно вести строки метрик и как переходить на структурные логи со slog — тема следующего, девятого урока про логирование.

Вторая линия на вырост — ошибки. Все отказы урока построены на echo.NewHTTPError, но у HTTPError есть поле Internal для исходной причины, Message принимает не только строки, а собственный ErrorHandler собирает единый формат для всего API — разбор ошибок целиком занимает десятый урок. Пока же проверьте себя карточками, пройдите fill и order ещё раз с закрытыми глазами и перенесите звенья в свой учебный сервер из прошлых уроков: метрики, аутентификация и валидация собираются в один файл без единой правки обработчиков. Навигация по курсу — на странице курса Echo, анатомия цепочки — в уроке про middleware и цепочки, каталог штатных слоёв — в уроке про встроенные middleware.

Карточки: кастомные middleware

Термин

echo.MiddlewareFunc

нажмите, чтобы перевернуть
Определение

type MiddlewareFunc func(next HandlerFunc) HandlerFunc в echo.go. Свой слой: func(next echo.HandlerFunc) echo.HandlerFunc с возвратом замыкания func(c echo.Context) error. Следующий слой вызывается как next(c) — метода c.Next в Echo нет. applyMiddleware строит обёртку на каждый запрос.

1 / 6

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • Свой middleware — func(next echo.HandlerFunc) echo.HandlerFunc с возвратом замыкания func(c echo.Context) error; следующий слой зовётся next(c), никакого c.Next в Echo нет.
  • Фабрика с конфигом — три ступени: тело фабрики исполняется один раз при e.Use (дефолты, проверки, состояние), слой func(next) строится applyMiddleware-ом на каждый запрос, замыкание func(c) — само звено.
  • Состояние между запросами живёт только в теле фабрики; счётчики, объявленные в слое func(next), создаются заново на каждый запрос, — loggerTimer из урока 06 не пострадал: у него состояния не было.
  • Метрики-таймер: start до next(c), latency := time.Since(start) после; конкурентные запросы — мьютекс вокруг счётчиков; c.Error(err) до печати статуса, иначе увидите дефолтный 200 вместо честного 401.
  • bearerAuth: Bearer-токен из заголовка Authorization, отказ — echo.NewHTTPError(401) без next(c), успех — c.Set(ctxUserKey, user) и next(c); штатный KeyAuth умеет Bearer из коробки, но missing-ключ отвечает 400, а invalid — 401.
  • Сквозная валидация: c.Bind → Normalize() → c.Validate в звене до обработчика, отказ 400, готовый TodoInput через c.Set; в обработчике остаётся только хранилище и ответ.
  • Скипер по паттерну каталога: поле Skipper в конфиге, nil-резолв в middleware.DefaultSkipper, первая строка замыкания if cfg.Skipper(c) { return next(c) }; сравнение путей по c.Path().

Домашнее задание

Дорастите звенья до боевых. Первое: добавьте в MetricsConfig поле SlowThreshold time.Duration и печатайте строку только для запросов медленнее порога — скипером исключите /healthz из счётчиков вовсе. Второе: напишите фабрику tenantAuth: идентификатор тенианта читается из заголовка X-Tenant-ID, проверка — по карте известных тениантов, при успехе имя кладётся в контекст константой, при отказе — 401 с заголовком WWW-Authenticate: Bearer. Третье: замените сравнение токена на subtle.ConstantTimeCompare и убедитесь curl-ом, что поведение не изменилось: запрос без заголовка и с неверным токеном даёт две строки метрик со статусом 401 (c.Error до печати!), запрос с правильным токеном и телом {"title":"купить хлеб","priority":3} — 201 и задачу в списке.

Личные заметкиПусто
Хранятся только в вашем браузере (Echo · Кастомные middleware в Echo: фабрика, Bearer-аутентификация и метрики)

Вопросы и ответы по уроку

Где объявлять состояние middleware, чтобы оно жило между запросами?

В теле фабрики — функции, которая возвращает echo.MiddlewareFunc. В Echo applyMiddleware из echo.go оборачивает обработчик на каждый запрос, поэтому переменные, объявленные в слое func(next echo.HandlerFunc) echo.HandlerFunc, создаются заново на каждый запрос и обнуляются. Тело фабрики исполняется один раз при e.Use(metricsTimer(...)), и захваченные его переменными замыкания живут столько, сколько живёт процесс. Голая функция без фабрики состояния между запросами не держит вовсе.

Как передать данные из middleware в обработчик?

Через c.Set и c.Get: аутентификация кладёт пользователя c.Set(ctxUserKey, user), обработчик читает ok-формой v, ok := c.Get(ctxUserKey).(string). Ключи объявляйте константами рядом со звеном — опечатка в строковом литерале ловится только в рантайме. Хранилище — ленивая карта Map, в актуальных v4 доступ под RWMutex, а Reset при переиспользовании контекста обнуляет её: данные живут ровно один запрос.

Нужно ли вызывать c.Error(err) в каждом своём middleware?

Нет: без него ответ всё равно оформится — ServeHTTP после выхода из цепочки передаст ошибку глобальному HTTPErrorHandler. c.Error нужен там, где дальше по цепочке кто-то печатает статус или пишет заголовки до выхода: штатный Logger и наш metricsTimer зовут его до печати, чтобы в строке был настоящий 401, а не дефолтный 200. Если ваш слой ничего не печатает, достаточно сохранить err и вернуть его наверх.

Термины урока

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

Сравните с альтернативами

Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.

Следующая ступень учебного пути

GORM25 уроков~14 чпрактика25 уроков уже доступны

GORM: ORM-библиотека для Go

Курс GORM: модели, CRUD, ассоциации, транзакции, хуки, пагинация, оптимизация запросов. 25 уроков с примерами и проектом слоя данных.

  • Подключать БД и настраивать пул соединений
  • Описывать модели и связи: 1:1, 1:N, M:N
  • Писать CRUD и сложные выборки chainable-стилем
  • Контролировать N+1 через Preload