---
title: "Middleware: понятие и встроенные"
description: "Middleware в Fiber: сигнатура Handler, цепочка вызовов и c.Next(), порядок луковицы, прерывание ошибкой, встроенные Logger, Recover и RequestID."
course: "Fiber: экспресс-подобный веб-фреймворк на Fasthttp"
url: "https://golangmaster.ru/courses/fiber/lessons/06-middleware/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 20
---

# Middleware: понятие и встроенные

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

Тема «fiber middleware» — это механизм, на котором держится каждый сервер Fiber: логи, таймеры, идентификаторы запросов и защита от паник не пишут внутри хендлеров, а вешают слоями на общую цепочку. Разберём сигнатуру Handler по исходникам v3, механику c.Next() и луковицы, прерывание цепочки ошибкой, а затем три встроенных middleware — Logger, Recover и RequestID — с их конфигами, тегами и граблями порядка регистрации.

### fiber middleware: слой вокруг каждого обработчика

Middleware — это функция того же типа, что и обычный хендлер, которую Fiber исполняет не ради ответа, а вокруг ответов: логирует, ставит заголовки, проверяет доступ, ловит паники. Вы уже встречались с этой механикой, не называя её: в [уроке 5](/courses/fiber/lessons/05-staticheskie-fajly/) статик-хендлер при промахе мимо файла вызывал 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 — там цена перестановки конкретная: потерянная строка лога.

### В каком порядке уйдут строки в консоль?

```go
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")
})
```

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

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

Классическая луковица: Use складывает оба слоя в стек в порядке регистрации, запрос проходит их снаружи внутрь — A, затем B, затем хендлер; на обратном пути слои отпускаются в обратном порядке — B после, потом A после. Последний вариант соблазнителен, но неверен: после c.Next() управление возвращается тому, кто его вызвал, — сосед по стеку не перепрыгивается.

### Прерывание цепочки: 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(). Что получит клиент?

- ✅ Статус 404 и тело «нет такой заметки»
- Статус 500 и тело «нет такой заметки»
- Статус 200 и пустое тело
- Статус 404 и стандартное «Cannot GET /api/notes»

DefaultErrorHandler видит *fiber.Error и берёт её код — 404; поскольку ошибка не пустая, в тело уходит её текст. «Cannot GET» — ответ роутера, когда маршрут не нашёлся вовсе, а тут маршрут нашёлся, просто слой прервал цепочку. Обычная (не-fiber) ошибка с тем же текстом дала бы 500 — тип ошибки решает.

### Куда вешать 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: дефолт и свой формат

```go
import "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, "нет доступа"). Что произойдёт?

- ✅ В ${error} попадёт «нет доступа», клиент получит 403
- В ${error} попадёт «нет доступа», клиент получит 500: логгер не знает статус
- Лог останется пустым, клиент получит 403
- Ошибка не дойдёт до логгера: цепочка оборвётся до него

Проверено по logger.go: после c.Next() логгер сохраняет chainErr и, если он не nil, сам вызывает ErrorHandler приложения — тот отвечает 403 по коду *fiber.Error. Поэтому и лог честный, и ответ корректный. Важное следствие: логгер, стоящий в цепочке до места ошибки, видит её — оборванная цепочка не прячет ошибки от вышестоящих слоёв.

### 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: две настройки

```go
import "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-комментарием с пометкой о переименовании, но пока пишется именно так.

### Что получит клиент вместо ответа?

```go
app.Use(recover.New())

app.Get("/boom", func(c fiber.Ctx) error {
        panic("boom")
})
```

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

- ✅ 500 с телом «boom»
- 500 с телом «Internal Server Error»
- 200 с пустым телом: ответ уже записан
- Процесс упадёт: fasthttp не ловит паники

Паника строкой «boom» доходит до recover-слоя и превращается DefaultPanicHandler-ом в обычную ошибку fmt.Errorf("%v", r). ErrorHandler не находит *fiber.Error и отвечает 500 с телом err.Error() — то есть «boom». Последний вариант был бы правдой без recover.New: без слоя паника уронила бы весь процесс. Именно поэтому recover ставят в каждый Fiber-сервис без исключений.

### Сервер без recover-слоя

**Вопрос:** В приложении НЕТ middleware recover. Один из хендлеров паникует на запросе. Что произойдёт с сервером?

- Паника попадёт в лог, сервер продолжит обслуживать запросы
- fasthttp перехватит панику и вернёт клиенту 500
- ✅ Весь процесс упадёт: восстановления паник в fasthttp нет
- Fiber вернёт 503 и перезапустит хендлер

Первый вариант описывает поведение net/http — там http.Server ловит панику хендлера и живёт дальше, и привычка переносится из стандартной библиотеки. Fiber работает поверх fasthttp, где, по комментарию в server.go, recovery не выполняется вовсе: паника роняет процесс целиком. Никакого автоматического перезапуска тоже нет — отсюда правило: recover.New() подключается одним из первых слоёв в каждом приложении.

### 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 заметок

```go
package 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](/courses/fiber/lessons/05-staticheskie-fajly/) «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 в правильном порядке: от создания приложения до запуска. Помните, кто кого должен видеть.

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

1. `app := fiber.New()`
2. `app.Use(requestid.New())`
3. `app.Use(logger.New())`
4. `app.Use(recover.New())`
5. `app.Get("/api/notes", notesHandler)`
6. `log.Fatal(app.Listen(":3000"))`

### Допишите настройку встроенных слоёв

Вставьте пропуски: имя пакета, тег таймера, фабрика recover и тег ошибки.

```go
func main() {
        app := fiber.New()

        // RequestID: идентификатор запроса в заголовке X-Request-ID
        app.Use(___.New())

        // Таймер latency тикает, только если тег есть в формате
        app.Use(logger.New(logger.Config{
                Format: "${status} ${___} ${method} ${path} ${___}\n",
        }))

        // Recover: перехват паник из внутренних слоёв
        app.Use(recover.___())

        app.Get("/api/health", func(c fiber.Ctx) error {
                return c.JSON(fiber.Map{"ok": true})
        })

        log.Fatal(app.Listen(":3000"))
}
```

Подсказка: Пакет requestid; таймер включается тегом ${latency}; текст ошибки цепочки несёт ${error}; recover подключается фабрикой recover.New().

Правильные ответы в пропусках: `requestid`, `latency`, `error`, `New`

> **Тихая остановка цепочки.** return nil без c.Next() — самый тихий баг из возможных: никто не паникует, лог пуст, а клиент получает пустой ответ 200. Fiber не считает завершение без Next ошибкой — он не может знать, намеренно ли вы закончили обработку. Если слой обязан пропускать запрос дальше, заканчивайте его return c.Next(): ошибка внутреннего слоя прокинется автоматически, а ответ уйдёт по назначению.

> **Таймер, которого нет.** logger замеряет latency только тогда, когда тег ${latency} присутствует в формате, — это видно по логике конфигурации в logger.go. Убрали тег из формата — исчез и замер, ноль накладных расходов. В обратную сторону это тоже подсказка: если в логе латентность пустая, первым делом проверьте формат.

### Словарь урока

- **type Handler = func(Ctx) error** — Единая сигнатура хендлеров и middleware в Fiber v3 (app.go); Ctx — интерфейс. В v2 контекст был указателем *fiber.Ctx.
- **c.Next()** — Передача эстафеты следующему слою цепочки; код после него исполняется на обратном пути луковицы. В Echo ту же роль играет аргумент next(c).
- **луковица** — Модель цепочки: запрос идёт снаружи внутрь до хендлера, ответ возвращается обратно через те же слои. До c.Next() — фаза запроса, после — фаза ответа.
- **logger.DefaultFormat** — «[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}» — теги-переменные; в консоли дефолт рендерится «столбиками». Есть готовые CommonFormat, CombinedFormat, JSONFormat, ECSFormat.
- **recover.New()** — Превращает панику внутренних слоёв в error: ErrorHandler отвечает 500 (или кодом *fiber.Error). Без него fasthttp роняет весь процесс.
- **requestid.New()** — Ведёт заголовок X-Request-ID: валидный входящий ID переиспользуется, иначе генерируется SecureToken-ом; тег ${requestid} доступен в формате logger.

### Запуск, проверка и что дальше

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](/courses/fiber/lessons/04-parametry-zaprosa/), полный список тем — на странице [курса Fiber](/courses/fiber/).

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

- 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 продолжает работать.
