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

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

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

Урок 18 из 25 ~16 мин +50 XP

Порядок middleware: почему он важен

Порядок middleware в chi — не вкусовщина, а поведение: поменять два слоя местами — и лог покажет 200 для упавшего с паникой запроса, а preflight получит 401 вместо согласия. Разберём луковицу цепочки, требование исходников chi к Logger и Recoverer и соберём боевой порядок слоёв в таблицу.

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

Инцидент из жизни сервиса: паника в новом хендлере, клиент получил 500, а в журнале — строка со статусом 200. Мониторинг, построенный на логах, показывает зелёный день, поддержка ловит жалобы, и эти две картинки не сходятся минут двадцать. Разбор по git-истории: на прошлой неделе кто-то «для чистоты стека» поставил Recoverer выше Logger — и строка лога ослепла. Второй баг из того же спринта: auth-слой перенесли выше cors-а, и preflight от браузера фронтенда стал получать 401 — фронтенд не может даже спросить разрешение, а в логах хендлеров пусто, потому что до хендлеров дело не доходит. Код слоёв в обоих случаях не менялся: не менялся вообще ни один хендлер. Менялся порядок r.Use — и этого хватило, чтобы сервис врал в лог и ломал браузер. middleware в chi — не список настроек, а конвейер, и перестановка двух строк меняет поведение так, как не меняет ни один рефакторинг.

Порядок middleware: луковица chi#

r.Use(A) и следом r.Use(B) собирают луковицу: слой A оборачивает слой B, B оборачивает хендлер. Запрос входит в A, потом в B, доходит до хендлера — и выходит наоборот: сначала своё дописывает B, потом A. Первый r.Use — самый внешний слой, последний — самый внутренний, хендлер — самое нутро. В исходниках chi middleware цепочка собирается из среза слоёв и оборачивает каждый вызов маршрута целиком — механика r.Use из урока про цепочку middleware здесь та же, меняется только вопрос: не «как подключить», а «в каком порядке». Отсюда правило чтения чужого стека: сверху вниз — путь запроса, снизу вверх — путь ответа. Каждый слой видит всё, что внутри него: лог видит статусы всех внутренних слоёв, recovery ловит паники всех внутренних слоёв, таймер меряет всё, что происходит под ним. Порядок middleware в chi и есть ответ на вопрос «кто кого видит»: подняли слой выше — он ослеп, опустили ниже — ослепли все над ним.

Луковица двумя слоями из Println

GO
func trace(name string) func(http.Handler) http.Handler {
        return func(next http.Handler) http.Handler {
                return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                        fmt.Println("вход:", name)
                        next.ServeHTTP(w, r)
                        fmt.Println("выход:", name)
                })
        }
}

r.Use(trace("внешний"))
r.Use(trace("внутренний"))

// GET /notes печатает по порядку:
// вход: внешний
// вход: внутренний
// выход: внутренний
// выход: внешний

Два слоя из Println показывают луковицу буквально: вход по порядку регистрации, выход строго наоборот. Каждая пара строк обнимает всё, что зарегистрировано ниже, — на этом строятся и лог, который обнимает ответ, и recovery, который обнимает панику, и таймер из собственного middleware. Проверьте на локальном Go с любым маршрутом — песочница сеть не поднимает.

Проверка: кто снаружи

В роутере сначала r.Use(requestTimer), затем r.Use(authRequired). В каком порядке запрос пройдёт слои?

Logger до Recoverer: что говорят исходники chi#

Механика обоих слоёв видна в исходниках v5. Logger создаёт запись, оборачивает ResponseWriter обёрткой и пишет строку в defer — после next.ServeHTTP: в строке оказывается статус, который обёртка успела увидеть. Recoverer в defer ловит панику и вызывает WriteHeader(500) — на том ResponseWriter, который ему передали. Если Logger снаружи, пятисотой от Recoverer проходит через обёртку, и строка лога показывает честный 500. Если Logger внутри Recoverer, при раскрутке его defer срабатывает раньше recover-а: в обёртке на тот момент нет ни одной записи статуса, и в лог уходит 200 по умолчанию, пока клиент получает 500 мимо обёртки. chi фиксирует это требование прямо в доккомментарии Logger: «Logger should go before any other middleware that may change the response, such as middleware.Recoverer». Стек паники при этом Recoverer печатает через PrintPrettyStack в stderr — с Logger и без него, а вот строка запроса зависит от порядка. Правило шире одной пары: до Logger не ставят ни один слой, который сам пишет ответ, — из урока про встроенные middleware это Logger и Recoverer, дальше к списку добавятся Timeout и Throttle.

Слои поменяли местами — что в логе?

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

r := chi.NewRouter()
r.Use(middleware.RequestID)
r.Use(middleware.Recoverer) // кто-то поменял слои местами
r.Use(middleware.Logger)

r.Get("/boom", func(w http.ResponseWriter, r *http.Request) {
        panic("boom")
})

// запрос: GET /boom

RequestID и RealIP: служебные слои в начале#

RequestID — первый содержательный слой стека. Logger собирает строку один раз, при завершении запроса, и в момент сборки читает GetReqID из контекста: идентификатор, положенный позже Logger-а, в строку не попадает — журнал останется без [id], и сшивать записи между службами будет нечем. RealIP ставится рядом и тоже до Logger: from-адрес в строке лога берётся из RemoteAddr в момент сборки, и слой должен переписать его на адрес клиента заранее — иначе в логе один и тот же прокси на каждую запись. Оба слоя ничего не пишут в ответ и не решают судьбу запроса: их работа — подготовить данные для всех, кто ниже. По этой же причине они почти не могут паниковать — и это не мелочь, а условие места в стеке, к которому вернёмся в красной врезке. Если балансировщик долбит /ping, слой Heartbeat ставят совсем первым: проба отвечает точкой и 200, не доходя ни до лога, ни до чего-либо ещё.

cors до auth: preflight без ключа#

Preflight — OPTIONS-запрос, который браузер шлёт перед непростым запросом, и у него есть свойство, ломающее порядок слоёв: Authorization в нём нет. Браузер не может приложить токен, потому что ещё не получил согласие сервера, — токен появится только в настоящем запросе. Если authRequired стоит выше cors.Handler, preflight доходит до проверки токена, токена нет — 401, заголовков согласия в ответе тоже нет, и браузер бракует запрос сам: фронтенд лежит, сервер невиновен, в логах хендлеров пусто. Поэтому cors.Handler регистрируется раньше auth: на preflight он отвечает сам, сверив Origin с настройками, и до авторизации дело не доходит, а настоящие запросы идут дальше — к проверке токена уже только там, где браузер его реально приложил. Общее правило из этого случая: слой, который должен отвечать на запрос особым образом, ставится выше слоя, который отсекает запросы по общему признаку.

cors и auth в правильном порядке

GO
r := chi.NewRouter()

r.Use(middleware.RequestID)
r.Use(middleware.RealIP)
r.Use(cors.Handler(cors.Options{
        AllowedOrigins: []string{"https://notes.example.com"},
        AllowedHeaders: []string{"Authorization", "Content-Type"},
}))

// auth ПОСЛЕ cors: preflight OPTIONS приходит без токена
r.Use(authRequired)

r.Get("/api/notes", listNotes)
r.Post("/api/notes", createNote)

cors.Handler стоит раньше authRequired: preflight он отвечает сам и до проверки токена дело не доходит, а настоящие запросы проходят дальше с Authorization в заголовке. Поменяйте два слоя — и каждый preflight станет 401 без заголовков согласия: фронтенд ляжет молча, потому что в логах хендлеров отражений этой аварии не будет вовсе.

Проверка: preflight мимо cors

authRequired зарегистрирован выше cors.Handler. Браузер отправил preflight OPTIONS. Что получит фронтенд?

Timeout и Throttle: предохранители до бизнес-логики#

Дедлайн и лимит параллелизма включаются до дорогой работы — иначе они охраняют уже потраченные силы. Timeout из урока про предохранители кладёт контексту дедлайн: чем раньше слой в цепочке, тем больше работы покрыто таймером, поэтому его место — сразу после страховок и задолго до хендлеров. Throttle отказывает при перегрузке до авторизации и декодирования тела: толпа не должна тратить сервис на проверки, результат которых всё равно будет выброшен. Относительно Logger оба стоят после него — отказы должны попадать в строку лога с честным статусом: обёртка видит и 504 от Timeout, и отказ Throttle, который по исходникам v5.0.8 уходит как 429 Too Many Requests с опциональным Retry-After. И заметьте направление: предохранители — тоже слои, меняющие ответ, поэтому они внутри Logger-а. Всё, что пишет ответ, живёт под логом; всё, что только готовит данные, — над ним.

Рекомендованный порядок слоёв#

МестоСлойЗачем здесь
1Heartbeat("/ping") — по желаниюпроба балансировщика отвечает 200 до лога, авторизации и всего остального
2middleware.RequestIDid в контексте до первого, кто его читает: строки лога и ответы об ошибках
3middleware.RealIPадрес клиента в from до сборки строки лога; только за доверенным прокси
4cors.Handlerpreflight без токена получает согласие раньше auth-проверки
5middleware.Loggerпишет после next — обязан быть снаружи всех, кто меняет ответ
6errorHandler / Recoverer, Timeout, Throttleстраховки и отказы внутри обёртки лога: их статусы видны строке
7authRequired и общая валидацияпроверки с ресурсным смыслом — последними перед хендлерами

Боевой стек целиком

GO
r := chi.NewRouter()

r.Use(middleware.RequestID)                  // id для всех логов и ошибок
r.Use(middleware.RealIP)                     // адрес клиента до Logger
r.Use(cors.Handler(cors.Options{             // preflight до auth
        AllowedOrigins: []string{"https://notes.example.com"},
}))
r.Use(middleware.Logger)                     // строка лога
r.Use(errorHandler)                          // единые ошибки: внутри Logger
r.Use(middleware.Timeout(30 * time.Second))  // дедлайн до логики
r.Use(middleware.Throttle(100))              // лимит параллелизма
r.Use(authRequired)                          // токены — у самой логики

r.Get("/healthz", health)
r.Post("/api/notes", createNote)

Восемь слоёв в порядке из таблицы. errorHandler из урока про централизованные ошибки здесь стоит после Logger — уточнение к тому стеку: ответы, отрендеренные после recover, пишутся в собственный w слоя, и внутри обёртки лога их статус виден строке, а снаружи — нет. Проверка на локальном Go: OPTIONS с Origin возвращает согласие без 401, /boom даёт 500 и строку лога со статусом 500.

Чек-лист порядка слоёв#

  • r.Use читается сверху вниз как вход запроса и снизу вверх как выход ответа.
  • RequestID — первый содержательный слой: id нужен каждой строке лога и каждому ответу об ошибке.
  • Logger раньше любого слоя, который пишет ответ: Recoverer, errorHandler, Timeout, Throttle.
  • cors.Handler раньше authRequired: preflight OPTIONS приходит без токена.
  • Timeout и Throttle — до бизнес-логики и внутри Logger: отказ виден логу честным статусом.
  • Слои выше Recoverer не имеют страховки от паники — держите их крошечными.

Соберите начало боевого стека

Восстановите три слоя, от которых зависят строки лога.

r := chi.NewRouter() // идентификатор нужен каждой строке лога r.Use(middleware.) // адрес клиента чинится до сборки строки лога r.Use(middleware.) // chi документирует: Logger должен стоять до Recoverer r.Use(middleware.) r.Use(middleware.Recoverer)

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

Расставьте шесть слоёв боевого стека: от самого внешнего к самому внутреннему.

Строки

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

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

Карточки: порядок middleware

Термин

Луковица middleware

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

Первый r.Use — внешний слой: запрос входит по порядку регистрации, ответ выходит наоборот. Хендлер — самый внутренний слой.

1 / 6

Стек перестал быть стопкой случайностей: каждый слой стоит там, где его выход видят остальные, и поведение сервиса описывается таблицей, а не догадками. В живом проекте порядок съезжает незаметно — слой добавили в конец списка Use «чтобы не мешать», и частично ослеп лог или лёг preflight, — поэтому проверка порядка становится частью ревью: два маршрута, /boom и preflight, показывают всю луковицу за минуту. errorHandler из урока про централизованные ошибки и пагинация из урока про валидацию встраиваются в таблицу на своих местах — страховки под логом, проверки у самой логики. Дальше по курсу — разбор структуры большого сервиса на subrouters, где каждый кусок API получит собственный стек поверх общего.

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

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

Вывод

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

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

  • Порядок middleware в chi — луковица: вход по порядку регистрации, выход строго наоборот.
  • Logger стоит до Recoverer по доккомментарию chi: строка лога должна увидеть 500, а не 200.
  • RequestID — первый содержательный слой, RealIP — до Logger: id и адрес клиента в каждой строке.
  • cors.Handler раньше authRequired: preflight OPTIONS не несёт токена и не должен получать 401.
  • Timeout и Throttle — до бизнес-логики и внутри Logger: отказ виден логу честным статусом.
  • Recoverer защищает только слои после себя: выше него — крошечные слои без собственной логики.
  • Порядок фиксируется таблицей и тестом на /boom и preflight — иначе он съезжает незаметно.

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

Соберите стек из таблицы урока в каркасе заметок и проверьте порядок двумя маршрутами: /boom с паникой должен вернуть 500 и строку лога со статусом 500, preflight OPTIONS с разрешённым Origin — заголовки согласия без 401. Затем поменяйте Logger и Recoverer местами и убедитесь, что лог покажет 200, — лучший способ запомнить луковицу.

Личные заметкиПусто
Хранятся только в вашем браузере (Chi · Порядок middleware: почему он важен)

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

Почему chi рекомендует Logger до Recoverer, а не наоборот?

Logger пишет строку после next.ServeHTTP и видит только записи в свою обёртку ответа. Recoverer при панике вызывает WriteHeader(500): если он внутри Logger, строка показывает честный 500; если Logger внутри Recoverer, обёртка фиксирует 200, а 500 уходит клиенту мимо лога. Требование зафиксировано в доккомментарии Logger в исходниках.

Куда ставить свой errorHandler относительно Logger?

После Logger: errorHandler рендерит ответы сам, и внутри обёртки лога каждый его статус виден строке. В уроке про централизованные ошибки слой стоял до Logger — для честной строки при паниках поменяйте их местами: RequestID остаётся первым, и id в контексте от перестановки не теряется.

Что будет, если RequestID поставить не первым?

Формально всё заработает, но Logger собирает строку с GetReqID в момент завершения запроса: id, положенный позже Logger-а, в строки не попадёт — журнал потеряет сквозной идентификатор. Его же не увидят ответы об ошибках, если те рендерят request_id из контекста. RequestID — всегда первый содержательный слой, сразу после Heartbeat.

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

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

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

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

Redis в Go: кэш, сессии и очереди на go-redis

Курс Redis для Go-разработчиков: кэширование запросов, сессии, счётчики, Pub/Sub, Streams и rate limiter. 25 уроков с примерами кода go-redis v9.

  • Подключаться к Redis из Go и настраивать пул соединений
  • Кэшировать результаты запросов и придумывать TTL
  • Работать с хэшами, списками, множествами и ZSET
  • Строить сессии, счётчики, лидерборды и rate limiter