---
title: "Middleware и цепочки в Echo: e.Use, next(c), порядок и прерывание"
description: "Middleware в Echo: сигнатура echo.MiddlewareFunc, e.Use и e.Pre, луковица вокруг next(c), c.Set/c.Get, прерывание цепочки и защищённая группа /admin."
course: "Echo: минималистичный быстрый фреймворк"
url: "https://golangmaster.ru/courses/echo/lessons/06-middleware/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 20
---

# Middleware и цепочки в Echo: e.Use, next(c), порядок и прерывание

*Курс «Echo: минималистичный быстрый фреймворк», урок 6 из 25. ~20 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/echo/lessons/06-middleware/*

Middleware в Echo — это обёртка над обработчиком: слой кода, который видит каждый запрос до того, как он дойдёт до вашей функции, и каждый ответ после. Один echo middleware заменяет десятки повторов: логи, таймеры, аутентификация, recovery. Разберём сигнатуру echo.MiddlewareFunc, подключение через e.Use и e.Pre, порядок луковицы, передачу данных через c.Set/c.Get, прерывание цепочки — и соберём связку логгер-таймер плюс аутентификация-заглушка на защищённой группе.

### echo middleware: зачем обёртка над обработчиком

В конце прошлого урока в каждом обработчике появилась одна и та же церемония: c.Bind, нормализация, c.Validate. А теперь представьте, что к этому добавилось логирование каждого запроса, замер времени ответа и проверка авторизации. Три if-а и два лога в каждом из двадцати обработчиков — и вот уже половина кода API это копипаста, которая расходится между обработчиками по мере правок.

Механизм echo middleware решает это структурно: слой — функция, которая принимает следующий обработчик цепочки и возвращает новый. Вы пишете логику один раз и подключаете её к маршруту, группе или всему серверу — а Echo сама проводит каждый запрос через все слои по очереди. Такая конструкция называется цепочка middleware: запрос проходит через слои снаружи внутрь, обработчик формирует ответ, и ответ идёт обратно через те же слои. Роутинг мы собирали в уроке про маршрутизацию, конвейер bind-validate — в уроке про привязку и валидацию; middleware — это третий слой архитектуры Echo, который оборачивает их все.

### Сигнатура MiddlewareFunc и миф про c.Next

В echo.go всё описано двумя строчками. type HandlerFunc func(c Context) error — так выглядят обработчики, к которым вы привыкли. А type MiddlewareFunc func(next HandlerFunc) HandlerFunc — middleware принимает следующий обработчик и возвращает новый. Смотрите, что здесь спрятано: в Echo следующий слой приходит вам аргументом next, и вы вызываете его как обычную функцию — next(c). Никакого метода c.Next() в Echo нет — ни в четвёртой версии, ни в пятой. c.Next() — это API Gin и Fiber, и его же пишут в старых постах про Echo по памяти о других фреймворках. Если такое встретите в рецепте про echo — рецепт либо устарел, либо скопирован из чужого фреймворка.

Второе, что стоит разглядеть в сигнатуре: MiddlewareFunc — это просто тип функции, и его удобно возвращать из фабрики. middleware.Logger() возвращает echo.MiddlewareFunc: сначала конфигурация, потом готовый слой. Ваши middleware будут строиться так же — либо голой функцией вида func(next echo.HandlerFunc) echo.HandlerFunc, либо фабрикой с настройками, которая возвращает такой же тип. Для совместимости с net/http-стеками в echo.go есть WrapMiddleware — он оборачивает классическую func(http.Handler) http.Handler в echo.MiddlewareFunc, но это уже мост в мир стандартной библиотеки, а не наш главный путь.

### Логгер-таймер: код до next(c) и после

```go
func loggerTimer(next echo.HandlerFunc) echo.HandlerFunc {
        return func(c echo.Context) error {
                start := time.Now() // фаза запроса: до прохода по цепочке

                err := next(c)      // дальше по цепочке — к обработчику
                if err != nil {
                        c.Error(err) // глобальный обработчик сформирует ответ
                }

                stop := time.Now() // фаза ответа: всё уже отработало
                fmt.Printf("%s %s -> %d %s\n",
                        c.Request().Method, c.Path(),
                        c.Response().Status, stop.Sub(start))
                return err
        }
}

// подключение в main():
e.Use(loggerTimer)
```

Три обязательные детали. Первая: return func(c echo.Context) error — middleware возвращает замыкание, и именно оно исполняется на каждый запрос; внешняя функция вызывается один раз при подключении. Вторая: err := next(c) — мы сохраняем ошибку, которую вернули внутренние слои, чтобы вернуть её наверх дальше. c.Error(err) зовёт глобальный HTTPErrorHandler прямо здесь, как это делает штатный middleware.Logger в исходниках Echo. Третья: строки после next(c) исполняются, когда внутренние слои уже закончили — c.Response().Status к этому моменту содержит реальный статус ответа, а stop.Sub(start) — честное время обработки. Импорты: fmt, time, net/http для полного файла. Про песочницу: Echo не входит в браузерный движок сайта, поэтому весь код урока запускается локально — go run и curl, как и в прошлых уроках курса.

### e.Use: порядок подключения — это порядок обёрток

e.Use добавляет middleware в общий стек сервера. Сам метод тривиален: e.middleware = append(e.middleware, middleware...) — всё, что вы передали (а это variadic, можно перечислить несколько через запятую), дописывается в конец среза. Но интересное — в том, как Echo из этого среза собирает цепочку. Внутри ServeHTTP сработает applyMiddleware: цикл идёт с конца среза к началу, и каждый элемент оборачивает текущий обработчик. Результат: последний добавленный слой оказывается ближайшим к обработчику, а самый первый добавленный — самым внешним.

Поэтому порядок вызовов e.Use — это не декорация, а архитектурное решение. e.Use(loggerTimer), затем e.Use(authStub) означает: логгер видит запрос первым и провожает его последним, аутентификация — второй и её строка «после» сработает раньше логгеровой. Если поменять вызовы местами — изменится всё: кто первый поставил метку времени, кто последним видит статус ответа, кто обрабатывает ошибки соседей. Док-комментарий к e.Use в echo.go лаконичен: adds middleware to the chain which is run after router — после маршрутизатора. Это не про порядок соседей, а про то, что маршрутизация уже случилась, когда слои начинают работать. Про то, что бывает до маршрутизатора, — отдельная история ниже.

### Какой порядок строк напечатает сервер

```go
// два middleware и обработчик:
func a(next echo.HandlerFunc) echo.HandlerFunc {
        return func(c echo.Context) error {
                fmt.Println("A: до")
                err := next(c)
                fmt.Println("A: после")
                return err
        }
}
// b — то же самое, печатает "B: до" и "B: после"

// в main():
e.Use(a)
e.Use(b)
e.GET("/", func(c echo.Context) error {
        fmt.Println("H: обработчик")
        return c.NoContent(http.StatusOK)
})
```

**Что выведет программа?**

- ✅ A: до, B: до, H: обработчик, B: после, A: после
- A: до, A: после, B: до, B: после, H: обработчик
- B: до, A: до, H: обработчик, A: после, B: после
- H: обработчик, A: до, B: до, B: после, A: после

a добавлен первым — он самый внешний: его строка «до» печатается раньше всех, его строка «после» — позже всех. b — второй, значит ближе к обработчику: вход после a, выход перед a. Обработчик — самое внутреннее звено. Порядок в срезе e.middleware — [a, b]; applyMiddleware оборачивает с конца, и b обёртывается вокруг обработчика первым, a — поверх всей конструкции.

### Луковица: до next(c) — запрос, после — ответ

Картину из predict-а называют луковицей: запрос проходит сквозь слои снаружи внутрь, обработчик сидит в центре, ответ выходит обратно сквозь те же слои. У каждого middleware поэтому две половины жизни. Код до next(c) работает в фазе запроса: здесь можно померить start, проверить аутентификацию, дополнить запрос данными, отклонить его совсем. Код после next(c) работает в фазе ответа: здесь есть итоговый статус, время обработки, и здесь можно дописать заголовки — при условии, что ответ ещё не отправлен клиенту.

Именно на этой модели стоят почти все штатные middleware Echo: таймер и статус у Logger-а живут по обе стороны next(c), у Recover-а — отложенный recover(), который ловит панику из внутренних слоёв, пока кадры unwind-ятся назад через цепочку. Отсюда же практическое правило: тот, кто должен видеть всё, добавляется первым. Логгер, добавленный последним, увидит только то, что не съели соседние слои. А тот, кто отклоняет запросы, ставится после логгера — тогда каждый отказ тоже попадёт в лог. Порядок в e.Use читайте как маршрут запроса снаружи внутрь.

### Аутентификация-заглушка на Basic Auth

```go
// заглушка: в реальных проектах пары берут из базы,
// а вместо Basic Auth ставят JWT или сессии
func authStub(next echo.HandlerFunc) echo.HandlerFunc {
        return func(c echo.Context) error {
                user, pass, ok := c.Request().BasicAuth()
                if !ok || user != "alice" || pass != "secret123" {
                        // прерывание цепочки: обработчик запрос не увидит
                        return echo.NewHTTPError(
                                http.StatusUnauthorized,
                                "нужна basic-аутентификация",
                        )
                }

                c.Set("username", user) // данные для звеньев ниже
                return next(c)
        }
}
```

Слева от if — фаза запроса: разбор заголовка Authorization методом BasicAuth() обычного *http.Request (Echo не нужна своя обёртка, чтобы достать заголовок). Не прошли проверку — возвращаем echo.NewHTTPError(http.StatusUnauthorized, ...) и next(c) не вызываем: это прерывание цепочки, обработчик и все слои ниже не исполнятся. Прошли — кладём имя пользователя в контекст методом c.Set и пропускаем запрос дальше. Возвращаемое из next(c) значение обязано уйти наверх без изменений: снаружи сидят другие слои, например наш loggerTimer, и они ждут эту ошибку.

### c.Set и c.Get: память на один запрос

Аутентификация выяснила, кто стучится, — как об этом узнает обработчик? Для таких передач между звеньями у echo.Context есть пара c.Set(key string, val any) и c.Get(key string) any. Внутри — карта Map (в echo.go это type Map map[string]interface{}), которая создаётся лениво при первом c.Set и лежит прямо в контексте запроса. Больше того, Echo хранит контексты в пуле и переиспользует их между запросами — c.Reset чистит хранилище, поэтому данные, положенные в c.Set, живут ровно один запрос и никогда не перетекут в соседний.

Важно читать контракт типов: c.Get возвращает any, а не строку. Доставать значение надо type assertion-ом, лучше ok-формой: name, ok := c.Get("username").(string). Без неё assert на неожиданном типе роняет программу паникой. Ключи — строки, и на реальном проекте их держат в константах (ctxUsername = "username"), чтобы опечатка в двух местах не превратилась в баг, который молча отдаст пустую строку. Примеры из самих исходников Echo: CSRF-middleware кладёт токен через c.Set("csrf", token), а роутер при 405 через c.Get достаёт заголовок Allow. Это штатный способ нести данные между звеньями, а не хак.

### Проверка: как достать то, что положили в c.Set

**Вопрос:** В authStub выполнено c.Set("username", "alice"). В обработчике нужно получить имя. Какая строка корректна?

- name := c.Get("username") — c.Get вернёт строку
- ✅ name, ok := c.Get("username").(string) — c.Get возвращает any, нужна ok-форма type assertion
- name := c.Param("username") — Set/Get и path-параметры это одно хранилище
- name := c.FormValue("username") — так Echo кладёт значения формы в контекст

Контракт c.Get — (key string) any: карта Map хранит interface{}. Прямое присваивание name := c.Get(...) не компилируется — нельзя присвоить any строке без приведения. Проверка через ok-форму безопасна: если ключа нет или тип другой, ok будет false, а name — пустой строкой, вместо паники. c.Param — это path-параметры маршрута (/users/:id), к c.Set/c.Get отношения не имеет.

### Защищённая группа и обработчик с c.Get

```go
func listUsers(c echo.Context) error {
        // ok-форма: ключ могли не установить выше
        name, ok := c.Get("username").(string)
        if !ok {
                return echo.NewHTTPError(
                        http.StatusInternalServerError,
                        "authStub не сработал: ключ username отсутствует в контексте",
                )
        }
        return c.JSON(http.StatusOK, echo.Map{
                "message": "служебный список пользователей",
                "from":    name,
        })
}
```

listUsers читает из контекста то, что положила аутентификация, и отвечает JSON-ом. echo.Map — сокращение из echo.go для map[string]interface{}: удобно для сборных ответов, хотя для стабильных контрактов, как мы решили в уроке про ответы, лучше структуры. Проверка ok — это страховка на случай, когда middleware забыли подключить: маршруты группы без authStub дойдут сюда с пустым контекстом, и лучше получить явную ошибку в логе, чем поле from без значения. Внутренние кавычки в сообщении ошибки — обычные, Go-строка в двойных кавычках.

### main(): глобальный слой и защищённая группа

```go
func main() {
        e := echo.New()

        // глобальный слой: каждый запрос, включая 404
        e.Use(loggerTimer)

        // группа с middleware: authStub получит только /admin/*
        admin := e.Group("/admin", authStub)
        admin.GET("/users", listUsers)

        // публичные маршруты: аутентификации нет
        e.GET("/todos", listTodos)
        e.POST("/todos", createTodo)

        e.Logger.Fatal(e.Start(":8080"))
}
```

Здесь встречаются три уровня подключения. e.Use(loggerTimer) — глобально: слой увидит вообще все запросы, включая те, что кончатся 404 — маршрутизатор в этом случае подставляет штатный NotFoundHandler, и цепочка всё равно проходит через глобальные слои. e.Group("/admin", authStub) — группа: middleware получает только маршруты, зарегистрированные через admin.GET/admin.POST и так далее; на /todos он не сработает. Третий уровень — route-level: e.GET(path, handler, authStub) — middleware для одного-единственного маршрута; он встаёт ближе всех к обработчику. Внутри группы порядок такой: глобальные слои снаружи, затем group-middleware, затем route-middleware, затем обработчик.

### Куда встаёт middleware: уровни и момент

| Способ | Момент в цепочке | Видит ли 404 | Типичное дело |
| --- | --- | --- | --- |
| e.Pre(mw) | до маршрутизации | да, и может изменить путь | переписать URL, добавить или убрать слэш |
| e.Use(mw) | после маршрутизации, снаружи всех | да — обработчиком станет NotFoundHandler | логи, таймеры, recovery, метрики |
| e.Group("/admin", mw) или g.Use(mw) | после глобальных, только для маршрутов группы | только внутри префикса: группа ставит catch-all | аутентификация, права доступа |
| e.GET(path, h, mw) | ближе всех к обработчику | нет | точечная логика одного маршрута |

### e.Pre: middleware до маршрутизации

e.Use-слои начинают работу после того, как роутер нашёл маршрут: в ServeHTTP сначала вызывается Find по дереву маршрутов, и только потом обработчик оборачивается стеком e.middleware. Значит, повлиять на сам выбор маршрута из них уже нельзя. Для этого есть e.Pre — premiddleware. Тот же тип MiddlewareFunc, та же луковица, но вставлена она раньше: premiddleware обёртывает не найденный обработчик, а замыкание, внутри которого происходит маршрутизация. Док-комментарий: adds middleware to the chain which is run before router.

Практическое применение у Pre узкое и честное: всё, что должно случиться до выбора маршрута. Штатные примеры — middleware.Rewrite, AddTrailingSlash и RemoveTrailingSlash: они правят r.URL.Path до того, как роутер сверится с таблицей, поэтому /admin/users/ и /admin/users попадают на один маршрут вместо 404 у одного из них. Всё остальное — логи, таймеры, аутентификация — живёт в e.Use: там уже известно, какой маршрут выбран, есть c.Path() и есть доступ к данным маршрута. Забавное следствие устройства: при несуществующем пути premiddleware и e.Use-слои всё равно исполнятся — 404 в Echo это тоже обработчик, просто штатный, а не ваш.

### Прерывание цепочки: return без next(c)

Пока каждый слой зовёт next(c), запрос движется вглубь. Но middleware имеет полное право этого не делать — и это главный рычаг контроля в echo middleware. Прерывание выглядит как обычный return. Вариант первый: return err — вернуть ошибку, не дойдя до обработчика. Ошибка начинает раскручивать цепочку обратно: каждый внешний слой получает её как результат next(c), может что-то сделать (залогировать, обернуть) и возвращает дальше. В самом конце ServeHTTP видит err != nil и зовёт глобальный HTTPErrorHandler: для *echo.HTTPError клиент получит её код и message в JSON, для любой другой ошибки — 500 Internal Server Error.

Вариант второй: написать ответ самому и вернуть nil. c.JSON(http.StatusUnauthorized, ...) прямо в middleware, return nil — клиент получит ровно ваш ответ, глобальный обработчик не вызовется, потому что ошибка не родилась. Оба варианта легальны, но первый лучше сочетается с остальной системой: HTTPError несёт статус и сообщение, обработчик оформит ответ единообразно, а логгер снаружи увидит ошибку в возвращаемом значении. Общее у обоих вариантов одно: звенья ниже не исполняются. Обработчик даже не узнает, что к нему кто-то стучался.

### Проверка: next(c) забыли, вернули nil

**Вопрос:** В middleware никто не вызвал next(c) и не записал ничего в ответ, функция вернула nil. Что увидит клиент?

- 500: Echo обнаружит пропущенный next(c) и посчитает это ошибкой сервера
- 404: запрос не дошёл до маршрутизатора
- ✅ 200 с пустым телом: статус никто не записал, net/http отправит дефолтный 200
- Программа не скомпилируется: next обязан вызываться

Компилятор проверяет только типы: next(c) — обычный вызов функции, и никакого контроля «вызван ли он» нет. Ошибки тоже нет — ServeHTTP получит nil и не запустит обработчик ошибок. Ответ никто не писал, значит WriteHeader не вызывался, и net/http по умолчанию отправит 200 с пустым телом. Это делает баги в middleware особенно тихими: сломанный слой, забывший next(c), выглядит для клиента как просто пустой ответ.

### Что покажет curl -i без аутентификации

```go
// цепочка из main() выше:
// e.Use(loggerTimer); admin := e.Group("/admin", authStub)
// curl -i localhost:8080/admin/users
// без заголовка Authorization
```

**Что выведет программа?**

- 200 OK и {"message":"служебный список пользователей"}: список отдаётся всем
- ✅ 401 Unauthorized и {"message":"нужна basic-аутентификация"}, а loggerTimer напечатает строку со статусом 401
- 500 Internal Server Error: ошибка из middleware не является HTTPError
- 404 Not Found: группа /admin перехватила несуществующий маршрут

authStub вернул *echo.HTTPError с кодом 401, не вызвав next(c). Ошибка раскрутилась до ServeHTTP, глобальный обработчик оформил её в JSON {"message": ...} со статусом 401. loggerTimer добавлен раньше группы — он самый внешний слой, поэтому его код после next(c) исполнился и напечатал GET /admin/users -> 401: c.Error(err) внутри логгера уже вызвал обработчик ошибок, а к моменту печати статус был зафиксирован. Двойного рендера не случится: DefaultHTTPErrorHandler начинается с проверки c.Response().Committed и выходит, не трогая готовый ответ.

### Порядок, который нельзя путать: логгер раньше recovery

Луковица делает некоторые порядки принципиально неправильными. Возьмём recovery — middleware с отложенным recover(), превращающий панику в ошибку 500. Паника раскручивает стек кадров по одному, и в каждом middleware код после next(c) просто не исполняется — кадр убит. Если recovery добавить раньше логгера (то есть recovery окажется снаружи), то паника из обработчика пролетит сквозь логгер, не дав ему напечатать строку, и остановится на recovery. Запрос уйдёт с 500, а в логе — ничего.

Именно поэтому в примере из доккомментария самого пакета echo порядок такой: e.Use(middleware.Logger()), затем e.Use(middleware.Recover()) — логгер снаружи, recovery внутри. Логгер печатает строку на каждый запрос, включая паникующий; recovery конвертирует панику в ошибку и возвращает её, логгер фиксирует статус 500. Тот же принцип для ваших слоёв: сначала то, что должно видеть всё, потом то, что может отклонить или уронить. И ещё один порядок в голове: c.Error(err) внутри middleware уже фиксирует ответ — после него писать в ответ поздно, Response становится committed, и любая следующая попытка WriteHeader даст в логе net/http предупреждение о superfluous WriteHeader.

> **next(c) можно вызвать дважды — и это не защита.** next — обычная функция, и никто не запретит вызвать её два раза: обработчик исполнится дважды, оба раза честно. Скажем, дважды вставится задача в хранилище. Той же природы и обратная ошибка: записать ответ в middleware, вернуть err наверх и позволить обработчику ошибок попытаться дописать — в лог прилетит superfluous response.WriteHeader, а клиенту уйдёт то, что успело отправиться первым. Дисциплина: один next(c) на ветку исполнения; записали ответ — возвращайте nil.

> **Skipper: как слою сказать «тут меня не звать».** Штатные middleware Echo не жёсткие: у каждого конфига есть поле Skipper — функция func(c echo.Context) bool, и если она вернула true, слой молча делает return next(c) (в middleware/middleware.go описан и DefaultSkipper, который всегда возвращает false). Вашим middleware можно дать такую же опцию: фабрика с конфигом, в конфиге Skipper, в начале замыкания проверка. Это спасает, когда health-check-запросы замусоривают лог или аутентификация не нужна на /healthz.

### Восстановите middleware аутентификации

Соберите authStub: типы middleware, доступ к запросу, статус ошибки и кладка данных в контекст.

```go
func authStub(next echo.___) echo.___ {
        return func(c echo.Context) error {
                user, pass, ok := c.___().BasicAuth()
                if !ok || user != "alice" || pass != "secret123" {
                        return echo.NewHTTPError(http.___, "нужна аутентификация")
                }
                c.___("username", user)
                return next(c)
        }
}
```

Подсказка: Сигнатура — MiddlewareFunc разворачивается в пару HandlerFunc; BasicAuth живёт на c.Request(); статус — 401; данные кладутся методом c.Set.

Правильные ответы в пропусках: `HandlerFunc`, `HandlerFunc`, `Request`, `StatusUnauthorized`, `Set`

### Соберите main() с цепочкой

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

Правильный порядок строк:

1. `func main() {`
2. `  e := echo.New()`
3. `  e.Use(loggerTimer)`
4. `  admin := e.Group("/admin", authStub)`
5. `  admin.GET("/users", listUsers)`
6. `  e.GET("/todos", listTodos)`
7. `  e.POST("/todos", createTodo)`
8. `  e.Logger.Fatal(e.Start(":8080"))`
9. `}`

### Что дальше: встроенные middleware и свой слой

Мы собрали руками то, что Echo даёт готовым: у пакета middleware есть Logger с конфигурируемым форматом, Recover с печатью стека, RequestID, Timeout, Gzip и ещё дюжина слоёв на все случаи — от CORS до rate limit; их разбор — тема следующего урока про встроенные middleware. Ваша связка логгер-таймер плюс authStub — не учебная игрушка: замена fmt.Printf на структурный логгер и заглушки на настоящую проверку паролей превращает её в боевой каркас.

И вернёмся к конвейеру из прошлого урока: bind → normalize → validate — три одинаковые строки в каждом обработчике. Это классический кандидат на обёртку: middleware, который прогоняет валидацию до того, как обработчик получит управление, — с Skipper-ом и передачей результата через c.Set. Попробуйте сделать это домашним упражнением — в задании ниже есть каркас. Навигация по курсу — на [странице курса Echo](/courses/echo/); устройство ответов и статусов, вокруг которых всё это крутится, разобрано в [уроке про запросы и ответы](/courses/echo/lessons/04-request-response/), а конвейер валидации, который теперь можно обернуть, — в [уроке про привязку и валидацию](/courses/echo/lessons/05-bind-validaciya/).

### Карточки: middleware и цепочки

- **echo.MiddlewareFunc** — type MiddlewareFunc func(next HandlerFunc) HandlerFunc: middleware принимает следующий слой и возвращает новый обработчик. Возвращается из фабрик вроде middleware.Logger(); c.Next() в Echo не существует — следующий слой зовётся next(c).
- **e.Use и e.Pre** — e.Use — слои после маршрутизации, глобально на все запросы (включая 404). e.Pre — до маршрутизации: умеет править r.URL.Path (Rewrite, слэши). Оба добавляют в конец среза; порядок добавления = порядок обёрток.
- **Луковица** — Запрос идёт снаружи внутрь через слои, ответ — обратно. Код до next(c) — фаза запроса, после — фаза ответа (статус и время уже известны). Первый добавленный слой — самый внешний: видит всех и первым, и последним.
- **c.Set / c.Get** — Карта Map в контексте запроса: передача данных между звеньями (CSRF так кладёт токен). c.Get возвращает any — нужна type assertion, лучше ok-формой. Живёт ровно один запрос: контексты пулятся и сбрасываются Reset-ом.
- **Прерывание цепочки** — Не вызвать next(c). Вариант 1: return echo.NewHTTPError(401, ...) — ошибка уйдёт в глобальный HTTPErrorHandler, клиент получит 401 с JSON. Вариант 2: написать ответ самому и вернуть nil. В обоих обработчик и слои ниже не исполняются.
- **Три уровня подключения** — e.Use — глобально; e.Group("/admin", mw) / g.Use — только маршруты группы; e.GET(path, h, mw) — один маршрут. Внутри: глобальные снаружи, затем группа, затем route-level, затем обработчик. Logger ставят раньше Recover — иначе паникующий запрос не попадёт в лог.

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

- echo.MiddlewareFunc — это func(next echo.HandlerFunc) echo.HandlerFunc: следующий слой приходит аргументом и зовётся next(c); c.Next() — API Gin и Fiber, в Echo такого метода нет.
- e.Use добавляет middleware в глобальный стек (после маршрутизации), e.Pre — в premiddleware (до): только Pre может повлиять на выбор маршрута, например переписать путь.
- Порядок подключения = порядок обёрток: последний добавленный слой ближе всех к обработчику; Logger добавляют раньше Recover, чтобы паникующие запросы попадали в лог.
- У middleware две половины: до next(c) — фаза запроса, после — фаза ответа с финальным статусом и временем; рядом с ответом следите за committed.
- c.Set/c.Get — карта на один запрос для передачи данных между звеньями; c.Get возвращает any, доставайте ok-формой type assertion.
- Прерывание цепочки: return HTTPError — ответ оформит глобальный обработчик; свой ответ плюс return nil — клиент получит ровно ваш вывод; в обоих случаях обработчик не исполняется.
- Три уровня: e.Use глобально, e.Group/g.Use для префикса, route-level middleware для одного маршрута; у штатных слоёв есть Skipper, чтобы пропускать ненужные пути.

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

Разверните пример урока: добавьте middleware timingMetric, который печатает только запросы дольше 100 миллисекунд, и поставьте его между loggerTimer и группой — проверьте через curl, что он видит статус и время. Затем напишите validate-middleware: он берёт из контекста структуру, положенную туда до вызова next(c), прогоняет через c.Validate и на ошибку возвращает 400, не доходя до обработчика. Проверьте оба случая: аутентифицированный запрос к /admin/users и запрос без заголовка Authorization — в логе должны быть строки со статусами 200 и 401.

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

### Чем e.Use отличается от e.Pre в Echo?

e.Use добавляет middleware, который исполняется после маршрутизации: уже известен выбранный маршрут, и слой обрабатывает все запросы, включая 404. e.Pre — premiddleware до роутера: он может изменить путь запроса (Rewrite, trailing slash) и тем самым повлиять на выбор маршрута, но не знает, какой маршрут совпадёт.

### Почему в коде Echo нет c.Next()?

В Echo v4 и v5 middleware — это echo.MiddlewareFunc: функция, которая получает следующий обработчик аргументом next и возвращает новый HandlerFunc. Вызов идёт как next(c). Метод c.Next() есть в Gin и Fiber, и его ошибочно переносят в рецепты про Echo — рабочим такой код не будет.

### Как остановить цепочку middleware и отдать ошибку?

Верните из middleware ошибку, не вызывая next(c): echo.NewHTTPError(http.StatusUnauthorized, "...") даст клиенту 401 с JSON-телом через глобальный обработчик ошибок, любая другая ошибка станет 500. Если нужен полностью свой ответ — напишите его через c.JSON и верните nil.
