Содержание урока · 12 разделов
- fiber middleware: слой вокруг каждого обработчика
- c.Next() и луковица: две фазы жизни слоя
- Прерывание цепочки: return err до c.Next()
- Куда вешать middleware: пять способов подключения
- fiber logger: формат и теги-переменные
- fiber Recover: паника вместо падения процесса
- fiber RequestID: X-Request-ID для трассировки
- Порядок встроенных middleware: почему logger раньше recover
- Запуск, проверка и что дальше
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
fiber middleware: слой вокруг каждого обработчика#
Middleware — это функция того же типа, что и обычный хендлер, которую Fiber исполняет не ради ответа, а вокруг ответов: логирует, ставит заголовки, проверяет доступ, ловит паники. Вы уже встречались с этой механикой, не называя её: в уроке 5 статик-хендлер при промахе мимо файла вызывал c.Next() — так запрос проваливался дальше по цепочке, к API и SPA-fallback. Статика в Fiber и есть middleware, только с файловой системой внутри.
Смысл подхода — один раз описать сквозную заботу и больше не повторять её в хендлерах. Лог строки запроса нужен каждому маршруту: если написать его в каждом хендлере, к третьему эндпоинту вы будете копировать одно и то же; если повесить middleware — получите логи даже для тех маршрутов, которые добавите через месяц. Продолжаем сквозной пример курса — API заметок: к нему прицепим три встроенных слоя, которые идут в комплекте с фреймворком. Все факты урока сверены с исходниками тега v3.5.0 — файла app.go и каталога middleware.
Сигнатура Handler: middleware — это обычный хендлер
GO// app.go, строка 41 (v3.5.0): тип, из которого состоит всё приложение
type Handler = func(Ctx) error
// Минимальный middleware: замеряем время вокруг всей цепочки
func timer(c fiber.Ctx) error {
// фаза запроса: код до c.Next() исполняется первым
start := time.Now()
// эстафета следующим слоям — вплоть до хендлера
err := c.Next()
// фаза ответа: сюда попадаем, когда внутренние слои закончили
fmt.Printf("%s %s -> %v\n", c.Method(), c.Path(), time.Since(start))
return err
}
// Подключение: все методы, все пути
app.Use(timer)Главная мысль урока помещается в одну строчку: в Fiber нет отдельного типа для middleware. В app.go объявлен type Handler = func(Ctx) error, и этим типом описаны и хендлеры, и слои вокруг них — различает их только способ подключения. Обратите внимание на Ctx без звёздочки: в v3 контекст — интерфейс (за ним стоит DefaultCtx), а в v2 сигнатура была func(fiber.Ctx) error с указателем; старые туториалы выдают себя именно звёздочкой. Метод Use принимает variadic any: строку-префикс, срез строк, вложенное приложение App или любое число хендлеров — на незнакомый тип он честно паникует с «use: invalid handler».
c.Next() и луковица: две фазы жизни слоя#
Как цепочка работает внутри: каждый сматченный маршрут несёт массив хендлеров, а Use-слои встают в общий стек раньше endpoint-маршрутов. Метод c.Next() (в ctx.go документирован как «executes the next method in the stack») двигает индекс и вызывает следующий слой; когда те закончили, управление возвращается в строку после вызова. Отсюда модель, которую все называют луковицей: запрос идёт снаружи внутрь сквозь слои, хендлер сидит в центре, ответ выходит обратно через те же слои. У каждого middleware поэтому две фазы: код до c.Next() видит входящий запрос, код после — уже готовый ответ с финальным статусом.
Практическое следствие: замерять время и читать c.Response().StatusCode() имеет смысл только после c.Next() — до него ответ ещё не существует. Именно так устроен штатный таймер в Logger-е. И отсюда же правило порядка: слои, зарегистрированные раньше, оборачивают остальные; слой, поставленный после endpoint-маршрута, почти никогда не получит управления — хендлер, не вызвавший c.Next(), завершает обработку сам. Мы ещё вернёмся к порядку при разборе связки Logger и Recover — там цена перестановки конкретная: потерянная строка лога.
В каком порядке уйдут строки в консоль?
Прочитайте код и выберите, что он выведет:
func first(c fiber.Ctx) error {
fmt.Println("A: до")
err := c.Next()
fmt.Println("A: после")
return err
}
func second(c fiber.Ctx) error {
fmt.Println("B: до")
err := c.Next()
fmt.Println("B: после")
return err
}
app.Use(first, second)
app.Get("/ping", func(c fiber.Ctx) error {
fmt.Println("H: обработчик")
return c.SendString("pong")
})Прерывание цепочки: return err до c.Next()#
Middleware не обязан дотягивать запрос до хендлера. Достаточно вернуть ошибку, не вызвав c.Next(), — внутренние слои не исполнятся вовсе, а ошибка уйдёт в глобальный обработчик. В докомментарии конфига это сформулировано прямо: ErrorHandler is executed when an error is returned from fiber.Handler. Что делает обработчик по умолчанию (DefaultErrorHandler в app.go): если ошибка — или заворачивает в себе — *fiber.Error, клиент получает её код; любая другая ошибка превращается в 500 с телом err.Error(); Content-Type при этом text/plain; charset=utf-8. Значит, статус ответа определяет не middleware, а тип ошибки, которую вы вернули.
Второй способ прервать цепочку — вернуть nil без c.Next(): никто не упадёт, но и ответ никто не соберёт. Клиент получит то, что успели записать до возврата, — как правило, пустой 200. Это почти всегда баг, а не приём, и ниже мы отведём ему отдельный предупреждающий блок. Тонкость про луковицу: когда слой вернул ошибку до Next, код после c.Next() у вышележащих слоёв всё равно исполнится — но их c.Next() вернёт эту самую ошибку, и они обязаны просто прокинуть её дальше, ничего не перезаписывая. Так ошибка всплывает наверх, а ErrorHandler получает её один раз.
Отказ до хендлера: fiber.NewError и аргументы маршрута
GO// Фабрика middleware: конфигурация снаружи, готовый Handler на выходе
func requireKey(key string) fiber.Handler {
return func(c fiber.Ctx) error {
// фаза запроса: проверяем заголовок до всей работы
if c.Get("X-Api-Key") != key {
// прерываем цепочку: код 401 придёт клиенту как есть
return fiber.NewError(fiber.StatusUnauthorized, "неверный ключ")
}
// ключ подошёл — пускаем дальше
return c.Next()
}
}
// Подключение аргументом маршрута: слой только для этого пути
app.Get("/api/notes", requireKey("s3cret"), func(c fiber.Ctx) error {
return c.JSON(fiber.Map{"items": []string{"первая заметка"}})
})Три детали. Первая: fiber.NewError(code, message) возвращает *fiber.Error — именно такой ошибки ждёт DefaultErrorHandler, поэтому клиент получит 401 и текст «неверный ключ», а не обезличенный 500; полноценную аутентификацию-заглушку мы построим сами в седьмом уроке, здесь важна механика прерывания. Вторая: middleware можно вешать не только через Use, но и аргументами маршрута — они встают в массив route.Handlers перед хендлером, и c.Next() перебирает их внутри этого же маршрута. Третья: фабрика, возвращающая fiber.Handler, — тот же рисунок, что у logger.New() и requestid.New(): конфиг на входе, слой на выходе.
Проверьте себя
Middleware для /api/notes вернул fiber.NewError(fiber.StatusNotFound, "нет такой заметки"), не вызвав c.Next(). Что получит клиент?
Куда вешать middleware: пять способов подключения#
| Способ | Пример | Кто получит слой |
|---|---|---|
| Use без префикса | app.Use(logger.New()) | Все запросы, зарегистрированные после него |
| Use с префиксом | app.Use("/api", limiter) | Только пути под /api, любой HTTP-метод |
| Аргументы маршрута | app.Get("/notes", mw, handler) | Только этот маршрут: слои в route.Handlers |
| Монтирование приложения | app.Use("/api", subApp) | Подпрефиксное приложение со своей конфигурацией |
| Регистрация после маршрутов | app.Get(...); затем app.Use(mw) | Слой в стеке позже: живые хендлеры его не дождутся |
| Next в конфиге middleware | logger.Config{Next: fn} | Слой пропускается, когда Next вернул true |
fiber logger: формат и теги-переменные#
Первый встроенный слой — access-log. Пакет github.com/gofiber/fiber/v3/middleware/logger экспортирует logger.New(config ...Config), который возвращает готовый fiber.Handler: внутри себя он вызывает c.Next(), а потом печатает строку — потому и видит финальный статус. Формат задаётся строкой с тегами-переменными вида ${status}: дефолт объявлен в format.go константой DefaultFormat — «[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}» с переводом строки. Теги собираются из tags.go: время, ${ip}, ${status}, ${latency}, ${method}, ${path}, ${url}, ${route}, ${ua}, ${referer}, ${protocol}, ${bytesSent}, ${error}, плюс параметризованные ${reqHeader:X}, ${respHeader:X}, ${locals:X}, ${query:X}, ${cookie:X} и цвета для терминала. Свои теги регистрируются функцией RegisterTag или MustRegisterTag — глобально на имя.
Про «fiber logger» полезно знать три вещи сверх тегов. Во-первых, есть готовые форматы под задачи: CommonFormat и CombinedFormat — классические Apache-формы для парсеров, JSONFormat и ECSFormat — для коллекторов логов. Во-вторых, конфиг богаче, чем кажется: Stream (по умолчанию os.Stdout), TimeFormat «15:04:05», TimeZone «Local», TimeInterval в полсекунды, DisableColors. В-третьих, в дефолтном конфиге строка выглядит не как шаблон, а «столбиками» — время | статус | латентность | IP | метод и путь | ошибка: для DefaultFormat есть быстрый путь рендера. Значения из запроса перед печатью чистятся от управляющих символов — клиент с хитрым путём не подделает строки вашего лога.
logger.New: дефолт и свой формат
GOimport "github.com/gofiber/fiber/v3/middleware/logger"
// Вариант 1: нулевая настройка — дефолтный DefaultFormat
app.Use(logger.New())
// Вариант 2 (вместо первого): свой формат из тегов
app.Use(logger.New(logger.Config{
Format: "${time} | ${status} | ${latency} | ${method} ${path} | ${error}\n",
TimeFormat: "15:04:05",
}))На каждый запрос при втором варианте уйдёт строка вроде «14:07:33 | 200 | 1.4ms | GET /api/notes | ». Тег ${error} пуст, если цепочка вернула nil, и содержит текст ошибки иначе — сейчас это важно, ниже разберём почему. Конфиг передаётся первым аргументом-вариадиком: незаполненные поля добираются из ConfigDefault, поэтому указывать одно поле Format легально. Два Use с логгером подряд в реальном приложении не нужны — выберите один вариант, здесь они показаны рядом для сравнения.
Ошибка в цепочке: что будет в логе и в ответе?
Логгер зарегистрирован с тегом ${error}. Внутренний слой вернул fiber.NewError(fiber.StatusForbidden, "нет доступа"). Что произойдёт?
fiber Recover: паника вместо падения процесса#
Второй слой отвечает на вопрос «что будет, если хендлер запаниковал». Ответ без middleware неприятный: fasthttp не восстанавливает паники — в server.go это зафиксировано комментарием «no panic recovery is done by fasthttp, thus any panic will take down the entire server». Одна забытая проверка на nil — и весь ваш сервис умирает вместе с обслуживанием других запросов. Для сравнения: стандартный net/http ловит панику хендлера на уровне сервера и продолжает жить; Fiber же предлагает решать вопрос явно — middleware-ем recover из пакета .../middleware/recover.
Механика элементарная и стоит того, чтобы вы её поняли: хендлер recover-слоя объявлен с именованным возвратом err, и внутри стоит defer с recover(). Паника из любого внутреннего слоя разматывает стек ровно до этого defer-а, её значение превращается в ошибку — err = PanicHandler(c, r) — и цепочка завершается штатным возвратом ошибки. Паника по умолчанию (DefaultPanicHandler) становится error как есть, если паниковали значением-ошибкой, иначе оборачивается через fmt.Errorf; значит, паника с *fiber.Error сохранит свой код, а паника со строкой «boom» даст 500. Стек вызовов по умолчанию не печатается: EnableStackTrace false, но при включении StackTraceHandler пишет в stderr строку «recovered panic: …» и полный debug.Stack() — для отладки удобно, в проде шумно.
recover.New: две настройки
GOimport "github.com/gofiber/fiber/v3/middleware/recover"
// Базовая защита: паника превращается в error -> ErrorHandler -> 500
app.Use(recover.New())
// Отладочный вариант: стек паники уйдёт в stderr
app.Use(recover.New(recover.Config{
EnableStackTrace: true,
}))Конфиг невелик: Next (пропустить слой целиком), PanicHandler (как превратить значение паники в error), StackTraceHandler и флаг EnableStackTrace. Достаточно одного экземпляра на приложение — defer с recover() ловит паники из всех слоёв, зарегистрированных после него. Импорт пакета выглядит непривычно: пакет называется recover, как встроенная функция, — авторы пометили это nolint-комментарием с пометкой о переименовании, но пока пишется именно так.
Что получит клиент вместо ответа?
Прочитайте код и выберите, что он выведет:
app.Use(recover.New())
app.Get("/boom", func(c fiber.Ctx) error {
panic("boom")
})Сервер без recover-слоя
В приложении НЕТ middleware recover. Один из хендлеров паникует на запросе. Что произойдёт с сервером?
fiber RequestID: X-Request-ID для трассировки#
Третий встроенный слой решает задачу, которую вы оцените при первом же инциденте: дать каждому запросу идентификатор, который виден в логах, в ответах и в системах трассировки. Пакет .../middleware/requestid с фабрикой requestid.New() работает так: читает входящий заголовок (по умолчанию X-Request-ID — константа fiber.HeaderXRequestID), и если значение валидно — непусто и состоит из видимых ASCII-символов, — переиспользует его; иначе генерирует новое через конфигурируемый Generator (дефолт — utils.SecureToken), с тремя попытками и запасным вариантом на случай капризного генератора. Готовый ID записывается в тот же заголовок ответа через c.Set и складывается в контекст запроса, откуда его достаёт requestid.FromContext.
Переиспользование входящего ID — не мелочь: клиент или балансировщик может прислать свой идентификатор, и тогда он пройдёт сквозь ваш сервис без изменений — сквозная трассировка собирается бесплатно. А связка с логгером тут буквальная: при первом запуске requestid-middleware регистрирует в logger теги ${requestid} и ${request-id}, после чего их можно вставлять прямо в формат access-log. Два независимых пакета согласованы внутри одной библиотеки — вам не нужно протаскивать ID руками через locals. Проверка простая: curl -i, и в ответе появится строка X-Request-ID со случайным токеном.
Собираем три слоя в main.go заметок
GOpackage main
import (
"log"
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/middleware/logger"
"github.com/gofiber/fiber/v3/middleware/recover"
"github.com/gofiber/fiber/v3/middleware/requestid"
"github.com/gofiber/fiber/v3/middleware/static"
)
func main() {
app := fiber.New()
// 1. RequestID: каждому запросу — идентификатор в X-Request-ID
app.Use(requestid.New())
// 2. Logger: идёт до статики, поэтому логирует и файлы из урока 5
app.Use(logger.New(logger.Config{
Format: "${time} ${requestid} ${status} ${latency} ${method} ${path} ${error}\n",
}))
// 3. Recover: паника станет ошибкой ДО возврата в logger,
// и он успеет записать её в ${error}
app.Use(recover.New())
// 4. Статика: существующие файлы уезжают отсюда, промах идёт дальше
app.Use("/", static.New("./public"))
// 5. API заметок — сквозной пример курса
app.Get("/api/notes", func(c fiber.Ctx) error {
return c.JSON(fiber.Map{"items": []string{"первая", "вторая"}})
})
log.Fatal(app.Listen(":3000"))
}Четыре слоя и маршрут в порядке регистрации — этот каркас стоит запомнить как есть. requestid первым: его значение нужно логгеру для ${requestid}. Логгер вторым и обязательно до статики — обещание из урока 5 «Logger честно логирует и раздаваемые файлы» выполняется именно так: запросы к /css/style.css пройдут через его слой. Recover третьим, между logger и статикой: на обратном пути паника успевает превратиться в ошибку до кадра логгера. Дальше — статика и API: их порядок мы репетировали в прошлом уроке, здесь ничего не изменилось. Проверка curl-ом: /api/notes даёт JSON и строку лога, /css/style.css — файл и тоже строку лога, а curl -i покажет X-Request-ID в каждом ответе.
Порядок встроенных middleware: почему logger раньше recover#
Порядок в main.go выше не вкусовщина, а следствие луковицы — разберём, что сломается при перестановке. Пусть recover стоит раньше logger: цепочка «recover → logger → хендлер». Паника в хендлере разматывает стек вверх и пролетает сквозь кадр логгера, не останавливаясь: код после его c.Next() не исполняется никогда — выполнение функции прервано. Лог теряет строку о запросе целиком, а ErrorHandler вызывается уже fiber-ом на верхнем уровне. Теперь как в нашем main.go: «logger → recover → хендлер». Паника сначала достигает recover-слоя (он внутри), превращается в ошибку, и logger-овский c.Next() возвращается штатно — с ошибкой в руках. Логгер пишет ${error} и сам передаёт ошибку в ErrorHandler. Итог: с логом, без потерь.
Это тот же порядок, что используют соседние фреймворки в своих дефолтных сборках: в gin.Default() роутер получает Logger, а затем Recovery — по той же причине. RequestID ставится ещё раньше логгера, потому что тот хочет печатать его значение. Общее правило читается легко: читайте список app.Use сверху вниз как маршрут запроса снаружи внутрь — то, что должно видеть всё, вешайте раньше; то, что должно чинить падения, — сразу после наблюдателей.
Соберите каркас middleware
Расставьте строки main.go в правильном порядке: от создания приложения до запуска. Помните, кто кого должен видеть.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Допишите настройку встроенных слоёв
Вставьте пропуски: имя пакета, тег таймера, фабрика recover и тег ошибки.
return nil без c.Next() — самый тихий баг из возможных: никто не паникует, лог пуст, а клиент получает пустой ответ 200. Fiber не считает завершение без Next ошибкой — он не может знать, намеренно ли вы закончили обработку. Если слой обязан пропускать запрос дальше, заканчивайте его return c.Next(): ошибка внутреннего слоя прокинется автоматически, а ответ уйдёт по назначению.
logger замеряет latency только тогда, когда тег ${latency} присутствует в формате, — это видно по логике конфигурации в logger.go. Убрали тег из формата — исчез и замер, ноль накладных расходов. В обратную сторону это тоже подсказка: если в логе латентность пустая, первым делом проверьте формат.
Словарь урока
Запуск, проверка и что дальше#
main.go из урока самодостаточен: создайте рядом папку public с index.html и стилем, запустите go run . и прогоните набор проверок. Первая — curl /api/notes: JSON в ответе и строка лога с requestid, статусом и латентностью в терминале. Вторая — curl /css/style.css: файл со статической раздачей и точно такая же строка лога — слои честно оборачивают и статику. Третья — curl -i на любой путь: в заголовках ответа появится X-Request-ID. Четвёртая — временно добавьте в хендлер panic("check") и убедитесь, что клиент получил 500 с телом «check», процесс жив, а в логе ошибка видна в ${error}. Финальный эксперимент: поменяйте местами logger и recover и найдите пропавшую строку лога на паникующем запросе. В браузерной песочнице сайта сетевые приложения не запускаются — runner-ов в уроках Fiber нет, проверяйте в терминале с локальным Go.
Следующий шаг по плану — свои middleware: напишем аутентификацию-заглушку, таймер и передачу данных между слоями руками, чтобы понять, из чего состоят готовые пакеты. Механика c.Next() и прерывания, которую вы отработали сегодня, — ровно тот инструмент. Про логирование в реальных сервисах — slog, корреляция по RequestID — будет отдельный урок ближе к концу курса; а освежить параметры запроса можно в уроке 4, полный список тем — на странице курса Fiber.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Middleware в Fiber — обычный Handler func(Ctx) error (в v3 Ctx — интерфейс); подключается через app.Use, префикс, аргументы маршрута или монтирование приложения.
- c.Next() передаёт эстафету следующему слою; код после него исполняется на обратном пути — модель луковицы: запрос внутрь, ответ наружу.
- return err до c.Next() прерывает цепочку: ошибку обрабатывает ErrorHandler — *fiber.Error даёт свой код, обычная ошибка — 500 с телом err.Error().
- logger.New() пишет access-log по формату из тегов ${status}, ${latency}, ${method}, ${path}, ${error}; таймер тикает только при наличии ${latency}, а ошибки цепочки логгер передаёт в ErrorHandler сам.
- recover.New() превращает панику в ошибку и ответ 500; без него fasthttp роняет весь процесс — восстановления паник на уровне сервера нет.
- requestid.New() ведёт X-Request-ID: валидный входящий ID переиспользуется, иначе генерируется SecureToken-ом; теги ${requestid} и ${request-id} доступны в логгере.
- Рабочий порядок слоёв: requestid → logger → recover → статика → маршруты; логгер стоит раньше recover, чтобы не терять строки лога о паниках.
Домашнее задание
Возьмите SPA-сервер из урока 5 и добавьте к нему requestid, logger и recover. Составьте формат с ${requestid} и ${error}, проверьте curl -i заголовок X-Request-ID на API и на статике. Уроните один хендлер паникой и убедитесь, что клиент получил 500 с текстом паники, процесс жив, а лог записал ошибку. Затем поменяйте logger и recover местами и опишите одной фразой, что именно исчезло из лога. Наконец, добавьте в конфиг логгера Next, пропускающий /api/health, и объясните результат.
Личные заметкиПусто
Вопросы и ответы по уроку
Чем middleware в Fiber отличается от middleware в Echo?
Сигнатурой. В Echo middleware — это func(next echo.HandlerFunc) echo.HandlerFunc: следующий слой приходит аргументом, и вы зовёте его как функцию — next(c). В Fiber middleware — обычный Handler func(fiber.Ctx) error, а эстафету передаёт метод контекста c.Next(). Обе модели рисуют одну и ту же луковицу, но при переезде между фреймворками перепутанный вызов — ошибка номер один.
Что будет при панике в хендлере, если не подключать recover?
Fasthttp не восстанавливает паники — в его server.go прямо сказано, что любая паника уронит весь сервер. Процесс завершится вместе со всеми обслуживаемыми запросами; перезапускать придётся внешнему супервизору. recover.New() перехватывает панику, превращает её в error, и ErrorHandler отвечает 500 (или кодом *fiber.Error, если паниковали ею).
Логгер подключен, но строки лога не появляются. Почему?
Чаще всего слой зарегистрирован после маршрутов: цепочка идёт по порядку стека, и хендлер, не вызывающий c.Next(), завершает обработку сам, не дойдя до логгера. Ставьте app.Use(logger.New()) до маршрутов и до статики. Вторая причина — в конфиге указан Skip, который отключает запись для сматченных запросов, при этом сам middleware продолжает работать.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
Следующая ступень учебного пути
Gin: самый популярный веб-фреймворк Go
Курс Gin для Go: маршрутизация, binding, валидация, middleware, JWT, WebSocket, тесты и деплой. 25 уроков с кодом и итоговым REST API.
- Уверенно владеть gin.Engine: маршруты, группы, версии
- Привязывать запросы к структурам (ShouldBindJSON и др.)
- Писать middleware и использовать встроенные
- Строить аутентификацию на JWT