Содержание урока · 12 разделов
- echo кастомный middleware: когда каталога мало
- Фабрика с конфигом: три ступени звена
- Латентность и статус: что видно после next(c)
- echo auth middleware: Bearer-токен из заголовка Authorization
- c.Set и c.Get: константы ключей и свежие нюансы
- Сквозная валидация: конвейер до обработчика
- Skipper в своём звене: паттерн каталога
- Скелеты звеньев урока: две фазы next(c)
- Что дальше: логи и ошибки
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
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 в звене
GOconst 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/todosSkipper в своём звене: паттерн каталога#
У каждого штатного конфига из урока про встроенные слои первое поле — 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) | Что кладёт в контекст |
|---|---|---|---|
| metricsTimer | start := time.Now() | latency := time.Since(start); счётчики под мьютексом; c.Error(err) при ошибке; печать строки | ничего |
| bearerAuth | разбор Authorization, сравнение токена; отказ — 401 без next(c) | ничего — success-ветка заканчивается next(c) | ctxUserKey → имя пользователя |
| validateCreate | c.Bind, Normalize(), c.Validate; отказ — 400 без next(c) | ничего | ctxTodoInputKey → TodoInput |
| штатный Logger (07) | метка времени, буфер из sync.Pool, Skipper | строка лога с тегами ${...}; c.Error(err) при ошибке | нет, но видит чужие: ${id}, ${custom} |
Для учебной заглушки token != cfg.Token — нормально. В бою сравнение строк оператором == уязвимо к timing-атакам: сравнение останавливается на первом несовпадающем байте, и по времени ответа можно вычислять корректный префикс токена посимвольно. Исходники Echo прямо предупреждают об этом в комментариях к KeyAuth и предлагают crypto/subtle: subtle.ConstantTimeCompare([]byte(token), []byte(cfg.Token)) == 1 — одинаковое время независимо от степени совпадения. Второе правило гигиены — не логировать Authorization: не подключайте к нему тег header:Authorization штатного Logger и не печатайте заголовок в своих звеньях; токен в логе — это скомпрометированная сессия.
Прежде чем заводить фабрику, загляните в каталог из урока 07: middleware.KeyAuth умеет Bearer из коробки — AuthScheme "Bearer" по умолчанию, ваш Validator ходит хоть в базу, хоть в сервис сессий; BasicAuth закрывает простые случаи; RateLimiter берёт ваш Store. JWT — отдельный модуль labstack/echo-jwt, и в курсе ему посвящён собственный урок. Своё звено оправдано, когда форматы действительно ваши: внутренние метки тениантов, подпись запроса, лимиты из собственного хранилища, комбинация проверки с метрикой. Правило то же, что у обработчиков: стандартная задача — штатный слой, доменная — фабрика.
Соберите bearerAuth заново
Заполните пропуски: типы MiddlewareFunc, скипер, константа заголовка, статус отказа и кладка в контекст.
Соберите main() с тремя уровнями подключения
Расставьте строки в правильном порядке: фабрика метрик — глобально и снаружи, аутентификация — на группе, валидация — на маршруте. Помните правило урока 06: тот, кто должен видеть всё, добавляется первым.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Что дальше: логи и ошибки#
Мы собрали три своих звена и ни разу не вышли за пределы модели «до next(c) — после»: фабрика с конфигом и состоянием на первой ступени, Bearer-аутентификация с отказом 401 и кладкой в контекст, сквозная валидация, снявшая конвейер с обработчиков. Порядок сборки из order-задания — репетиция реального main: сначала то, что видит всё, потом то, что отклоняет, потом то, что готовит данные. Единственный шаг от заглушек к бою — подмена сравнения токена походом в базу и fmt.Printf на настоящий логгер: куда именно вести строки метрик и как переходить на структурные логи со slog — тема следующего, девятого урока про логирование.
Вторая линия на вырост — ошибки. Все отказы урока построены на echo.NewHTTPError, но у HTTPError есть поле Internal для исходной причины, Message принимает не только строки, а собственный ErrorHandler собирает единый формат для всего API — разбор ошибок целиком занимает десятый урок. Пока же проверьте себя карточками, пройдите fill и order ещё раз с закрытыми глазами и перенесите звенья в свой учебный сервер из прошлых уроков: метрики, аутентификация и валидация собираются в один файл без единой правки обработчиков. Навигация по курсу — на странице курса Echo, анатомия цепочки — в уроке про middleware и цепочки, каталог штатных слоёв — в уроке про встроенные middleware.
Карточки: кастомные middleware
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +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 и задачу в списке.
Личные заметкиПусто
Вопросы и ответы по уроку
Где объявлять состояние 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 и вернуть его наверх.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
Следующая ступень учебного пути
GORM: ORM-библиотека для Go
Курс GORM: модели, CRUD, ассоциации, транзакции, хуки, пагинация, оптимизация запросов. 25 уроков с примерами и проектом слоя данных.
- Подключать БД и настраивать пул соединений
- Описывать модели и связи: 1:1, 1:N, M:N
- Писать CRUD и сложные выборки chainable-стилем
- Контролировать N+1 через Preload