---
title: "Middleware в chi: Use и цепочка"
description: "chi middleware: сигнатура func(http.Handler) http.Handler, луковица цепочки, r.Use и забытый next.ServeHTTP. Порядок слоёв, 404 в логах, ранний ответ."
course: "Chi: роутер для Go HTTP-сервисов"
url: "https://golangmaster.ru/courses/chi/lessons/05-middleware/"
published: "2026-09-19"
updated: "2026-09-27"
minutes: 18
---

# Middleware в chi: Use и цепочка

*Курс «Chi: роутер для Go HTTP-сервисов», урок 5 из 25. ~18 минут. Обновлено 27.09.2026. Источник: https://golangmaster.ru/courses/chi/lessons/05-middleware/*

Каждый хендлер сервиса рано или поздно обрастает повторами: лог, таймер, проверка токена — один и тот же код в десятке мест. chi middleware решает это обёрткой func(http.Handler) http.Handler: поведение добавляется до и после хендлера, а сами хендлеры остаются чистыми. Разберём сигнатуру, луковицу цепочки, r.Use и цену забытого next.ServeHTTP.

В REST-каркасе из [прошлого урока](/courses/chi/lessons/04-urlparam/) хендлеры научились отвечать по делу: карточка заметки, список, обновление. Добавьте к этому нормальный сервис — и каждый хендлер обрастает служебным кодом: замерить время, записать в лог запрос, проверить токен. Три хендлера — три копии одного и того же. Десять — десять, и рано или поздно одну копию правят, а другую забывают. Middleware в chi снимает эти повторы с уровня хендлеров на уровень роутера: поведение описывается один раз как обёртка func(http.Handler) http.Handler и вешается на цепочку одним r.Use. Обёртка сама решает, что сделать до хендлера, что после — и вызвать ли его вообще. Это та же сигнатура, что во всей экосистеме net/http, поэтому сторонние слои подключаются без адаптеров. В этом уроке — механика цепочки, порядок прохождения запроса, ранние ответы и цена забытого вызова next.

### Middleware в chi: сигнатура func(http.Handler) http.Handler

Middleware — функция, которая принимает следующий хендлер и возвращает хендлер. Разберём по частям: func(http.Handler) http.Handler. Аргумент next — всё, что стоит дальше по цепочке: другие middleware и в конце ваш хендлер. Возвращаемое значение — новый http.Handler, обычно замыкание, внутри которого живёт старая цепочка. Внутри замыкания три места для кода. До next.ServeHTTP(w, r) — то, что должно случиться раньше хендлера: замер времени, присвоение id запроса, проверка заголовка. Вызов next.ServeHTTP(w, r) сам — передача эстафеты дальше; без него до маршрутизации и хендлера дело не доходит. После вызова — то, чему нужен результат: длительность, итоговый статус. Приём с defer здесь любимец: код после next гарантированно сработает, даже если хендлер запаниковал. Сигнатура не изобретена chi — это общий язык net/http-экосистемы, и любой middleware из интернета компилируется с chi как есть.

### Первый middleware: таймер

```go
func timer(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                start := time.Now()

                defer func() {
                        log.Printf("%s %s: %v", r.Method, r.URL.Path, time.Since(start))
                }()

                next.ServeHTTP(w, r)
        })
}

func main() {
        r := chi.NewRouter()

        r.Use(timer)

        r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
                w.Write([]byte("ok"))
        })

        http.ListenAndServe(":8080", r)
}
```

Таймер — классический первый middleware. start фиксируется до передачи эстафеты, а log.Printf уходит в defer: строка в логе появится после того, как цепочка отработает, — с методом, путём и длительностью. next.ServeHTTP(w, r) — сердце функции: именно он запускает маршрутизацию и хендлер. r.Use(timer) регистрирует слой на весь роутер. Для запуска нужен локальный Go: песочница сайта TCP-сервер не поднимает.

### Луковица: порядок запроса и ответа

r.Use(timer, logger, auth) складывает слои в стек, и порядок в списке — это направление входа. Запрос идёт снаружи внутрь: сначала timer, потом logger, потом auth, и только затем маршрутизация и хендлер. Ответ возвращается в обратном порядке: auth досмотрел на результат, logger дописал строку, timer посчитал общее время — уже с учётом всех, кто работал внутри. Картинка, которая прилипла к теме, — луковица: каждый middleware — слой, через который запрос проходит туда и обратно. Отсюда два практических вывода. Замер длительности имеет смысл ставить раньше других слоёв — иначе время, потраченное внутренними middleware, в замер не попадёт. А слои, которым нужен результат, например логгер со статусом ответа, располагают так, чтобы их вызов next стоял над тем, кто этот результат меняет; разбору порядка в chi посвящён отдельный урок дальше по курсу. Пока запомните простое: регистрация слева направо — это путь запроса.

### Какой будет порядок строк?

```go
func alpha(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                fmt.Println("alpha: до")
                next.ServeHTTP(w, r)
                fmt.Println("alpha: после")
        })
}

func beta(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                fmt.Println("beta: до")
                next.ServeHTTP(w, r)
                fmt.Println("beta: после")
        })
}

// регистрация: r.Use(alpha, beta); запрос пришёл на существующий маршрут
```

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

- ✅ alpha: до, beta: до, beta: после, alpha: после
- alpha: до, alpha: после, beta: до, beta: после
- beta: до, alpha: до, alpha: после, beta: после
- alpha: до, beta: до, alpha: после, beta: после

Регистрация alpha, beta задаёт путь запроса: срабатывает «до» у alpha, затем «до» у beta, затем хендлер. На выходе порядок зеркалится — «после» beta печатается раньше, чем «после» alpha. Поэтому таймер ставят в начало стека: его «после» засчитывает всех.

### r.Use: регистрация и правила

r.Use принимает сколько угодно middleware за один вызов: r.Use(timer, logger) эквивалентен двум строкам r.Use(timer) и r.Use(logger). Слои вешаются на весь роутер; отдельный стек для ветки маршрутов делается через Route и Group — [об этом урок через один](/courses/chi/lessons/07-groups-routes/). И правило, о которое спотыкаются все: Use вызывают до регистрации маршрутов. Chi собирает цепочку один раз — на первом r.Get стек замораживается, и поздний r.Use приводит к панике с говорящим текстом: all middlewares must be defined before routes on a mux. Паника честная: middleware после маршрутов — почти всегда ошибка чтения кода, и chi предпочитает упасть на старте, а не удивлять в проде. Практика сборки: сначала все r.Use сверху вниз, потом маршруты — тогда файл читается как путь запроса, что сверху, то и выполнится раньше.

> **Забытый next.ServeHTTP.** Middleware, который не вызвал next.ServeHTTP и не записал ничего в w, оставляет клиента с открытым соединением и без ответа: браузер крутится, curl висит до собственного таймаута или до WriteTimeout у http.Server. Типичный способ поймать это — новый слой с return внутри условия, где return сработал и на легитимных запросах. Отладка проста: клиент завис — эстафета оборвалась где-то в цепочке, проверяйте каждый слой на вызов next. Обратный случай легален: middleware отвечает сам — авторизация с 401 — но это осознанное решение, а не потерянный вызов.

### Три места поведения в middleware

| Фаза | Что доступно | Примеры |
| --- | --- | --- |
| До next.ServeHTTP | запрос целиком: заголовки, путь, тело | замер времени, проверка токена, id запроса |
| Сам вызов next | эстафета дальше по цепочке | обязателен, если слой не отвечает сам |
| После next или в defer | результат: статус, длительность, паника | строка лога, метрика, восстановление |

### Собираем цепочку из двух слоёв

```go
func main() {
        r := chi.NewRouter()

        // порядок Use = путь запроса
        r.Use(timer)  // снаружи: засекает время первым
        r.Use(logger) // внутри: логирует уже после таймера

        r.Get("/health", health)
        r.Get("/notes/{id}", getNote)

        // GET /notes/42 идёт: timer -> logger -> маршрутизация -> getNote,
        // ответ возвращается в обратном порядке

        http.ListenAndServe(":8080", r)
}
```

Два слоя плюс хендлер — уже луковица: timer обёрнут вокруг logger, logger вокруг маршрутизации и хендлера. Комментарии в коде — то, что стоит проговаривать при ревью: Use сверху вниз, запрос снаружи внутрь, ответ обратно. Запустите локальным Go и посмотрите в лог: одна строка таймера на каждый запрос, каким бы ни был маршрут — включая 404, о них следующий блок.

### Middleware видит и 404

Запрос на несуществующий путь всё равно проходит через весь зарегистрированный стек. Chi сначала прогоняет запрос через middleware и только потом ищет маршрут; когда ни один шаблон не совпал, в работу включается обработчик 404 — но уже внутри цепочки. Для логов это ключевой сценарий: кривые запросы сканеров, опечатки после редизайна, пробы чужих API — всё попадает в журнал с длительностью и id, как обычные запросы. В фреймворках с другим устройством 404 нередко отдаётся до middleware, и лог молчит ровно тогда, когда интереснее всего. Побочный эффект стоит запомнить: тяжёлые слои считаются и на мусорных запросах, так что middleware, который на каждом входе лезет в базу, начнёт есть ресурсы и на переборе адресов. Служебные проверки делайте дешёвыми и ранними — или ставьте дешёвый фильтр первым слоем.

### Проверка: 404 и цепочка

**Вопрос:** Запрос пришёл на путь, которого нет ни в одном маршруте. Что произойдёт с зарегистрированными middleware?

- Не выполнятся: 404 отдаётся до middleware
- ✅ Выполнятся: chi прогоняет через стек и несовпавшие запросы, 404 отдаётся внутри цепочки
- Выполнится только первый middleware в стеке
- Зависит от того, задан ли обработчик NotFound

Chi сначала оборачивает запрос стеком и только потом ищет маршрут: несовпадение обрабатывается внутри цепочки, поэтому логи, id и таймеры видят и 404. Пропускать middleware на несовпадении — поведение некоторых других фреймворков, из-за которого в логах исчезают сканеры и опечатки.

### Ранний ответ: middleware отвечает сам

Вызов next — не обязанность любой ценой, а выбор. Если дальше идти нельзя, middleware отвечает сам и эстафету не передаёт: проверка токена не сошлась — w.WriteHeader(http.StatusUnauthorized) и return; квота исчерпана — 429. Клиент получает ответ, остальные слои и хендлер не тратят ни такта. Заметьте: «после» в слоях выше всё равно сработает — defer в таймере напечатает строку и для отклонённого запроса, ведь ответ вернулся по цепочке, просто не дошёл до хендлера. Обратная задача — передать данные вниз, в хендлер: в Go это делается через context.Context, к которому доступ из *http.Request. Разбор контекста — в [уроке продвинутого курса](/courses/go-advanced/lessons/09-context/), а собственный middleware с пользователем в контексте будет позже в курсе chi, после знакомства с готовыми слоями.

### Middleware с ранним ответом

```go
func auth(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                if r.Header.Get("Authorization") == "" {
                        http.Error(w, "нет токена", http.StatusUnauthorized)
                        return // эстафета не передаётся
                }

                next.ServeHTTP(w, r)
        })
}

r := chi.NewRouter()
r.Use(auth)
r.Get("/notes", listNotes)
```

Две ветки: без заголовка Authorization слой сам отвечает 401 Unauthorized через http.Error и возвращает управление — хендлер listNotes не выполнится никогда для такого запроса. С заголовком — next.ServeHTTP(w, r), и запрос уходит дальше. Ранний ответ — штатная функция middleware, а не обходное правило; настоящую проверку токена и передачу пользователя в контексте соберём позже. Здесь — упрощение ради механики.

### Проверка: пропущенный next

**Вопрос:** Middleware не вызвал next.ServeHTTP и не записал ничего в http.ResponseWriter. Что получит клиент?

- Пустую страницу со статусом 200
- ✅ Ничего: соединение открыто, пока не сработает таймаут
- 404 Not Found от роутера
- Автоматический 500 от chi

Пока никто не записал в w и эстафета не передана, ответа не существует: соединение держится открытым до клиентского таймаута или WriteTimeout сервера. Роутер не подставит 404 — маршрутизация запускается вызовом next, до которого дело не дошло.

### Соберите таймер

Восстановите замер времени, строку лога в defer и передачу эстафеты.

```go
func timer(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                start := time.___

                defer func() {
                        log.Printf("%s %s: %v", r.Method, r.URL.Path, time.___(start))
                }()

                next.___(w, r)
        })
}
```

Подсказка: Время берётся time.Now, длительность считает time.Since, а эстафету передаёт next.ServeHTTP.

Правильные ответы в пропусках: `Now`, `Since`, `ServeHTTP`

### Пять правил первого middleware

- Одна ответственность на слой: таймер меряет, логгер пишет, авторизация пускает — слияние трёх задач ломает переиспользование.
- Либо next.ServeHTTP, либо собственный ответ — третьего варианта нет; пропущенный вызов оставляет клиента без ответа.
- Код после next выносите в defer: он сработает и на панике хендлера.
- r.Use — до регистрации маршрутов: поздний Use в chi приводит к панике на старте.
- Порядок Use = путь запроса: тот, кто должен посчитать всех, — первым.

> **Middleware — не свалка для бизнес-логики.** Как только r.Use появляется в проекте, возникает соблазн: не дублировать ли сюда бизнес-правила? Ограничения — в middleware, скидки — в middleware... Моё правило: в цепочке живёт инфраструктура плюс то, что обязано решаться до хендлера по безопасности — авторизация, лимиты. Граница простая: если логику нужно тестировать отдельно от HTTP и переиспользовать без запросов — это сервисный слой, а не middleware. Правило нарушаемо — rate limiter, понимающий бизнес-квоты, законный middleware, — но нарушение должно быть осознанным.

### Соберите таймер по строкам

Расставьте строки middleware-таймера в правильном порядке: от сигнатуры до передачи эстафеты.

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

1. `func timer(next http.Handler) http.Handler {`
2. `return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {`
3. `start := time.Now()`
4. `defer log.Printf("%s %s: %v", r.Method, r.URL.Path, time.Since(start))`
5. `next.ServeHTTP(w, r)`
6. `})`

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

- **Middleware** — func(http.Handler) http.Handler — обёртка вокруг следующего хендлера: добавляет поведение до, после или вместо него. Та же сигнатура во всей экосистеме net/http.
- **next.ServeHTTP(w, r)** — Передача эстафеты дальше: запуск маршрутизации и хендлера. Пропуск без собственного ответа оставляет клиента висеть до таймаута.
- **Луковица** — Модель цепочки: запрос проходит слои в порядке регистрации, ответ — в обратном. Первый слой видит вход раньше всех и выход последним.
- **r.Use** — Регистрация middleware на весь роутер, до первого маршрута: r.Use(timer, logger) — несколько слоёв одним вызовом. Поздний Use в chi приводит к панике на старте.
- **Ранний ответ** — Middleware отвечает сам — 401, 429 — и не вызывает next. Внешние слои всё равно досчитывают своё «после»: defer срабатывает на любом ответе.
- **404 в цепочке** — Несовпавший маршрут проходит через весь стек: логи, id и таймеры видят и мусорные запросы. Цена — тяжёлые слои считаются и на сканерах.

Механика собрана: сигнатура, луковица, правила r.Use, ранние ответы и 404, которые не теряются из логов. Писать каждый слой руками, впрочем, долго — большая часть обвязки уже написана авторами chi. В [следующем уроке](/courses/chi/lessons/06-vstroennye-middleware/) — встроенные middleware: RequestID, RealIP, Logger и Recoverer, из которых боевой стек собирается четырьмя строками r.Use. К собственному middleware с авторизацией и контекстом вернёмся после знакомства с готовыми слоями, а группы маршрутов со своим стеком — тема урока про Route и Group, через один. Попробуйте напоследок поменять местами r.Use(timer) и r.Use(logger): разница между строками двух слоёв покажет на живом коде, кто кого оборачивает.

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

- Middleware — func(http.Handler) http.Handler: обёртка принимает следующий хендлер и возвращает новый; поведение живёт до, после или вместо next.ServeHTTP.
- Луковица: запрос проходит слои в порядке регистрации, ответ — в обратном; код после next выносится в defer.
- r.Use(timer, logger) регистрирует стек на весь роутер и вызывается до маршрутов — поздний Use в chi приводит к панике на старте.
- next.ServeHTTP обязателен: без него и без собственного ответа клиент висит до таймаута — маршрутизация не запускается.
- Middleware может ответить сам — 401, 429 — и не вызывать next; внешние слои всё равно досчитают своё «после».
- 404 проходит через весь стек: логи, id и таймеры видят и мусорные запросы — вместе с их ценой.
- Бизнес-логика в цепочке — редкая удача, а не правило: middleware держат для инфраструктуры и проверок безопасности.

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

Напишите к каркасу заметок два middleware: timer с defer и log.Printf, и headerCheck, отвечающий 401 без заголовка X-Role. Зарегистрируйте их r.Use до маршрутов и проверьте curl-ом три ответа: обычный запрос, запрос без роли и путь к несуществующему маршруту — таймер должен отработать во всех трёх.

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

### Чем middleware в chi отличается от middleware в Gin?

Сигнатурой и типами: chi — func(http.Handler) http.Handler со стандартными w и r, Gin — func(*gin.Context), и свойство слоя завязано на gin-контекст. Механика луковицы одинакова, но chi-слой компилируется с любым net/http-кодом, а gin-слой живёт только внутри Gin.

### Что произойдёт, если вызвать r.Use после регистрации маршрутов?

Chi запаникует на старте с текстом all middlewares must be defined before routes on a mux: стек замораживается на первом r.Get. Это fail-fast вместо сюрпризов в проде — собирайте r.Use до маршрутов, тогда файл читается как путь запроса.

### Можно ли поставить middleware только на часть маршрутов?

На весь роутер — r.Use, на ветку — Route или Group со своим стеком, о них отдельный урок дальше; на одиночный маршрут слой вешается вручную оборачиванием хендлера. Так авторизация живёт в ветке /admin, а логи остаются общими для всего сервиса.
