---
title: "Интеграция с фреймворками (net/http)"
description: "Connect как http.Handler: монтаж на ServeMux, chi и echo, middleware net/http против интерсепторов, StripPrefix, h2c, health и graceful shutdown."
course: "Connect: современный RPC от Buf"
url: "https://golangmaster.ru/courses/connect/lessons/19-net-http/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 18
---

# Интеграция с фреймворками (net/http)

*Курс «Connect: современный RPC от Buf», урок 19 из 25. ~18 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/connect/lessons/19-net-http/*

connect net http — это не мост между мирами, а одно и то же: хендлер Connect — обычный http.Handler, и вся экосистема net/http работает для него без адаптеров. Монтируем сервис на ServeMux и chi с echo, разводим middleware net/http и интерсепторы по границе ответственности, ставим h2c за завершающим TLS ingress и закрываем сервис graceful shutdown-ом.

Самый практичный факт про Connect вы уже знаете с третьего урока, но сейчас он раскроется целиком: NewNoteServiceHandler возвращает строку пути и handler типа http.Handler. Не особый интерфейс, не магический тип — ровно тот http.Handler с методом ServeHTTP, на котором стоит весь стандартный Go. Из этого следует всё содержание урока: ваш RPC-сервис монтируется на любой mux — стандартный ServeMux, chi, echo — рядом с обычными HTTP-роутами; оборачивается любыми middleware net/http; живёт за теми же прокси и балансировщиками; выключается тем же graceful shutdown-ом. Интеграция connect не требует ни одного пакета-адаптера, потому что адаптер уже написан: сам Connect построен на net/http — это его принципиальное отличие от gRPC, который принёс свой транспорт. Седьмой урок обещал, что мы обернём хендлер в цепочки chi и Echo — обещание исполняется в этом уроке. В тринадцатом FAQ мы зафиксировали границу «про RPC — в интерсептор, про HTTP — в middleware» — теперь построим ту цепочку руками. План: монтаж и соседи по mux, middleware-паттерн и его порядок, chi/echo/gin и честный разговор про fiber, StripPrefix и префиксы, h2c для plaintext HTTP/2, health-checks и корректное выключение.

### Хендлер — это http.Handler: что из этого следует

Разберём сам факт до конца. Первый аргумент возврата NewNoteServiceHandler — путь /notes.v1.NoteService/ — строка, под которой зарегистрированы все процедуры сервиса; второй — http.Handler, который принимает каждый вызов, разбирает протокол (Connect, gRPC или gRPC-Web — механика четвёртого урока) и диспатчит на ваш implementation. Связка connect net http буквальна до конца: так как это http.Handler, к нему применимо всё, что вы знаете о net/http, и это не метафора, а буквальная замена компонентов: http.Server с TLS-конфигом из тринадцатого урока обслуживает RPC-вызовы тем же кодом, что и статику; middleware из любого учебника по Go оборачивают его без единой правки; httptest.NewServer из шестнадцатого урока поднимает его для тестов той же строкой, что и обычный роутер. Обратная сторона той же медали: ограничения net/http тоже ваши — таймауты сервера, лимиты заголовков, поведение при паниках, модель горутин на соединение. В gRPC-мире интеграция с обычным HTTP-миром — проект: grpc-go живёт в своей горутине с своим транспортом, и совместный mux требует cmux или отдельных портов. В connect-мире вопрос «как поставить RPC рядом с health-check и метриками» — это одна пара mux.Handle. Именно за это свойство Connect ценят в командах со смешанной инфраструктурой: RPC-сервис не требует изменений в платформе, которая уже гоняет HTTP.

### Базовый монтаж: RPC, healthz и metrics на одном mux

```go
mux := http.NewServeMux()

// соседи по mux — обычные HTTP-роуты (паттерны Go 1.22: метод + путь)
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        w.Write([]byte("ok"))
})
mux.Handle("GET /metrics", promhttp.Handler()) // из семнадцатого урока

// RPC-хендлер: строка пути + http.Handler
path, handler := notesv1connect.NewNoteServiceHandler(
        &notesServer{},
        connect.WithInterceptors(&metricsInterceptor{}, &authInterceptor{}),
)
mux.Handle(path, handler)

log.Fatal(http.ListenAndServe(":8080", mux))
```

Вся сборка — стандартный net/http: два обычных роута и один RPC-хендлер на одном mux. Паттерны Go 1.22 ограничивают методы соседей; путь RPC-сервиса приходит из генератора и покрывает все его процедуры. Интерсепторы из семнадцатого и тринадцатого уроков живут внутри хендлера, middleware — снаружи: две линии обороны, о которых ниже.

**Вопрос:** Что возвращает NewNoteServiceHandler?

- Готовый http.Server, который остаётся только запустить
- ✅ Пару: строку пути для регистрации и значение типа http.Handler
- Экземпляр вашего имплементационного типа notesServer
- chan connect.AnyResponse, который надо читать в горутине

Пара (путь, http.Handler) — весь контракт интеграции. Путь подставляется в mux.Handle, хендлер оборачивается любыми middleware и монтируется куда угодно. Сервер вы собираете сами — http.Server с тем Handler-ом, — и в этом гибкость: TLS, таймауты, h2c и graceful shutdown настраиваются стандартными средствами net/http, а не опциями Connect.

### ServeMux 1.22 и когда приходит chi

Стандартный http.ServeMux после Go 1.22 вырос из игрушки в рабочий инструмент, и для многих сервисов этого достаточно: паттерны с методом (GET /healthz), сегменты-подстановки (/users/{id} и r.PathValue("id")), завершающий слэш-суффикс {path...} для префиксов. Если ваш сервис — RPC плюс пара служебных роутов, депенденси не нужен: ServeMux лёгок, предсказуем и уже в рутине. chi приходит, когда нужен настоящий роутинг-стек: группы роутов с middleware на группу, вложенные маршруты, сотни роутов с общими префиксами, комбинаторика middleware по поддеревьям. Echo — когда команда уже живёт в его экосистеме: те же группы, свой контекст запроса, свои хелперы ошибок, обширная коллекция middleware. Важно понимать цену выбора: всё это — роутеры поверх net/http, они принимают http.Handler и отдают http.Handler, поэтому connect-хендлер садится в них нативно, без адаптеров — интеграция connect сводится к одному вызову в каждом фреймворке. Отдельный жанр — REST-соседи RPC: на том же mux удобно держать несколько публичных GET-роутов для простых клиентов; полноценный REST-шлюз над proto-схемой — тема двадцать первого урока, там же сравнение с grpc-gateway, а сейчас — только соседи по роутеру.

### Middleware net/http: паттерн и порядок

Middleware net/http — это функция вида func(http.Handler) http.Handler: принимает следующий обработчик, возвращает обёрнутый. Внутри — замыкание с вашей логикой до и после next.ServeHTTP: до — подготовка (таймер, контекст, лимиты), после — фиксация (лог, метрика), вокруг — защита (recover). Композиция — матрёшка: withLogging(withRecovery(mux)) означает, что logging снаружи, recovery внутри, mux в центре; запрос проходит logging → recovery → mux, ответ — обратно. Порядок — это контракт, как у WithInterceptors из седьмого урока: внешний слой видит весь жизненный цикл запроса, включая то, что внутренние слои сделали с ответом. Классическая пара — recovery внутри logging: тогда лог-строка пишется и для паниковавших запросов, после того как recovery их погасил и вернул 500. Поменяйте порядок — и запрос с паникой останется без лог-строки: внешний recovery погасит её раньше, чем logging увидит запрос. Дежурная коллекция middleware: logging с длительностью, recovery с честным 500, лимит тела http.MaxBytesReader, таймаут через http.TimeoutHandler (с оговоркой для стримов — ниже), заголовки безопасности. Всё это живёт на уровне HTTP: middleware не знает ни процедур, ни сообщений — он видит пути и статусы, и это нормальная точность для его задач.

### Пара recovery + logging

```go
func withRecovery(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                defer func() {
                        if rec := recover(); rec != nil {
                                log.Printf("panic: %v", rec)
                                http.Error(w, "internal error", http.StatusInternalServerError)
                        }
                }()
                next.ServeHTTP(w, r)
        })
}

func withLogging(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                start := time.Now()
                next.ServeHTTP(w, r)
                log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
        })
}

handler := withLogging(withRecovery(mux))
log.Fatal(http.ListenAndServe(":8080", handler))
```

Обёртка через http.HandlerFunc — канонический паттерн без зависимостей. Recovery снаружи mux-а защищает и RPC-хендлер, и соседей по роутеру; logging видит все запросы, включая /metrics и /healthz. Заметьте, что RPC-лог по-прежнему пишут интерсепторы из седьмого урока — с процедурой и кодом; middleware-лог дополняет картину путями и статусами HTTP.

### Что окажется в логах при панике?

```go
handler := withLogging(withRecovery(mux))
// Хендлер GetNote запаниковал: panic("nil map write")
```

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

- Процесс падает: recover работает только в горутине main
- ✅ Клиент получает 500; в логах сначала строка panic, затем строка withLogging с длительностью — внешний слой видел погашенный запрос
- Клиент получает 500; строка withLogging не пишется — паника оборвала внешний хендлер
- Клиент получает ответ с кодом internal: панику превращает в ошибку сам Connect

Порядок withLogging(withRecovery(mux)): recovery внутри гасит панику, пишет panic-строку и отправляет 500; управление возвращается в withLogging, который видит завершившийся запрос и пишет свою строку с длительностью. Если поменять местами — recovery снаружи погасит панику до того, как logging продолжится, и лог-строки не будет. По умолчанию net/http тоже гасит паники на каждый запрос, но без честного 500 и с обрывом соединения — потому recovery-хелпер и пишут.

### Граница ответственности: middleware против интерсепторов

Седьмой урок построил карту, тринадцатый закрепил её в FAQ — теперь она стала кодом, и стоит сформулировать правило одной строкой: то, что про HTTP, — в middleware; то, что про вызов RPC, — в интерсептор. В middleware: recovery (защита всего процесса HTTP, включая соседей по mux), логирование на уровне путей и статусов, редиректы, заголовки ответа, CORS для браузерной границы из девятого урока, лимиты соединений. В интерсепторах: аутентификация и авторизация с req.Spec().Procedure, логирование с процедурой и кодом, метрики вызовов из семнадцатого урока, ретраи и трассировка на клиенте. Два критерия помогают решать пограничные случаи. Критерий видимости: если логике нужна процедура или сообщение — интерсептор, middleware их не видит. Критерий стороны: интерсепторы работают и на клиенте (клиентская луковица из седьмого урока), middleware — только на сервере; логика, нужная обоим концам, живёт в интерсепторе. Пограничный жанр — таймауты: per-call дедлайн ставит клиент контекстом из пятого урока, сервер его уважает автоматически; общий таймаут HTTP — http.TimeoutHandler, но для стримов из восьмого урока он вреден: длинный bidi-поток убьётся по таймеру даже при активном обмене, поэтому глобальный таймаут либо ставят только на unary-подобные пути, либо держат дедлайны на уровне контекста вызова.

**Вопрос:** Паника в RPC-хендлере без middleware recovery — что делает net/http по умолчанию?

- Весь процесс завершается: паника не локализована
- ✅ Сервер гасит панику на конкретном запросе: пишет стек в лог, рвёт текущее соединение; остальные соединения живут
- Паника превращается в connect.Error с кодом internal и уезжает клиенту
- Запрос зависает до таймаута клиента

Стандартный сервер обслуживает каждое соединение в своей горутине и восстанавливается после паники на уровне запроса: стек залогирован, соединение разорвано, сервис жив. Клиент при этом получает обрыв, а не аккуратный 500 с кодом internal — и для стрима это разрыв посреди потока. Поэтому recovery-middleware и пишут: она превращает панику в чистый 500 по HTTP и в connect-ошибку для тех вызовов, которые успели начаться, — а перехват паники в интерсепторе с превращением в код internal остаётся тонкой настройкой из седьмого урока.

### chi, Echo, Gin: нативная посадка

Посадка в популярные роутеры — по одному вызову, потому что все они живут на http.Handler. chi: r.Handle(path, handler) или r.Mount(prefix, handler) для поддерева; middleware chi — через r.Use(withLogging, withRecovery) — и connect-хендлер оказывается внутри всей цепочки автоматически. Echo: e.Any(path, echo.WrapHandler(handler)) — обёртка из коробки; с middleware echo.WrapMiddleware если хотите вклинить echo-хендлер в общий слой. Gin: r.Any(path, gin.WrapH(handler)) — симметричная обёртка. Во всех трёх случаях путь RPC-хендлера берётся у генератора, как и со стандартным mux, и клиенты не замечают, на каком роутере живёт сервер. Честный разговор про fiber: он не построен на net/http — fiber стоит на fasthttp, другой реализации HTTP с другими интерфейсами; connect-хендлер в него не вставляется напрямую, нужен адаптер fasthttp-to-net/http с конвертацией контекста, а это трение, дублирование и риск в горячем пути. Практический вывод курса: connect-сервис держите на net/http-стеке (чистый ServeMux, chi, echo, gin — что угодно из http.Handler-мира), а fiber — для сервисов, где Connect не участвует. Ключевая фраза для поиска в доках — connect middleware: раздел интеграции показывает ровно эти обёртки для каждого фреймворка.

### Монтаж на chi и Echo

```go
// chi
r := chi.NewRouter()
r.Use(withLogging, withRecovery)          // middleware на всё дерево
r.Get("/healthz", healthHandler)
r.Mount(notesPath, notesHandler)           // notesPath и notesHandler от генератора

// Echo
e := echo.New()
e.Use(echoWrapMiddleware)
e.GET("/healthz", healthEcho)
e.Any(notesPath, echo.WrapHandler(notesHandler))
```

Оба роутера принимают http.Handler без адаптеров: chi — через Mount с префиксом от генератора, Echo — через echo.WrapHandler. Проверяйте, что middleware зарегистрированы до монтирования хендлера, — иначе часть запросов пройдёт мимо цепочки. Клиентам всё равно, какой роутер внутри: путь процедуры тот же, протокол тот же.

### На каком пути ответит GetNote?

```go
api := http.NewServeMux()
path, handler := notesv1connect.NewNoteServiceHandler(&notesServer{})
api.Handle(path, handler) // /notes.v1.NoteService/

root := http.NewServeMux()
root.Handle("/rpc/", http.StripPrefix("/rpc", api))

client := notesv1connect.NewNoteServiceClient(httpClient, "http://localhost:8080/rpc")
```

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

- /notes.v1.NoteService/GetNote — StripPrefix не влияет на исходящий путь
- ✅ /rpc/notes.v1.NoteService/GetNote — клиент шлёт с префиксом, StripPrefix снимает его перед внутренним mux
- /rpc/rpc/notes.v1.NoteService/GetNote — префикс дублируется на каждом слое
- 404: StripPrefix ломает путь RPC-хендлера

Базовый URL клиента содержит /rpc, поэтому запрос уходит на /rpc/notes.v1.NoteService/GetNote. Внешний mux маршрутизирует его по префиксу /rpc/ и StripPrefix срезает /rpc перед передачей внутрь — внутренний mux видит чистый путь /notes.v1.NoteService/GetNote, под который хендлер и зарегистрирован. Пара «базовый URL с префиксом + StripPrefix на границе» — типовая схема, когда RPC живёт под общим префиксом деплоя.

### Префиксы и несколько сервисов

Реальный бэкенд — не один сервис, и mux справляется с этим без церемоний: NewNoteServiceHandler и, скажем, NewUserServiceHandler монтируются рядом, у каждого свой путь из имени пакета — /notes.v1.NoteService/ и /users.v1.UserService/ — маршрутизация автоматична, как и для версий из восемнадцатого урока, которые вы можете смонтировать тут же третьей и четвёртой парой. Префикс появляется, когда деплой-топология требует общего сегмента пути: API-шлюз снимает свой префикс, или все RPC живут под /rpc, или версия деплоя видна в пути. Инструмент — http.StripPrefix: внешний mux маршрутизирует по префиксу и срезает его перед внутренним mux, где хендлеры зарегистрированы под своими родными путями; критично, чтобы базовый URL клиентов содержал тот же префикс, иначе внешний mux не найдёт путь. Обратный вариант — mount без StripPrefix: если внутренний хендлер умеет жить с префиксом (chi Mount так делает сам), обёртка не нужна. Практический совет: один префикс на всю RPC-плоскость проще, чем разные, — тогда ingress-правила из двадцать второго урока будут однострочными, а дашборды метрик из семнадцатого будут читать procedure без сюрпризов. Помните также, что mux.ServeHTTP линейно перебирает паттерны — на сотнях роутов это не проблема, но измеряйте, если подозреваете маршрутизацию в профиле CPU.

### h2c: HTTP/2 без TLS внутри контура

Сценарий из тринадцатого урока: TLS терминируется на ingress, дальше к сервису едет plaintext HTTP. Тут ждёт подводный камень, который кусает всех, кто гоняет gRPC-жанры: grpc-клиент требует HTTP/2, а net/http по умолчанию говорит HTTP/2 только поверх TLS; plaintext-соединение сервер умеет только в HTTP/1.1 — и gRPC-вызовы за Terminating-TLS-балансировщиком падают с загадочными ошибками протокола. Connect-протокол из четвёртого урока неприхотлив — он работает и на HTTP/1.1, это осознанное свойство дизайна, — но если ваши жанры включают gRPC или gRPC-Web для старых клиентов, сервер должен уметь HTTP/2 без TLS. Ответ — h2c: пакет golang.org/x/net/http2/h2c, обёртка h2c.NewHandler(mux, &http2.Server{}) перед ListenAndServe. Она позволяет серверу принять HTTP/2-апгрейд или prior-knowledge соединение на plaintext-порту — и gRPC-жанры оживают за TLS-балансировщиком при одном условии: ingress обязан проксировать HTTP/2 внутрь, а не даунгрейдить до 1.1; это настройка ingress, и в двадцать втором уроке она займёт своё место в деплой-чек-листе. Запомните трио: TLS на границе, h2c внутри, HTTP/2-проксирование в ingress — стандартная топология gRPC-совместимых сервисов на Connect.

### Сервер с h2c

```go
import (
        "golang.org/x/net/http2"
        "golang.org/x/net/http2/h2c"
)

mux := http.NewServeMux()
// ... healthz, metrics, notes handler ...

srv := &http.Server{
        Addr:    ":8080",
        Handler: h2c.NewHandler(mux, &http2.Server{}),
}
log.Fatal(srv.ListenAndServe())
```

Одна обёртка Handler — и plaintext-порт принимает и HTTP/1.1 (Connect-протокол), и HTTP/2 (gRPC-жанры за TLS-ingress). Проверка после деплоя: вызов grpcurl-ом и вызов curl-ом на один порт — оба должны работать; если gRPC падает, а Connect жив, смотрите, что ingress реально проксирует HTTP/2, а не даунгрейдит.

**Вопрос:** TLS терминируется на ingress, внутрь — plaintext. Что произойдёт с gRPC-клиентом без h2c на сервере?

- Всё работает: gRPC умеет HTTP/1.1 как Connect
- ✅ gRPC-вызовы падают: жанр требует HTTP/2, а plaintext-сервер без h2c говорит только HTTP/1.1; Connect-вызовы работают
- Падают оба: без TLS net/http не принимает запросы
- gRPC работает только при prior-knowledge, Connect — никогда

gRPC-протокол строится на HTTP/2-механике — стримах, заголовках, трейлерах — и без HTTP/2 невозможен в принципе. Connect-протокол спроектирован совместимым с HTTP/1.1 (урок четвёртый): обычные запросы и ответы без трейлеров. Обёртка h2c.NewHandler учит plaintext-порт понимать HTTP/2, и тогда за TLS-ingress живут все три жанра; ещё одно условие — ingress должен проксировать HTTP/2 внутрь.

### Health-checks и graceful shutdown

Последние два слоя интеграции — жизнь и смерть сервиса. Health-checks: на том же mux живут GET /healthz (жив ли процесс) и по желанию /readyz (готов ли принимать трафик) — обычные http.HandlerFunc без единой connect-строки; оркестратор пингует их, а RPC-плоскость остаётся чистой от служебных маршрутов. Готовность честнее всего связывать с реальными зависимостями: проверка коннекта к базе с таймаутом в полсекунды — но держите её лёгкой, readiness пингуют часто. Выключение: graceful shutdown — это гарантия, что деплой не рвёт активные вызовы. Схема: http.Server с h2c-обёрткой, сигнал SIGTERM ловится signal.NotifyContext, затем srv.Shutdown(ctx) — сервер перестаёт принимать новые соединения и ждёт активные до дедлайна контекста; для RPC-мира важна деталь: unary-вызовы успевают завершиться за типичный grace-период 15-30 секунд, а долгие bidi-стримы не закончатся сами — их гасит дедлайн, и клиент из пятого урока обязан переживать обрыв ретраем или переподключением; держите grace-период больше p99 длительности unary из семнадцатого урока — вот где метрики встречаются с деплоем. После Shutdown закрываются ресурсы: базы, кэши. Кейсы, когда Shutdown не спасает: незакрытые стримы без дедлайнов — ставьте таймаут на контекст стрима; клиенты без ретраев — деплой всё равно переживут благодаря балансировщику, выводящему поду из ротации до сигнала, — это связка readiness и деплоя, её разбор в двадцать втором уроке.

### Graceful shutdown скелет

```go
srv := &http.Server{
        Addr:    ":8080",
        Handler: h2c.NewHandler(withLogging(withRecovery(mux)), &http2.Server{}),
}

ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()

go func() {
        if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
                log.Fatalf("listen: %v", err)
        }
}()

<-ctx.Done() // SIGTERM от оркестратора

shutdownCtx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
        log.Printf("shutdown: %v", err)
}
db.Close()
```

Полный жизненный цикл: сервер в горутине, блокировка до сигнала, Shutdown с дедлайном 20 секунд — больше p99 unary из метрик, — затем закрытие зависимостей. Активные вызовы доезжают, новые отклоняются; долгие стримы гасятся дедлайном, и клиент обязан переживать обрыв. Оркестратор подождёт readiness-пробу соседней версии, прежде чем слать SIGTERM следующей.

### Где решать задачу: middleware или интерсептор

| Задача | Место | Инструмент | Почему там |
| --- | --- | --- | --- |
| recovery после паник | middleware | обёртка с recover вокруг mux | защищает все роуты и превращает панику в честный 500 |
| лог HTTP-запросов | middleware | logging-обёртка пути и статуса | видит и RPC, и /metrics, и /healthz одним слоем |
| аутентификация вызова | интерсептор | WrapUnary + conn.RequestHeader | нужны процедура и коды 401/403 из тринадцатого урока |
| метрики вызовов | интерсептор | интерсептор из семнадцатого урока | метки procedure и code недоступны middleware |
| лимит сообщения | опция хендлера | WithReadMaxBytes | цена считается на уровне сообщения, не HTTP-тела |
| дедлайн вызова | контекст | с клиента из пятого урока | per-call семантика; http.TimeoutHandler режет стримы |

### Соберите middleware и монтаж

Восстановите сигнатуру middleware и регистрацию хендлера.

```go
func withLogging(next http.___) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                start := time.Now()
                next.ServeHTTP(w, r)
                log.Printf("%s %v", r.URL.Path, time.Since(start))
        })
}

mux := http.NewServeMux()
mux.___("GET /healthz", healthHandler)
path, handler := notesv1connect.NewNoteServiceHandler(&notesServer{})
mux.Handle(path, ___)
```

Подсказка: Middleware принимает и возвращает http.Handler; регистрацию делает метод mux.Handle; в регистрацию уходит второй элемент пары от генератора — handler.

Правильные ответы в пропусках: `Handler`, `Handle`, `handler`

### Путь запроса через полный стек

Расставьте события одного вызова CreateNote от приёма до ответа.

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

1. `http.Server принимает соединение — TLS на ingress, h2c внутри`
2. `withLogging ставит таймер и пропускает запрос внутрь`
3. `withRecovery держит defer с recover на случай паники`
4. `mux маршрутизирует путь в Connect-хендлер по /notes.v1.NoteService/`
5. `хендлер разбирает протокол: Connect, gRPC или gRPC-Web`
6. `интерсепторы считают метрики и проверяют токен`
7. `имплементация создаёт заметку и возвращает сообщение`
8. `ответ идёт обратно: лог-строка пишется, соединение доезжает до клиента`

> **fiber — не net/http, и TimeoutHandler не для стримов.** fiber стоит на fasthttp — другой реализации HTTP с несовместимыми интерфейсами: connect-хендлер в него не вставляется без адаптера с конвертацией контекста; для Connect-сервисов выбирайте стек из http.Handler-мира. Вторая ловушка — http.TimeoutHandler как общий таймаут: длинные bidi-стримы будут убиты таймером даже при активном обмене; дедлайны стримов держите в контексте вызова, глобальный таймаут — только на unary-подобных путях.

> **Сначала ServeMux 1.22, chi — по потребности.** Не тяните роутер-фреймворк в сервис с RPC и двумя служебными роутами: паттерны стандартного mux (метод, подстановки, PathValue) покрывают большинство случаев, а меньше зависимостей — меньше сюрпризов при апгрейдах Go. chi добавляйте, когда появились группы с middleware на поддеревья; порядок обёрток фиксируйте комментарием-схемой, как учил седьмой урок для интерсепторов, — это тот же контракт.

> **Как проходить этот курс.** Код урока — настоящий Go: middleware, монтаж и shutdown-скелет копируются в проект из третьего урока один в один и проверяются тестами из шестнадцатого. Браузерная песочница сайта сетевых вызовов не исполняет, поэтому исполняемых блоков в курсе нет — интерактивы работают на странице и дают XP, а сборку проверяйте curl-ом на healthz, metrics и пути процедуры.

### Карточки: Connect и net/http

- **Природа хендлера Connect** — NewNoteServiceHandler возвращает (путь string, http.Handler). Никаких адаптеров: TLS, middleware, httptest, прокси — всё от net/http работает напрямую. gRPC для этого нужен cmux или отдельный порт.
- **Middleware-паттерн** — func(http.Handler) http.Handler: логика до и после next.ServeHTTP. Композиция-матрёшка: withLogging(withRecovery(mux)) — logging внешний. Порядок — контракт: внешний слой видит весь жизненный цикл.
- **Middleware vs интерсептор** — Про HTTP (recovery, лог путей, CORS, редиректы) — middleware; про вызов (процедура, токен, коды, метрики) — интерсептор. Критерии: видимость процедуры/сообщения и сторона (клиент+сервер — только интерсептор).
- **h2c** — HTTP/2 без TLS: h2c.NewHandler(mux, &http2.Server{}) на plaintext-порту. Нужен, когда TLS терминирует ingress и жанры включают gRPC/gRPC-Web, которые требуют HTTP/2. Connect-протокол живёт и на HTTP/1.1.
- **Graceful shutdown** — signal.NotifyContext → srv.Shutdown(ctx с дедлайном): новые соединения не принимаются, активные доезжают. Grace-период больше p99 unary из метрик; долгие стримы гасит дедлайн — клиент обязан переживать обрыв.
- **Монтаж под префиксом** — root.Handle("/rpc/", http.StripPrefix("/rpc", api)): внешний mux маршрутизирует и срезает префикс, внутренний видит родной путь. Базовый URL клиента должен содержать /rpc — иначе внешний mux вернёт 404.

### Что дальше по курсу

Интеграция закрыта со всех сторон: хендлер стоит на общем mux с healthz и метриками, middleware и интерсепторы делят границу ответственности, h2c открывает gRPC-жанры за TLS-ingress, graceful shutdown выключает сервис без разрыва вызовов. Дальше по курсу — миграция с gRPC на Connect: пошаговый план переезда существующего сервиса, где сегодняшняя тема станет фундаментом — хендлеры двух миров встанут на один mux на время перехода. Потом — REST-шлюз и HTTP-совместимость, деплой с ingress и HTTP/2 на границе. Для повторения: первый монтаж и структура проекта — в [третьем уроке](/courses/connect/lessons/03-pervyj-servis/), три протокола и их требования к HTTP — в [четвёртом](/courses/connect/lessons/04-protokoly/), TLS-топология — в [тринадцатом](/courses/connect/lessons/13-bezopasnost/), httptest для проверки всей сборки — в [шестнадцатом](/courses/connect/lessons/16-testirovanie/). Программа всех двадцати пяти уроков — на [странице курса](/courses/connect/).

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

- Хендлер Connect — http.Handler: весь мир net/http работает без адаптеров; gRPC для совместного mux требует cmux или отдельного порта.
- ServeMux 1.22 с паттернами закрывает сервис с RPC и служебными роутами; chi и echo — когда нужны группы и middleware на поддеревья.
- Middleware — func(http.Handler) http.Handler; порядок обёрток — контракт: recovery внутри logging, чтобы паниковавшие запросы попали в лог.
- Граница: про HTTP — middleware (recovery, лог путей, CORS), про вызов — интерсептор (токен, метрики, коды); per-call дедлайны — в контексте.
- Паника без recovery: net/http гасит её на запрос, рвёт соединение и пишет стек; recovery-middleware даёт честный 500.
- h2c.NewHandler учит plaintext-порт HTTP/2 — gRPC-жанры за TLS-ingress живут; ingress обязан проксировать HTTP/2 внутрь.
- Graceful shutdown: Shutdown с grace-периодом больше p99 unary; долгие стримы гасит дедлайн, клиент переживает обрыв ретраем.
- fiber стоит на fasthttp — Connect туда не вставляется без адаптера; для connect-сервисов выбирайте http.Handler-стек.

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

Соберите notes-сервис как полноправного гражданина net/http-мира и попробуйте его на прочность деплоем. Шаг первый: общий mux с GET /healthz, GET /metrics из семнадцатого урока и notes-хендлером; оберните всё в withLogging(withRecovery(mux)) и поднимите через h2c.NewHandler на plaintext-порту. Шаг второй — проверка middleware: добавьте хендлер-паниковщик на тестовый путь и убедитесь curl-ом, что приедет 500, а в логах будут и panic-строка, и лог-строка с длительностью; поменяйте порядок обёрток и зафиксируйте исчезновение лог-строки. Шаг третий — префикс: вынесите RPC под /rpc через StripPrefix, обновите базовый URL клиента из пятого урока и проверьте оба жанра — Connect по HTTP/1.1 и grpc-клиент по HTTP/2 (если собран grpcurl — и его). Шаг четвёртый — выключение: повесьте signal.NotifyContext и Shutdown с дедлайном, запустите долгий bidi-стрим и пошлите SIGTERM; убедитесь, что unary-вызовы доезжают, а стрим гасится дедлайном, и запишите в заметки свой grace-период против p99 из метрик. Шаг пятый: смонтируйте рядом второй сервис (users или версии из восемнадцатого урока) и убедитесь, что пути разъезжаются без конфигурации.

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

### Что ставить между middleware и интерсепторами, если задача подходит обоим?

Спросите: нужна ли логике процедура, код ошибки или сообщение? Если да — интерсептор, middleware этого не видит. Нужна ли логика на клиенте? Интерсептор работает и там, middleware — только на сервере. Правило тринадцатого урока остаётся: про RPC — в интерсептор, про HTTP — в middleware, а задачи типа лимитов решаются третьим способом — опциями хендлера (WithReadMaxBytes).

### Как монтировать Connect-хендлер в существующее Echo/chi-приложение?

Echo: e.Any(path, echo.WrapHandler(handler)) — обёртка принимает http.Handler; chi: r.Mount(path, handler). Путь берите у генератора, как и для ServeMux. Два нюанса: регистрируйте middleware до монтирования, чтобы RPC попал под цепочку, и проверьте, что body-лимиты фреймворка не конфликтуют с WithReadMaxBytes — из двух лимитов сработает меньший.

### Нужен ли h2c, если жанры только Connect-протокол?

Нет: Connect-протокол работает поверх HTTP/1.1 и HTTP/2 одинаково, обёртка не обязательна. h2c нужен, когда в хендлере живут gRPC или gRPC-Web клиенты (они требуют HTTP/2), а TLS терминируется раньше сервера. Если сомневаетесь — поставьте h2c: обёртка прозрачна для HTTP/1.1 и ничего не ломает, зато жанры не отвалятся при добавлении старых клиентов.
