---
title: "Протоколы: gRPC, gRPC-Web, Connect"
description: "Протоколы Connect: один handler обслуживает gRPC, gRPC-Web и Connect. Диспетчеризация по Content-Type, фреймы, трейлеры, GET-кеш, grpc web vs connect."
course: "Connect: современный RPC от Buf"
url: "https://golangmaster.ru/courses/connect/lessons/04-protokoly/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 18
---

# Протоколы: gRPC, gRPC-Web, Connect

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

Три протокола — один и тот же сервер. Разбираем connect протоколы на уровне проводов: как handler выбирает протокол по Content-Type, из чего состоят пятибайтовые фреймы gRPC, куда gRPC-Web прячет трейлеры, почему собственный протокол Connect отвечает честными HTTP-статусами и умеет GET-кеш, и как выглядит выбор «grpc web vs connect» на практике.

Третий урок закончился обещанием: разобрать, как один и тот же handler обслуживает curl с JSON, классических gRPC-клиентов и браузер. Обещание в силе, и сегодня мы спускаемся на уровень HTTP-проводов — это сердце курса, потому что здесь лежит ответ на вопрос «что такое Connect». Возьмите проект из [урока об установке](/courses/connect/lessons/02-ustanovka/): сервер NoteService на localhost:8080 из [третьего урока](/courses/connect/lessons/03-pervyj-servis/) уже умеет всё, о чём пойдёт речь, — ничего дописывать не придётся, только смотреть в терминал и curl. План такой: сначала дисциплина диспетчеризации — по какому признаку handler различает connect протоколы; затем анатомия классического gRPC — пятибайтовые конверты, HTTP/2, трейлеры; дальше gRPC-Web — что именно он перепрятал и когда нужен Envoy; потом собственный протокол Connect — тело без конвертов, HTTP-статусы вместо трейлеров и GET-кеш для процедур без побочных эффектов; и в финале — таблица выбора «grpc web vs connect» и разбор того, как три протокола живут на одном сервере, не мешая друг другу. Это connect протоколы в одном уроке: прочитав его, вы сможете предсказать любой ответ сервера по одному заголовку запроса.

### Диспетчер по Content-Type: один handler, три обработчика

Главный факт урока: протокол выбирается не сборкой, не конфигом и не отдельным портом, а заголовком Content-Type каждого запроса. Сгенерированный handler, который вернул NewNoteServiceHandler, — это три обработчика под одним фасадом: собственный протокол Connect, классический gRPC и gRPC-Web. Каждый HTTP-запрос handler смотрит на Content-Type и передаёт его профильному обработчику: application/grpc — gRPC; application/grpc-web и его варианты — gRPC-Web; application/json и application/proto — собственный протокол Connect. Для стриминговых процедур те же роли играют application/connect+proto и application/connect+json. Диспетчеризация занимает микросекунды и не создаёт ни горутин, ни буферов заранее: это обычный switch по строке заголовка. Почему это возможно? Потому что все три протокола согласованы сверху: один и тот же путь /notes.v1.NoteService/GetNote, одни и те же message-типы, один и тот же словарь из шестнадцати кодов ошибок. Различаются они только упаковкой: как сериализуется сообщение, где едут метаданные и где объявляется итог вызова. Именно поэтому схему не нужно трогать при смене протокола — первым уроком мы это зафиксировали, теперь видите механику.

### Content-Type → протокол: таблица диспетчеризации

| Content-Type запроса | Протокол | Формат тела | Типовой клиент |
| --- | --- | --- | --- |
| application/grpc | классический gRPC | фреймы protobuf | grpc-go, grpcurl, чужие сервисы |
| application/grpc+proto | классический gRPC | фреймы protobuf | явное указание кодека |
| application/grpc-web | gRPC-Web | фреймы protobuf | старые браузерные клиенты, Envoy |
| application/grpc-web+proto | gRPC-Web | фреймы protobuf | браузерные клиенты grpc-web |
| application/grpc-web+json | gRPC-Web | фреймы JSON | редкий, но рабочий вариант |
| application/json | протокол Connect | JSON без конвертов | curl, браузеры, devtools |
| application/proto | протокол Connect | protobuf без конвертов | Connect-клиент, экономия трафика |
| application/connect+proto | протокол Connect (стримы) | фреймы protobuf | стриминговые вызовы |
| application/connect+json | протокол Connect (стримы) | фреймы JSON | стриминговые вызовы |

### Протокол Connect: curl-вызов в чистом виде

```go
$ curl -i \
    --header "Content-Type: application/json" \
    --data '{"id": "n1"}' \
    http://localhost:8080/notes.v1.NoteService/GetNote

HTTP/1.1 200 OK
Content-Type: application/json

{"id":"n1","title":"Первая заметка"}
```

Это тот же вызов, что мы делали в третьем уроке, но теперь вы знаете, что произошло внутри: заголовок application/json выбрал обработчик собственного протокола, тело запроса — просто сериализованный GetNoteRequest без конвертов, а итог вызова объявлен HTTP-статусом. Никаких фреймов, никаких трейлеров, никаких полей-невидимок: 200 — «вызов удался», 404 — not_found, 429 — resource_exhausted. Именно эта честность делает протокол Connect лучшим другом curl, devtools и нагрузочных прокси: каждое слово на проводе читается без спецификации.

### Анатомия gRPC: конверты из пяти байт

Теперь классика. gRPC определён поверх HTTP/2, и каждое сообщение в нём едет в конверте: один байт флага сжатия плюс четыре байта длины big-endian, и только затем байты сериализованного сообщения. Пять служебных байтов на каждое сообщение — это цена, за которую протокол получает независимость от кодека и поддержку сжатия на уровне сообщения. Метаданные в gRPC — не HTTP-заголовки, а отдельный механизм HTTP/2: request-заголовки уезжают в HEADERS-кадрах, а итог вызова — в трейлерах, специальных кадрах после тела. Успешный unary-вызов выглядит как HTTP 200 с телом из фреймов и трейлером grpc-status: 0; неудачный — как HTTP 200 с трейлерами grpc-status: 5 и grpc-message: not found. Да, вы не ослышались: в классическом gRPC ошибка едет с кодом 200. HTTP-статус в этом протоколе почти ничего не значит — единственный источник правды grpc-status в трейлерах. Отсюда и жёсткое требование транспорта: HTTP/2 обязателен, потому что трейлеров на HTTP/1.1 не бывает. В браузерном Fetch API трейлеры недоступны — помните тупик из [первого урока](/courses/connect/lessons/01-vvedenie/)? Вот его техническая причина: JavaScript физически не может прочитать grpc-status, каким бы сервер ни был.

### Протокол gRPC: тот же вызов через grpcurl

```go
$ grpcurl -plaintext \
    -import-path proto -proto notes/v1/notes.proto \
    -d '{"id": "n1"}' \
    localhost:8080 notes.v1.NoteService/GetNote

{
  "id": "n1",
  "title": "Первая заметка"
}
```

grpcurl — швейцарский нож gRPC-мира, и наш Connect-сервер отвечает ему без единой правки: grpcurl отправляет application/grpc, диспетчер отдаёт запрос gRPC-обработчику, тот разбирает пятибайтовые конверты. Флаг -plaintext нужен для локального cleartext-сервера, а -import-path и -proto — потому что reflection у нас не включён: без них grpcurl не знает схемы. Если включить пакет reflection из экосистемы Connect, схемы станут известны серверу, и вызов упростится до grpcurl -plaintext -d '...' localhost:8080 notes.v1.NoteService/GetNote. Заметьте в выводе: JSON здесь — косметика grpcurl, по проводу ехали protobuf-фреймы.

**Вопрос:** Запрос пришёл на наш handler с заголовком Content-Type: application/grpc-web+json. Что произойдёт?

- Handler вернёт 415 Unsupported Media Type: этот тип он не знает
- ✅ Запрос обслужит обработчик gRPC-Web: фреймы с JSON-кодеком, трейлеры в конце тела
- Запрос обслужит обработчик классического gRPC: суффикс +json игнорируется
- Handler перенаправит запрос в собственный протокол Connect: там тоже JSON

Суффикс +json указывает кодек, а префикс application/grpc-web — протокол: запрос заберёт gRPC-Web-обработчик, который разбирает конверты и ожидает трейлеры в конце тела. Префикс важнее суффикса: именно он выбирает обработчик. 415 вы увидите только на совсем незнакомый тип — например, application/grpc-web-text, который Connect-сервер не обслуживает, потому что base64-вариант gRPC-Web нужен лишь экзотическим браузерам без Fetch.

### gRPC-Web: трейлеры, спрятанные в тело

gRPC-Web — это gRPC, адаптированный под браузерные ограничения: он работал поверх HTTP/1.1 и не требовал доступа к трейлерам, которых в браузере нет. Как? Трейлеры переехали из HTTP/2-кадров в конец тела ответа: последним элементом идёт текстовый фрейм grpc-status: 0 (или кодом ошибки), и клиентская библиотека вычитывает его из тела. На практике gRPC-Web почти всегда едет с protobuf-кодеком — бинарным фреймованным потоком, поэтому «человекочитаемость» здесь заканчивается на заголовках. Историческая раскладка такая: классический gRPC-сервер не понимает gRPC-Web, поэтому перед ним ставили переводчик — Envoy с grpc-web-фильтром, который переупаковывал запросы и ответы; это и есть «gRPC-Web требует прокси», фраза из первого урока. Connect-сервер переводчик не нужен: gRPC-Web-обработчик встроен в handler, и запрос application/grpc-web+proto обслуживается тем же кодом, что и application/grpc. При этом gRPC-Web сохраняет главный рудимент родителя: итог вызова не в HTTP-статусе — ответ обычно 200, а grpc-status вычитывается из тела, и лог-системы на уровне HTTP видят «200 OK» даже на ошибку. Это не дефект реализации, а наследие протокола: HTTP/1.1-совместимость стоит того, чтобы жертвовать статусами.

**Вопрос:** Куда gRPC-Web кладёт итоговый код вызова — grpc-status?

- В HTTP-статус ответа: ошибка — это 4xx/5xx
- В трейлеры HTTP/2: кадры после тела
- ✅ В текстовый фрейм в конце тела ответа
- В заголовок Retry-After: код превращается в подсказку повтора

Фирменная черта gRPC-Web: трейлеры, которых нет в браузере, кодируются текстовым фреймом в конце тела. Библиотека вычитывает его и превращает в код ошибки; HTTP-статус при этом остаётся 200. В классическом gRPC тот же код живёт в настоящих HTTP/2-трейлерах, а в протоколе Connect — в самом HTTP-статусе. Три протокола — три места хранения одного и того же числа.

### Собственный протокол Connect: unary без конвертов

Третий обработчик обслуживает протокол, который Buf спроектировал с нуля, учтя ошибки предшественников — дальше будем звать его просто «протокол Connect». Unary-вызов на нём максимально плоский: POST по пути /package.Service/Method, тело — сериализованное сообщение без конвертов, Content-Type application/json или application/proto объявляет кодек. Итог вызова объявляется настоящим HTTP-статусом по таблице кодов из первого урока: not_found — 404, invalid_argument — 400, resource_exhausted — 429, unimplemented — 501, unavailable — 503. На ошибку тело — маленький JSON с полями code, message и опциональным details; на успех — обычный сериализованный ответ. Метаданные — обычные HTTP-заголовки в обе стороны, никаких HEADERS-кадров и текстовых фреймов-трейлеров. Стриминговые процедуры устроены чуть сложнее: там сообщения едут фреймами, потому что потоку нужна делимитация, и кодек объявляется конвертированным типом application/connect+proto или application/connect+json — но даже в стримах итог вызова остаётся в HTTP-статусе для серверного стриминга и в финальном фрейме-конце для двунаправленного. Дизайн-цель протокола сформулирована в его спецификации честно: RPC должен читаться человеком в devtools и жить на любом HTTP-транспорте, включая HTTP/1.1, без прокси-переводчиков.

### Протокол Connect: ошибка на проводе

```go
$ curl -i \
    --header "Content-Type: application/json" \
    --data '{"id": "missing"}' \
    http://localhost:8080/notes.v1.NoteService/GetNote

HTTP/1.1 404 Not Found
Content-Type: application/json

{"code":"not_found","message":"заметка не найдена"}
```

Та же схема из шестого урока, увиденная с проводной стороны: connect.NewError(CodeNotFound, ...) на сервере превратился в HTTP 404 и JSON с code и message. Сравните с классическим gRPC, где тот же код ехал бы трейлером grpc-status: 5 при HTTP 200, и с gRPC-Web, где он лежал бы в конце тела. Одно и то же событие «заметки нет» — три упаковки; код шестнадцати значений один и тот же, поэтому клиенты на любом стеке ветвятся одинаково.

### Процедура помечена NO_SIDE_EFFECTS, вызов ушёл GET-ом. Чем ответит сервер?

```go
// Схема notes.v1, метод GetNote:
rpc GetNote (GetNoteRequest) returns (Note) {
  option idempotency_level = NO_SIDE_EFFECTS;
}

// Клиент:
$ curl -i "http://localhost:8080/notes.v1.NoteService/GetNote?connect=v1&encoding=json&message=eyJpZCI6Im4xIn0%3D&base64=1"
```

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

- 405 Method Not Allowed: RPC бывает только POST
- ✅ 200 OK и JSON-заметку: протокол Connect обслуживает GET для процедур без побочных эффектов
- 301 Redirect на POST-вариант того же пути
- 400 Bad Request: query string — не место для message

Уникальная фича протокола Connect: если rpc помечен idempotency_level = NO_SIDE_EFFECTS, клиент отправляет unary-вызов GET-ом, а сообщение упаковывается в query string — параметр message с base64url-сериализацией, encoding=json объявляет кодек, connect=v1 — версию протокола. Сервер декодирует и вызывает обычный хендлер. Никакой магии безопасности здесь нет: NO_SIDE_EFFECTS — ваше обещание, что вызов ничего не меняет, и потому его можно кешировать.

### GET-кеш: idempotency_level и что это вам даёт

Раз вызов без побочных эффектов уезжает GET-ом по обычному URL, его начинает обслуживать вся HTTP-инфраструктура кеширования: браузерный HTTP-кеш, прокси корпоративной сети, CDN на границе, а также ретраи становятся безопасными — повторить GET можно без страха задвоить запись. В gRPC GET-вызовы существовали только как редко реализованная экзотика спецификации, в gRPC-Web их нет вовсе, у Connect это первоклассный путь. Правила гигиены: помечайте NO_SIDE_EFFECTS только действительно чистые процедуры — чтения, подсчёты, статусы; метод с пометкой обязан не иметь последствий, иначе кеш превратит первый вызов в единственный настоящий. Учтите и границу: GET-оптимизация — собственность протокола Connect; клиенты на gRPC и gRPC-Web будут ходить тем же методом POST, и это нормально — контракт от этого не ломается, пометка просто игнорируется теми обработчиками. И да, кешу нужна помощь с заголовками Cache-Control — их ставит ваш middleware или CDN-политика; протокол гарантирует форму запроса, а политику устаревания определяете вы.

### Бинарный gRPC-Web-запрос прилетел прямо на Connect-handler без Envoy. Что будет?

```go
// Наш Connect-сервер на localhost:8080. Envoy и других прокси НЕТ.
$ curl -i \
    --header "Content-Type: application/grpc-web+proto" \
    --data-binary @request.bin \
    http://localhost:8080/notes.v1.NoteService/GetNote
```

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

- 415 Unsupported Media Type: без Envoy gRPC-Web не обслуживается
- 502 Bad Gateway: handler передаст запрос выше по цепочке
- ✅ 200 OK в формате gRPC-Web: обработчик встроен, переводчик не нужен
- 301 Redirect на application/json: сервер исправит протокол за клиента

Ключевое отличие Connect-сервера от классического grpc-go: gRPC-Web-обработчик встроен в сгенерированный handler, поэтому браузерный клиент ходит напрямую, без Envoy и его grpc-web-фильтра. Классическому gRPC-серверу прокси нужен, потому что он не умеет читать трейлеры из тела — а Connect умеет, и отвечал бы в том же формате gRPC-Web. Правило: выбор «grpc web vs connect» для браузера — выбор между «работает везде, но с прокси» и «работает напрямую с Connect-сервером».

### grpc web vs connect: таблица выбора

Соберём всё в одну карту — она отвечает на вопрос, который вы будете задавать себе в каждом новом проекте. Протокол gRPC выбирайте, когда оба конца ваши и нативные: сервис-сервис внутри периметра, клиенты на Go/Kotlin/Java, HTTP/2 гарантирован от начала до конца. gRPC-Web — когда в цепочке есть браузер, но сервер остаётся классическим gRPC, и прокси-переводчик уже стоит или стоит того по другим причинам; это протокол совместимости, а не выбора. Собственный протокол Connect — дефолт для новых проектов: он обслуживается тем же handler-ом, читается curl-ом, живёт на HTTP/1.1, даёт браузеру то же самое без прокси и добавляет GET-кеш для чистых процедур. Спор «grpc web vs connect» на практике решается одним вопросом: есть ли у вас уже классический gRPC-бэкенд и Envoy? Если да — gRPC-Web достался бесплатно; если вы начинаете с чистого листа — берите протокол Connect и не оглядывайтесь, а gRPC оставьте как опцию совместимости, которая включена всегда и без настройки.

### Три протокола на одном сервере: сравнительная карта

| Свойство | gRPC | gRPC-Web | Connect |
| --- | --- | --- | --- |
| Транспорт | только HTTP/2 | HTTP/1.1 или HTTP/2 | HTTP/1.1 или HTTP/2 |
| Unary на проводе | фреймы 5 байт + тело | фреймы 5 байт + тело | чистое тело без конвертов |
| Итог вызова | трейлер grpc-status | текстовый фрейм в теле | HTTP-статус ответа |
| Неудачный вызов выглядит | HTTP 200 + grpc-status: 5 | HTTP 200 + фрейм в конце | HTTP 404 + JSON ошибки |
| Браузер напрямую | нет: трейлеров нет в JS | да, но трейлеры из тела | да, без оговорок |
| Envoy/прокси | не нужен | нужен перед gRPC-сервером | не нужен |
| curl-дружелюбность | почти нет: нужен grpcurl | нет: бинарные фреймы | да: читается целиком |
| GET-кеш | нет | нет | да: NO_SIDE_EFFECTS |
| Типовой сценарий | сервис-сервис, строгий ingress | браузер + готовый gRPC-бэкенд | новые API, curl, браузер |

### Совместимость: один контракт, любое число клиентов

Диспетчеризация по Content-Type даёт свойство, ради которого всё затевалось: контракт один, клиентов сколько угодно. Пометим это на примере notes.v1, который мы собираем с первого урока: хендлер GetNote возвращает одну и ту же заметку — но curl получает JSON со статусом 404 на промахе, grpc-клиент из курса по [gRPC-серверам](/courses/grpc-go/lessons/04-server/) — трейлер grpc-status: 5, а TypeScript-клиент девятого урока — объект ошибки с кодом not_found. Ни одна строка серверного кода не знает, кто на другом конце: обработчики протоколов переводят один и тот же *connect.Error в формат своего провода. Отсюда три практических вывода. Первый: версии схем эволюционируют независимо от протоколов — совместимость живёт в protobuf-поле за полем. Второй: метрики и логи на уровне HTTP стоит снимать с Content-Type — вы увидите, каким протоколом реально пользуются, и увидите, что gRPC-клиентов у вашего Connect-сервера может не оказаться вовсе. Третий: переезд клиента с gRPC на протокол Connect — это строка в его конфигурации, а не серверная эпопея: handler принял бы его завтра без деплоя.

**Вопрос:** Браузерное приложение на TypeScript ходит в ваш новый сервис на Connect. Какой протокол поедет по умолчанию и что для этого нужно?

- Классический gRPC: браузер поддерживает HTTP/2 из коробки
- gRPC-Web: придётся поднять Envoy с grpc-web-фильтром
- ✅ Протокол Connect: JSON по HTTP/1.1 или HTTP/2, Envoy не нужен — обработчик встроен
- Любой из трёх: браузеру безразличен Content-Type

Классическому gRPC в браузере мешают трейлеры и требование HTTP/2-кадров, недоступных JavaScript; gRPC-Web решает это ценой прокси перед классическим сервером. Клиентская библиотека Connect для TypeScript говорит собственным протоколом: обычный HTTP, обычные статусы, и ваш handler уже обслуживает его по умолчанию — никакой инфраструктуры добавлять не нужно.

### Один mux, много сервисов: маршрутизация не зависит от протокола

Последний кирпич — маршрутизация. Путь каждого вызова — это строка /package.Service/Method из схемы, и регистрация сервиса в ServeMux не зависит от того, каким протоколом к нему поедут: path, handler := notesv1connect.NewNoteServiceHandler(&notesServer{}); mux.Handle(path, handler). Хотите второй сервис — второй mux.Handle рядом, как мы делали с ElizaService в третьем уроке; пути не пересекаются, потому что включают полное имя сервиса. Важно понять уровень, на котором развилка: mux различает сервисы по пути, а handler внутри одного сервиса различает протоколы по Content-Type — два независимых измерения маршрутизации, и каждое решается своим механизмом. Поэтому «три обработчика на одном сервере» — не фигура речи: на одном порту живут все три протокола для каждого сервиса, и ни один из них не конфликтует с middleware, health-чеками и статикой, потому что всё это обычный net/http. Никакого отдельного порта «для gRPC», никакого отдельного listener-а: если вы видели такие конструкции в чистом grpc-go, здесь они не нужны — и это одна из самых недооценённых экономик курса.

### Соберите три вызова одного метода

Восстановите Content-Type каждого вызова: JSON для протокола Connect, grpc-web для браузерного варианта, connect+json для стримов.

```go
# 1. Протокол Connect, unary, человекочитаемо:
curl --header "Content-Type: application/___" \
     --data '{"id": "n1"}' \
     http://localhost:8080/notes.v1.NoteService/GetNote

# 2. gRPC-Web, protobuf-фреймы (браузерный вариант):
--header "Content-Type: application/___+proto"

# 3. Протокол Connect, стриминговая процедура:
--header "Content-Type: application/___"
```

Подсказка: Unary протокола Connect — application/json; gRPC-Web носит префикс application/grpc-web; стримы Connect едут конвертированным типом application/connect+json.

Правильные ответы в пропусках: `json`, `grpc-web`, `connect+json`

### Путь одного curl-запроса через диспетчер

Расставьте по порядку события между curl-командой и JSON-ответом на localhost:8080.

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

1. `curl отправляет POST на /notes.v1.NoteService/GetNote с Content-Type: application/json`
2. `ServeMux находит маршрут по пути и передаёт запрос сгенерированному handler-у`
3. `Handler смотрит на Content-Type и выбирает обработчик протокола`
4. `application/json — запрос уходит обработчику собственного протокола Connect`
5. `Тело декодируется кодеком JSON прямо в GetNoteRequest, без конвертов`
6. `Выполняется реализация notesServer.GetNote и возвращает заметку`
7. `Обработчик сериализует Note в JSON и ставит HTTP 200`
8. `curl печатает статус 200 и тело {"id":"n1","title":"Первая заметка"}`

> **Отладка: сначала curl, потом клиент.** Когда вызов падает, сопротивляйтесь искушению дебажить его клиентской библиотекой — спускайтесь на уровень провода. Вызовите метод curl-ом в протоколе Connect: вы сразу увидите HTTP-статус, Content-Type и JSON ошибки — три факта, которых достаточно в девяти случаях из десяти. Если curl работает, а клиент нет, проблема в клиентской части: протокол, транспорт, метаданные. Браузерная песочница сайта сетевых вызовов не исполняет, поэтому эта привычка отрабатывается только на вашем локальном сервере из второго урока — и это лучший повод его поднять.

### Карточки: три протокола

- **Диспетчер Content-Type** — Handler выбирает протокол по заголовку: application/grpc — gRPC, application/grpc-web — gRPC-Web, application/json и application/proto — протокол Connect. Для стримов — application/connect+json и connect+proto.
- **Пятибайтовый конверт gRPC** — Каждое сообщение в gRPC и gRPC-Web едет в конверте: 1 байт флага сжатия + 4 байта длины big-endian, затем сериализованное сообщение. Протокол Connect unary конвертов не использует.
- **grpc-status: где живёт итог** — gRPC — трейлер HTTP/2 (ответ с HTTP 200 даже при ошибке); gRPC-Web — текстовый фрейм в конце тела; Connect — сам HTTP-статус: not_found → 404, resource_exhausted → 429.
- **Почему gRPC-Web требует Envoy** — Классический gRPC-сервер не умеет читать трейлеры из тела — перед ним ставят Envoy с grpc-web-фильтром. Connect-сервер обслуживает gRPC-Web нативно: тот же handler, прокси не нужен.
- **idempotency_level = NO_SIDE_EFFECTS** — Опция rpc в схеме: протокол Connect отправляет такой unary-вызов GET-ом с сообщением в query string. Открывает дорогу HTTP-кешу, CDN и безопасным ретраям.
- **grpc web vs connect** — gRPC-Web — протокол совместимости для браузера перед готовым gRPC-бэкендом (Envoy обязателен). Протокол Connect — дефолт для новых проектов: HTTP/1.1, curl, статусы, GET-кеш, без прокси.

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

Теперь вы знаете connect протоколы не понаслышке: диспетчер по Content-Type, конверты gRPC, трейлеры-в-теле gRPC-Web, честные статусы и GET-кеш протокола Connect — и понимаете, почему один handler покрывает всё это без единой настройки. Следующий урок — [клиенты](/courses/connect/lessons/05-klienty/): как из Go-кода выбрать протокол осознанно — опции connect.WithGRPC() и connect.WithGRPCWeb(), требования к HTTP/2-транспорту, метаданные и таймауты. Дальше — ошибки в полную силу (шестой урок) и перехватчики (седьмой), которые видят протокол вызова через req.Spec(). Программа всех двадцати пяти уроков — на [странице курса](/courses/connect/). Домашняя работа урока: прогнать три вызова GetNote из этого урока — curl-ом, grpcurl-ом и браузерным fetch с application/grpc-web+proto — и записать в заметки, чем отличаются ответы на уровне статусов; это войдёт в ваш тестовый арсенал проекта.

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

- Протокол выбирается по Content-Type каждого запроса: application/grpc — gRPC, application/grpc-web — gRPC-Web, application/json или application/proto — протокол Connect.
- Сгенерированный handler — три обработчика под одним фасадом: диспетчеризация стоит микросекунды и не требует ни конфигов, ни отдельных портов.
- gRPC: сообщения в пятибайтовых конвертах (флаг сжатия + длина), итог вызова в трейлере grpc-status, HTTP 200 даже при ошибке, HTTP/2 обязателен.
- gRPC-Web: трейлеры переехали в конец тела, работает на HTTP/1.1, но перед классическим gRPC-сервером нужен Envoy; Connect обслуживает его нативно.
- Протокол Connect: unary — тело без конвертов, ошибки — HTTP-статус плюс JSON {code, message, details}, метаданные — обычные заголовки.
- NO_SIDE_EFFECTS превращает unary-вызов в GET с сообщением в query string — HTTP-кеш, CDN и безопасные ретраи становятся доступными.
- Маршрутизация двухуровневая: ServeMux различает сервисы по пути /package.Service/Method, handler различает протоколы по Content-Type.

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

Поднимите сервер из третьего урока и вызовите GetNote тремя способами из этого урока: curl-ом с Content-Type application/json (ожидайте 200 и JSON-заметку), grpcurl-ом с -import-path и -proto (ожидайте тот же ответ в его форматировании) и повторите промах с несуществующим id обоими способами (404 с code: not_found против grpc-status: 5). Затем добавьте в notes.proto методу GetNote опцию option idempotency_level = NO_SIDE_EFFECTS;, перегенерируйте buf generate и повторите вызов GET-ом с параметрами connect=v1 и encoding=json — убедитесь, что ответ тот же. Запишите в заметки, где именно каждый протокол объявил итог вызова: статус, трейлер или фрейм в теле — эта таблица станет вашей шпаргалкой по отладке.

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

### Как Connect различает протоколы gRPC, gRPC-Web и собственный?

По заголовку Content-Type каждого запроса: application/grpc уходит обработчику классического gRPC, application/grpc-web — обработчику gRPC-Web, application/json и application/proto — собственному протоколу Connect. Все три обработчика встроены в один сгенерированный handler и обслуживаются без настройки.

### Нужен ли Envoy для gRPC-Web перед Connect-сервером?

Нет. Обработчик gRPC-Web встроен в Connect-handler, поэтому браузерные клиенты ходят напрямую. Envoy нужен только перед классическим gRPC-сервером, который не умеет читать трейлеры, закодированные в тело ответа.

### Что выбрать: grpc web vs connect?

Если сервер уже классический gRPC и Envoy стоит — gRPC-Web достаётся бесплатно. Для новых проектов берите собственный протокол Connect: он работает на HTTP/1.1, читается curl-ом, отдаёт честные HTTP-статусы, поддерживает GET-кеш для NO_SIDE_EFFECTS и обслуживается тем же handler-ом, что и gRPC.
