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

HTTP: протокол, версии, коды, cookies, REST и gRPC

HTTP — это прикладной протокол запрос-ответ без состояния (stateless), который исторически работает поверх надёжного транспорта: TCP для HTTP/1.0, 1.1 и 2, и QUIC поверх UDP для HTTP/3. Ключевая мысль, которую надо держать в голове: семантика HTTP (методы, коды статуса, заголовки, тело) одна и та же во всех версиях, а меняется только способ сериализации сообщений на проводе. HTTP/1.x — текстовая построчная сериализация с разделителями CRLF; HTTP/2 — бинарные фреймы и мультиплексирование потоков в одном TCP-соединении плюс сжатие заголовков HPACK; HTTP/3 — та же модель потоков, но поверх QUIC, где потоки независимы на транспортном уровне. Именно поэтому RFC 9110 («HTTP Semantics») отделён от RFC 9112/9113/9114, описывающих конкретные версии.

Второй опорный тезис — «без состояния» означает, что сервер не обязан помнить ничего между запросами: каждый запрос самодостаточен. Всё, что выглядит как состояние (сессия, авторизация, корзина), навешивается сверху — через cookies, заголовок Authorization, токены. Отсюда же растут REST-ограничения и удобство горизонтального масштабирования: любой запрос можно отдать любому инстансу за балансировщиком.

Третий тезис — HTTPS не является отдельным протоколом. Это тот же HTTP, но сообщения идут внутри TLS-туннеля: сначала TCP-рукопожатие, затем TLS-хендшейк с проверкой сертификата сервера и согласованием симметричного ключа, и только потом HTTP-запрос. TLS даёт три свойства: конфиденциальность (шифрование), целостность (AEAD/MAC) и аутентификацию сервера (цепочка сертификатов до доверенного корня). Порт по умолчанию 443 против 80, схема https:// против http://, а в HTTP/2 в браузерах TLS фактически обязателен (согласование версии идёт через ALPN).

Четвёртый тезис, который на собеседовании ценят выше знания номеров кодов, — любой сетевой вызов должен быть ограничен по времени. В Go http.Client{} по умолчанию не имеет общего таймаута вообще, а http.Server{} — таймаутов на чтение и запись; забыть их означает, что при зависшем пире горутина и соединение живут неограниченно долго, пул соединений и файловые дескрипторы утекают, и деградация одного downstream-сервиса каскадом валит ваш сервис. Правильная дисциплина — context.WithTimeout на каждый исходящий запрос плюс явные ReadHeaderTimeout/ReadTimeout/WriteTimeout/IdleTimeout на сервере.

Коротко. Это не вопрос, а ссылка на агрегатор вопросов с собеседований, попавшая в выгрузку. Обрывок исходника, вопрос не восстанавливается.

Глубже. По смыслу такие ссылки в конспектах интервьюеров означают «пройдись по типовому списку вопросов Go-разработчика». Практическая польза от них есть: easyoffer собирает частотность вопросов по реальным собеседованиям, и её стоит использовать как чек-лист покрытия тем, а не как источник ответов.

Коротко. Ссылка на репозиторий-шпаргалку, не вопрос. Обрывок исходника, вопрос не восстанавливается.

Коротко. Ссылка на статью с Хабра, не вопрос. Обрывок исходника, вопрос не восстанавливается.

Коротко. Ссылка на статью с Хабра, не вопрос. Обрывок исходника, вопрос не восстанавливается.

Коротко. Ссылка на подборку вопросов, не вопрос. Обрывок исходника, вопрос не восстанавливается.

Коротко. Ссылка на подборку вопросов, не вопрос. Обрывок исходника, вопрос не восстанавливается.

Техничка: Дали вот этот код https://shorturl.at/KaVdM . Присылают за 1-2 дня, есть время проглядеть. На созвоне шаришь экран и комментируешь замечания, может какой-то код добавляешь.

Заголовок раздела «Техничка: Дали вот этот код https://shorturl.at/KaVdM . Присылают за 1-2 дня, есть время проглядеть. На созвоне шаришь экран и комментируешь замечания, может какой-то код добавляешь.»

Коротко. Это описание формата секции — code review вслух по присланному заранее коду, а не вопрос. Готовиться к нему надо как к разбору чужого HTTP-сервиса: заранее составить чек-лист и идти по нему сверху вниз, комментируя вслух.

Глубже. Рабочий каркас чек-листа для типичного Go-HTTP-сервиса: (1) контекст — пробрасывается ли r.Context() в БД и в исходящие вызовы, есть ли таймауты на клиенте и сервере; (2) ресурсы — закрывается ли resp.Body, дочитывается ли он до конца перед закрытием (иначе соединение не вернётся в пул), закрываются ли rows, file; (3) ошибки — не проглатываются ли, оборачиваются ли через %w, не утекает ли внутренняя информация в тело ответа; (4) конкурентность — гонки на общих мапах, sync.Mutex vs sync.RWMutex, утечки горутин без завершения; (5) HTTP-специфика — коды статуса адекватны семантике, Content-Type выставляется до WriteHeader, тело не пишется после WriteHeader, ограничен ли размер тела через http.MaxBytesReader; (6) валидация входа и SQL-инъекции; (7) тесты и httptest. Отдельно ценится, когда кандидат отделяет «это баг» от «это вкусовщина» и предлагает конкретный патч, а не абстрактную критику.

Коротко. HttpOnly — флаг в заголовке Set-Cookie, который запрещает доступ к cookie из JavaScript (document.cookie). Браузер по-прежнему отправляет такую cookie в HTTP-запросах, но скрипт на странице её не прочитает — это защита сессионного токена от кражи при XSS.

Глубже. HttpOnly закрывает только вектор «скрипт прочитал и отправил токен наружу»; он не защищает от CSRF (браузер всё равно приложит cookie к запросу) — от CSRF работает SameSite=Lax/Strict и/или CSRF-токен, и не защищает от перехвата в сети — для этого нужен флаг Secure (отправлять только по HTTPS). Практический набор для сессионной cookie: HttpOnly; Secure; SameSite=Lax; Path=/, а срок жизни — через Max-Age. В Go это поля структуры http.Cookie:

http.SetCookie(w, &http.Cookie{
Name: "session",
Value: token,
Path: "/",
MaxAge: int((24 * time.Hour).Seconds()),
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteLaxMode,
})

Какой HTTP-код следует использовать для перенаправления пользователя с короткой ссылки на длинную?

Заголовок раздела «Какой HTTP-код следует использовать для перенаправления пользователя с короткой ссылки на длинную?»

Коротко. Для сокращателя ссылок обычно берут 302 Found или 307 Temporary Redirect — временный редирект, чтобы браузер не кэшировал соответствие навсегда и каждый переход доходил до сервиса (это нужно для аналитики и для возможности переназначить ссылку). 301 Moved Permanently уместен только если ссылка гарантированно неизменна и нагрузку хочется снять кэшем браузера.

Глубже. Разница внутри пар важна: 301/302 исторически позволяют клиенту сменить метод на GET при редиректе, а 308/307 обязаны сохранить исходный метод и тело. Для сокращателя это почти не имеет значения (запросы GET), поэтому чаще всего пишут 302. Главный практический аргумент против 301 — браузеры кэшируют его агрессивно и надолго, и после смены цели ссылки пользователи продолжают уходить на старый адрес. В Go достаточно http.Redirect(w, r, longURL, http.StatusFound) — он сам выставит Location и напишет короткое тело.

Где в HTTP-ответе указывается адрес для перенаправления?

Заголовок раздела «Где в HTTP-ответе указывается адрес для перенаправления?»

Коротко. В заголовке ответа Location. Клиент, получив код 3xx, читает Location и повторяет запрос по этому адресу.

Глубже. Location может содержать как абсолютный URI, так и относительную ссылку — клиент разрешает её относительно исходного запроса (RFC 9110). Заголовок используется не только в редиректах: при 201 Created в Location принято класть URI созданного ресурса. В Go http.Redirect выставляет Location и пишет минимальное HTML-тело для GET; на клиенте http.Client по умолчанию сам следует редиректам (до 10 переходов), а отключить это можно, задав CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }.

Коротко. Это заголовок секции из плана интервью, а не вопрос. Обрывок исходника, вопрос не восстанавливается.

Глубже. Если такая секция реально идёт первой на собеседовании, интервьюер хочет услышать связную историю: почему вы смотрите на эту компанию/продукт, что именно в текущей работе перестало давать рост, какие задачи хотите решать дальше. Каркас ответа: контекст (где сейчас и чем занимаетесь) → чего не хватает → почему именно этот продукт/стек закрывает нехватку → что вы уже сделали в эту сторону. Ошибка — отвечать «хочу больше денег и интересных задач» без конкретики.

Коротко. Из стартовой строки (метод, target/путь, версия протокола), набора заголовков «имя: значение», пустой строки-разделителя и необязательного тела. Формально: request line + headers + CRLF + body.

Глубже. В HTTP/1.1 это выглядит буквально так, байт в байт:

POST /api/v1/users?debug=1 HTTP/1.1\r\n
Host: example.com\r\n
Content-Type: application/json\r\n
Content-Length: 27\r\n
\r\n
{"name":"Ann","age":30}

Разделитель строк — CRLF (\r\n), пустая строка отделяет заголовки от тела. Длина тела задаётся либо Content-Length, либо Transfer-Encoding: chunked (тогда тело идёт кусками, и длина заранее неизвестна). В HTTP/2 и HTTP/3 «стартовой строки» как таковой нет: метод, схема, :authority и путь передаются как псевдозаголовки :method, :scheme, :authority, :path в HEADERS-фрейме, а тело — в DATA-фреймах; семантика при этом та же. В Go всё это разложено по полям http.Request: Method, URL, Proto, Header, Body, а Host вынесен в отдельное поле, а не лежит в Header.

Коротко. HTTPS — это тот же HTTP, но передаваемый внутри TLS-соединения. Отличия: шифрование и целостность данных, аутентификация сервера по сертификату, порт 443 вместо 80, схема https://, и дополнительная стоимость на TLS-хендшейк при установке соединения.

Глубже. Важно проговаривать, что именно защищено, а что нет. Внутри TLS скрыты: метод, путь, заголовки (включая cookies и Authorization), тело. Снаружи видны: IP-адреса и порты, размер и тайминг пакетов, DNS-запрос имени (если не используется DoH/DoT) и имя хоста в SNI на этапе ClientHello — до появления Encrypted Client Hello его видит любой наблюдатель на пути. Современный TLS 1.3 сократил хендшейк до одного round trip (1-RTT), с 0-RTT для возобновляемых сессий, так что «HTTPS медленный» — сегодня в основном миф; накладные расходы на симметричное шифрование с AES-NI пренебрежимо малы.

Коротко. Чтобы вызов гарантированно завершался: без таймаута зависший или медленно отвечающий сервер удерживает горутину, соединение и файловый дескриптор бесконечно, и отказ одного downstream-сервиса каскадом исчерпывает ресурсы вашего. Таймаут превращает «висим навсегда» в предсказуемую ошибку, которую можно обработать — ретраем, фолбэком или деградацией.

Глубже. В Go это особенно важно, потому что http.Client{} по умолчанию имеет Timeout == 0, то есть без ограничений; ограничен только DialContext таймаутом ОС на установку TCP. Плохая практика — полагаться на «сервер сам ответит». Минимально корректный клиент:

client := &http.Client{
Timeout: 5 * time.Second, // на всё: dial + TLS + запрос + чтение тела
Transport: &http.Transport{
DialContext: (&net.Dialer{Timeout: 2 * time.Second, KeepAlive: 30 * time.Second}).DialContext,
TLSHandshakeTimeout: 2 * time.Second,
ResponseHeaderTimeout: 3 * time.Second,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 90 * time.Second,
},
}
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
resp, err := client.Do(req)

Отдельно помните: client.Timeout покрывает и чтение тела, поэтому для стриминговых ответов его выставлять нельзя — там нужен контекст с отменой и ResponseHeaderTimeout.

Зачем нужны таймауты в http-запросах, и как их подобрать?

Заголовок раздела «Зачем нужны таймауты в http-запросах, и как их подобрать?»

Коротко. См. выше про назначение таймаутов. Подбирают их от целевого SLA сверху вниз: берут бюджет ответа своего эндпоинта, вычитают собственную обработку и делят остаток между зависимостями, ориентируясь на p99 их латентности с запасом примерно ×1.5–2, а не на среднее.

Глубже. Практическая методика: (1) измерить распределение латентности зависимости (p50/p95/p99) по метрикам, а не по ощущениям; (2) поставить таймаут чуть выше p99 — так вы отрезаете хвост, но не рубите нормальные запросы; (3) убедиться, что сумма таймаутов вниз по цепочке меньше таймаута вызывающего, иначе клиент отвалится раньше и вся работа будет выброшена; (4) использовать сквозной дедлайн: пробрасывать context.WithTimeout от входящего запроса, тогда каждый следующий вызов автоматически получает остаток бюджета. Таймаут почти всегда идёт в паре с ретраями (только для идемпотентных операций, с экспоненциальной задержкой и jitter) и с circuit breaker, иначе ретраи по таймауту сами устраивают шторм на уже перегруженный сервис.

Коротко. Код статуса — машиночитаемый итог обработки запроса: он говорит клиенту, кэшу и прокси, что делать дальше, без разбора тела. Пять классов: 1xx — информационные, 2xx — успех, 3xx — перенаправление, 4xx — ошибка клиента, 5xx — ошибка сервера.

Глубже. Смысл классов важнее конкретных номеров: по первой цифре клиент принимает решения, даже если код ему незнаком (RFC 9110 прямо это предписывает). Разграничение 4xx/5xx определяет, кто виноват и надо ли ретраить: 4xx — запрос повторять бессмысленно, пока клиент его не изменит (исключения — 408, 425, 429), 5xx — проблема на стороне сервера, ретрай с backoff уместен. Ходовой набор: 200 OK, 201 Created, 202 Accepted, 204 No Content, 206 Partial Content; 301/302/304/307/308; 400/401/403/404/405/409/410/415/422/429; 500/501/502/503/504. Отдельно стоит знать 401 («не аутентифицирован», обязателен заголовок WWW-Authenticate) против 403 («аутентифицирован, но не разрешено»), 429 Too Many Requests с Retry-After и 503 Service Unavailable для временной недоступности.

Коротко. HTTPS — это HTTP поверх TLS. Он нужен для трёх вещей: скрыть содержимое трафика от посторонних, гарантировать, что данные не подменили по пути, и убедиться, что вы разговариваете именно с тем сервером, чьё имя в адресной строке.

Глубже. Без TLS любой узел на пути (Wi-Fi-точка, провайдер, прозрачный прокси) может прочитать cookie сессии и подменить содержимое ответа — классическая атака «человек посередине» и инъекция скриптов в HTML. Помимо этого HTTPS сегодня — техническое требование: HTTP/2 и HTTP/3 в браузерах работают только по TLS, а часть Web API (Service Workers, геолокация, WebCrypto, HTTP/2 Server Push) доступны только в «secure context». Для принуждения к HTTPS используется заголовок Strict-Transport-Security (HSTS), который запрещает браузеру ходить на этот домен по обычному HTTP.

Коротко. Резолв имени в IP через DNS → установка TCP-соединения (three-way handshake) → при HTTPS ещё TLS-хендшейк → отправка запроса (строка, заголовки, тело) → обработка на сервере → получение ответа (статус, заголовки, тело) → закрытие или удержание соединения в keep-alive для последующих запросов.

Глубже. Детали, которые ждут услышать на уровне middle+: DNS может отдать несколько адресов и клиент выберет один (в Go — net.Resolver, с Happy Eyeballs для IPv4/IPv6); TCP-хендшейк — это один RTT, TLS 1.3 — ещё один (TLS 1.2 — два); ALPN во время TLS-хендшейка согласует версию HTTP (h2 или http/1.1); при keep-alive следующие запросы к тому же хосту переиспользуют соединение и пропускают первые три шага — в Go это делает пул внутри http.Transport. Важная деталь Go: чтобы соединение вернулось в пул, тело ответа надо дочитать до конца и закрыть, иначе Transport порвёт соединение и следующий запрос заплатит за новый хендшейк.

Коротко. См. выше про отличия HTTP и HTTPS: это один и тот же прикладной протокол, но HTTPS оборачивает его в TLS, работает по порту 443 и даёт шифрование, целостность и аутентификацию сервера.

Глубже. Единственное дополнение, которое стоит проговорить в такой краткой формулировке: HTTPS ничего не говорит о безопасности самого приложения. Валидный сертификат подтверждает только владение доменом (для DV-сертификатов), а не добросовестность владельца, и не защищает от XSS, SQL-инъекций или утечки данных из логов.

Зачем нужен таймаут в HTTP запросе и как его подобрать?

Заголовок раздела «Зачем нужен таймаут в HTTP запросе и как его подобрать?»

Коротко. См. выше — таймаут ограничивает время удержания ресурсов и превращает зависание в обрабатываемую ошибку; подбирается от бюджета SLA вниз, ориентируясь на p99 зависимости.

Глубже. Дополнение про серверную сторону, которую в двух предыдущих вопросах часто забывают. У http.Server в Go таймауты по умолчанию тоже нулевые, и это прямой вектор Slowloris-атаки: клиент открывает соединения и шлёт заголовки по байту, удерживая горутины. Разумный минимум:

srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 15 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 60 * time.Second,
MaxHeaderBytes: 1 << 20,
}

Для эндпоинтов со стримингом или загрузкой больших файлов WriteTimeout/ReadTimeout глобально ставить нельзя — либо выносите их на отдельный сервер, либо управляйте дедлайном через http.ResponseController (Go 1.20+): http.NewResponseController(w).SetWriteDeadline(time.Time{}).

Коротко. 200 OK — стандартный код успешной обработки запроса, при котором в ответе есть тело с результатом. Это часть чек-листа «какие коды должен возвращать сервис», а не вопрос как таковой.

Глубже. Тонкости, за которые цепляются на собеседовании: 200 уместен для GET, PUT, PATCH, POST с телом ответа; если тела нет — корректнее 204 No Content; если запрос создал ресурс — 201 Created с Location; если работа принята в асинхронную обработку — 202 Accepted. Частая ошибка проектирования — отдавать 200 OK с телом {"error": "..."}: это ломает клиентов, кэши и мониторинг, потому что по коду ошибка невидима.

400 Bad Request - Некорректный запрос. Например, отсутствует необходимое поле или запрос имеет не валидный JSON формат.

Заголовок раздела «400 Bad Request - Некорректный запрос. Например, отсутствует необходимое поле или запрос имеет не валидный JSON формат.»

Коротко. 400 Bad Request — сервер не смог обработать запрос из-за ошибки клиента в его форме: битый JSON, отсутствующее обязательное поле, некорректный тип значения. Повторять такой запрос без изменений бессмысленно.

Глубже. Стоит разделять «синтаксически невалидно» и «синтаксически валидно, но не проходит бизнес-валидацию»: для второго часть команд использует 422 Unprocessable Content, для неподдерживаемого Content-Type415 Unsupported Media Type, для конфликта состояния — 409 Conflict. Единого правила нет, но внутри одного API схема должна быть последовательной и задокументированной. В теле полезно отдавать машиночитаемую структуру ошибки — например, по RFC 9457 (application/problem+json) с полями type, title, status, detail, — и никогда не класть туда стектрейсы и SQL-запросы.

403 Forbidden - Доступ запрещен. Попытка обратиться к ресурсу, принадлежащему другому пользователю.

Заголовок раздела «403 Forbidden - Доступ запрещен. Попытка обратиться к ресурсу, принадлежащему другому пользователю.»

Коротко. 403 Forbidden — клиент опознан, но прав на эту операцию у него нет; повторная аутентификация не поможет. Противопоставляется 401 Unauthorized, который означает «личность не установлена или креды невалидны».

Глубже. В сценарии «обращение к чужому ресурсу» (классический IDOR) есть выбор между 403 и 404: 404 не раскрывает сам факт существования объекта и потому предпочтителен там, где идентификаторы перебираемы, а утечка «такой заказ существует» уже является утечкой. 401 обязан сопровождаться заголовком WWW-Authenticate с указанием схемы (Bearer, Basic). И запомните историческую путаницу в названиях: 401 называется «Unauthorized», хотя по смыслу это «Unauthenticated».

Коротко. 404 Not Found — сервер не нашёл ресурс по указанному URI и не сообщает, существовал ли он когда-либо. Для «существовал и удалён навсегда» есть более точный 410 Gone.

Глубже. Практические нюансы: 404 кэшируется по умолчанию как «эвристически кэшируемый», так что для динамических API его стоит явно помечать Cache-Control: no-store, если ресурс может появиться. Не путайте «маршрут не зарегистрирован» и «сущность не найдена в БД» — оба дают 404, но в теле полезно различать их для отладки. Если URI существует, а метод не поддерживается — это 405 Method Not Allowed с обязательным заголовком Allow; в Go начиная с 1.22 http.ServeMux умеет маршрутизацию по методу (mux.HandleFunc("GET /items/{id}", h)) и сам отдаёт 405 с Allow.

Сервис всегда возвращает данные в формате JSON, кроме случаев, когда явно необходим другой формат данных.

Заголовок раздела «Сервис всегда возвращает данные в формате JSON, кроме случаев, когда явно необходим другой формат данных.»

Коротко. Это пункт из тестового задания, а не вопрос: требование отдавать Content-Type: application/json для всех ответов, включая ошибки, и отступать от него только там, где нужен другой формат (например, отдача файла как application/octet-stream или text/csv).

Глубже. Как это делают в Go правильно: заголовок выставляется до первой записи в тело и до WriteHeader, иначе Go применит определение типа по содержимому (http.DetectContentType) и заголовок уже не изменить.

func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
if err := json.NewEncoder(w).Encode(v); err != nil {
// тело уже частично отправлено — только логируем
log.Printf("write json: %v", err)
}
}

Тонкость: json.NewEncoder(w).Encode пишет прямо в сокет, поэтому при ошибке маршалинга статус уже отправлен и исправить его нельзя; если структура сложная и есть риск ошибки — маршалите в буфер (json.Marshal), проверяйте ошибку, ставьте Content-Length и только потом пишите. Согласование форматов по запросу клиента делается через заголовок Accept (content negotiation) с ответом 406 Not Acceptable, если ни один вариант не поддерживается.

Реализуйте методы API для получения списка всех закаченных файлов;

Заголовок раздела «Реализуйте методы API для получения списка всех закаченных файлов;»

Коротко. GET /files возвращает 200 OK и JSON-массив метаданных (id, имя, размер, mime, время загрузки), с пагинацией через query-параметры limit/offset или курсор. Тело в запросе отсутствует, операция идемпотентна и кэшируема.

Глубже. Минимальная реализация на стандартной библиотеке Go 1.22+, где ServeMux уже умеет метод в шаблоне:

type FileMeta struct {
ID string `json:"id"`
Name string `json:"name"`
Size int64 `json:"size"`
MIME string `json:"mime"`
CreatedAt time.Time `json:"created_at"`
}
func (s *Server) listFiles(w http.ResponseWriter, r *http.Request) {
limit := 50
if v := r.URL.Query().Get("limit"); v != "" {
n, err := strconv.Atoi(v)
if err != nil || n <= 0 || n > 1000 {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid limit"})
return
}
limit = n
}
items, err := s.store.List(r.Context(), limit)
if err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal"})
return
}
writeJSON(w, http.StatusOK, items) // пустой список -> [], а не null
}
// mux.HandleFunc("GET /files", s.listFiles)

Что оценивают: пагинация есть (иначе список однажды не влезет в память), пустой результат сериализуется как [], а не null (в Go nil-слайс маршалится в null — инициализируйте items := []FileMeta{}), контекст запроса пробрасывается в хранилище, ошибки хранилища не утекают наружу текстом.

Коротко. DELETE /files/{id}: успешное удаление — 204 No Content (или 200 OK с телом), несуществующий id — 404 Not Found. Метод должен быть идемпотентным: повторный DELETE того же id не считается ошибкой сервера.

Глубже. Спорный момент, который любят обсуждать: что возвращать на повторное удаление. По RFC метод идемпотентен по эффекту, а не по коду ответа, поэтому оба варианта («всегда 204» и «404 на второй раз») допустимы — важно выбрать и задокументировать.

func (s *Server) deleteFile(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id") // Go 1.22+
if id == "" {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "empty id"})
return
}
switch err := s.store.Delete(r.Context(), id); {
case err == nil:
w.WriteHeader(http.StatusNoContent)
case errors.Is(err, ErrNotFound):
writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
default:
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal"})
}
}
// mux.HandleFunc("DELETE /files/{id}", s.deleteFile)

Отдельно: id из пути нельзя подставлять в путь на диске напрямую — это path traversal (../../etc/passwd). Храните файлы под сгенерированными именами (UUID) и валидируйте id регуляркой; при удалении сначала удаляйте запись из БД, потом файл, и делайте операцию устойчивой к повтору.

Реализуйте работу сервера по протоколу HTTPS.

Заголовок раздела «Реализуйте работу сервера по протоколу HTTPS.»

Коротко. В Go достаточно srv.ListenAndServeTLS(certFile, keyFile) — сертификат и приватный ключ в PEM. В проде обычно TLS терминируется на балансировщике/ingress, а сертификаты выпускаются автоматически (ACME/Let’s Encrypt), но уметь поднять TLS на самом сервисе надо.

Глубже. Полный пример с явной конфигурацией TLS и редиректом с HTTP:

srv := &http.Server{
Addr: ":8443",
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
TLSConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
},
}
log.Fatal(srv.ListenAndServeTLS("server.crt", "server.key"))

Самоподписанный сертификат для локальной разработки: openssl req -x509 -newkey rsa:2048 -nodes -keyout server.key -out server.crt -days 365 -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost" — обязательно с SAN, потому что современные клиенты (и Go начиная с 1.15) игнорируют CN и требуют subjectAltName. Для автоматического получения сертификатов есть golang.org/x/crypto/acme/autocert. Взаимную аутентификацию (mTLS) включают через ClientAuth: tls.RequireAndVerifyClientCert и ClientCAs. Не забудьте HandleFunc на 80-м порту с http.Redirect(..., http.StatusMovedPermanently) и заголовок HSTS.

Коротко. См. выше про HTTP и HTTPS: HTTP — текстовый/бинарный протокол запрос-ответ прикладного уровня, HTTPS — он же, но внутри TLS-туннеля, с шифрованием, проверкой целостности и аутентификацией сервера по сертификату.

Глубже. Дополнение, которое отличает уверенный ответ: в браузере переход на https:// меняет не только транспорт, но и модель безопасности — появляется secure context, начинают действовать Secure-cookies, работает HTTP/2 и HTTP/3, а смешанный контент (http://-ресурсы на https://-странице) блокируется.

Вопрос про HTTP протоколы. Что знаешь о протоколе HTTP?

Заголовок раздела «Вопрос про HTTP протоколы. Что знаешь о протоколе HTTP?»

Коротко. HTTP — прикладной протокол запрос-ответ, stateless, текстовый в версии 1.x и бинарный начиная с 2.0, работает поверх TCP (HTTP/3 — поверх QUIC/UDP). Определяет методы, коды статуса, заголовки и правила кэширования; всё состояние навешивается сверху через cookies и токены.

Глубже. Структурированный ответ на такой открытый вопрос стоит строить по осям: (1) семантика — методы, их идемпотентность и безопасность, коды статуса, заголовки, согласование контента; (2) версии — 0.9/1.0/1.1/2/3 и что в каждой изменилось на уровне провода; (3) соединения — keep-alive, пайплайнинг (мёртв), мультиплексирование, head-of-line blocking; (4) кэширование — Cache-Control, ETag, Last-Modified, условные запросы и 304; (5) безопасность — TLS, HSTS, CORS, cookies-флаги. Если пройти по этим пяти пунктам за две минуты, дальше интервьюер сам выберет, куда углубляться.

Реализовать фильтрацию продуктов по цене и заголовку наиболее быстрым способом

Заголовок раздела «Реализовать фильтрацию продуктов по цене и заголовку наиболее быстрым способом»

Коротко. Быстрее всего — не фильтровать в приложении, а отдать работу хранилищу: диапазон по цене закрывается B-tree индексом, поиск по заголовку — полнотекстовым индексом (GIN + tsvector в PostgreSQL) или триграммным (pg_trgm) для подстроки. В API это GET /products?price_min=&price_max=&q= с пагинацией по курсору.

Глубже. Если по условию задачи данные уже в памяти сервиса, «наиболее быстрый способ» — предподготовленные структуры вместо линейного прохода на каждый запрос: отсортированный по цене слайс с sort.Search (двоичный поиск границ диапазона за O(log n)) и инвертированный индекс map[string][]int по нормализованным токенам заголовка, затем пересечение двух множеств от меньшего к большему.

// products отсортированы по Price
lo := sort.Search(len(products), func(i int) bool { return products[i].Price >= min })
hi := sort.Search(len(products), func(i int) bool { return products[i].Price > max })
candidates := products[lo:hi] // O(log n) на поиск границ

Что ждут услышать: сложность до и после (O(n) на запрос против O(log n + k)), где хранится состояние и как оно обновляется при записи, что делать с пагинацией (offset деградирует на больших смещениях — используйте keyset/seek-пагинацию по (price, id)), и что «наиболее быстро» без профилирования — гипотеза; на 10 000 записей линейный проход по слайсу структур может обогнать любые индексы за счёт локальности кэша.

Пользователь отправил HTTP-запрос и ждет ответа. Сервис выполняет долгую операцию. Почему не стоит позволять запросу выполняться неограниченно долго?

Заголовок раздела «Пользователь отправил HTTP-запрос и ждет ответа. Сервис выполняет долгую операцию. Почему не стоит позволять запросу выполняться неограниченно долго?»

Коротко. Потому что ресурсы под этот запрос удерживаются всё время его жизни: горутина, TCP-соединение, дескриптор, соединение к БД из пула. Клиент при этом почти наверняка уже отвалился по своему таймауту или закрыл вкладку, и работа делается впустую, а под нагрузкой такие «висяки» накапливаются и исчерпывают пулы — сервис перестаёт обслуживать даже быстрые запросы.

Глубже. Ключевая деталь для Go: когда клиент разрывает соединение, r.Context() отменяется, и правильно написанный обработчик это увидит — но только если он реально передаёт контекст вниз (в db.QueryContext, в http.NewRequestWithContext) и проверяет ctx.Err() в длинных циклах. Если контекст игнорируется, работа продолжается «в никуда». Архитектурно долгие операции не должны жить внутри синхронного HTTP-запроса вообще: правильный паттерн — принять задачу, вернуть 202 Accepted с идентификатором и Location на ресурс статуса, выполнять в фоне (воркер, очередь), а клиент опрашивает статус или получает вебхук. Плюс к этому — ограничение конкурентности (семафор/пул воркеров), чтобы всплеск запросов не породил неограниченное число фоновых задач.

Коротко. Обычным http.Client теоретически можно — gRPC это HTTP/2 с телом в специфичном формате кадров (1 байт флага сжатия + 4 байта длины + protobuf), и Go-шный http.Client умеет HTTP/2 и потоковые тела через io.Pipe. Но на практике так не делают: придётся вручную реализовывать фрейминг, trailers с grpc-status, дедлайны, сжатие и обработку ошибок — проще взять google.golang.org/grpc.

Глубже. Что действительно нужно от клиента: поддержка HTTP/2 с полным дуплексом (запрос ещё пишется, а ответ уже читается), поддержка HTTP-trailers (gRPC кладёт grpc-status/grpc-message именно в трейлеры после тела) и Content-Type: application/grpc+proto. Go-шный http.Transport с ForceAttemptHTTP2 дуплекс поддерживает, трейлеры читает через resp.Trailer после исчерпания тела — так что технически всё есть. Ограничения появляются в браузере: fetch/XHR не дают контроля над HTTP/2-фреймами и трейлерами, поэтому из браузера используют gRPC-Web через прокси (Envoy, grpcwebproxy), а он не поддерживает клиентский и двунаправленный стриминг — только unary и server-streaming.

Коротко. Да. Стандартный путь — grpc-gateway: в .proto через опции google.api.http описывается маппинг RPC-методов на HTTP-методы и пути, кодогенератор строит reverse-proxy, который принимает JSON поверх REST и транслирует в gRPC. Альтернативы — Connect (connectrpc.com/connect), который сам умеет отдавать и gRPC, и HTTP/JSON из одного сервера, и Envoy с транскодингом gRPC-JSON.

Глубже. Пример аннотации в proto:

service Users {
rpc GetUser(GetUserRequest) returns (User) {
option (google.api.http) = { get: "/v1/users/{id}" };
}
}

Из того же файла генерируется и OpenAPI-спека (protoc-gen-openapiv2), так что контракт остаётся единственным источником правды. Подводные камни: маппинг protobuf↔JSON регулируется protojson (поля в lowerCamelCase, enum строками, int64 строкой), поля со значением по умолчанию по умолчанию опускаются в JSON — это часто ломает клиентов, ожидающих 0/false; отсутствие в proto3 различия «поле не задано» и «поле равно нулю» решают через optional (наличие presence) или wrapper-типы. И REST, получившийся автоматически, обычно «RPC в URL-обёртке», а не идиоматичный REST — если нужен настоящий resource-oriented API, маппинг придётся проектировать руками.

Как работает https и как работает сертификат?

Заголовок раздела «Как работает https и как работает сертификат?»

Коротко. Клиент устанавливает TCP-соединение, затем TLS-хендшейк: посылает ClientHello (поддерживаемые шифры, SNI с именем хоста, ALPN), сервер отвечает своим сертификатом и участвует в обмене ключами (в TLS 1.3 — ECDHE), клиент проверяет цепочку сертификатов до доверенного корня и совпадение имени, стороны выводят общий симметричный ключ, и дальше весь HTTP идёт зашифрованным AEAD-шифром.

Глубже. Сертификат X.509 — это структура с публичным ключом сервера, именами (subjectAltName), сроком действия и подписью удостоверяющего центра. Проверка на клиенте включает: (1) построение цепочки от листового сертификата через промежуточные до корня из системного/встроенного хранилища доверия; (2) проверку каждой подписи публичным ключом вышестоящего; (3) срок действия; (4) соответствие имени хоста SAN-записям; (5) статус отзыва (CRL/OCSP, в браузерах — CRLite/OCSP stapling); (6) назначение ключа (extendedKeyUsage: serverAuth). Обладание приватным ключом сервер доказывает подписью в сообщении CertificateVerify (TLS 1.3) — иначе сертификат можно было бы просто скопировать. Важно: сам сертификат не шифрует данные, он только аутентифицирует сторону и даёт ключ для проверки подписи; шифрование трафика идёт симметричным ключом, выведенным из ECDHE-обмена, и обладает forward secrecy — компрометация приватного ключа сервера не расшифровывает записанный ранее трафик. В Go проверка цепочки — crypto/x509, (*x509.Certificate).Verify; отключение проверки через InsecureSkipVerify: true допустимо только в тестах.

Коротко. HTTP/2 (RFC 9113) — бинарная версия того же HTTP: сообщения нарезаются на фреймы, много запросов мультиплексируются в одном TCP-соединении параллельно, заголовки сжимаются алгоритмом HPACK, есть приоритеты потоков и (в исходной спецификации) server push. Семантика методов и кодов не изменилась.

Глубже. Что решалось: в HTTP/1.1 на одно соединение можно вести только один запрос за раз (пайплайнинг практически не работал из-за head-of-line blocking на уровне ответов), поэтому браузеры открывали по 6 соединений на хост, а разработчики городили спрайты, конкатенацию и доменное шардирование. HTTP/2 убирает всё это: одно соединение, N потоков, каждый со своим ID, фреймы HEADERS/DATA/SETTINGS/WINDOW_UPDATE/RST_STREAM/GOAWAY. HPACK — статическая таблица частых заголовков плюс динамическая таблица уже переданных, что резко сокращает повторяющиеся Cookie/User-Agent. Оставшаяся проблема — HOL blocking на уровне TCP: потерянный сегмент задерживает доставку всех потоков сразу, потому что TCP гарантирует порядок для всего соединения; это и стало мотивацией HTTP/3. Server push оказался неудачным и удалён из Chrome; в Go http.Pusher формально есть, но использовать его не стоит. Практика в Go: net/http поддерживает HTTP/2 «из коробки» для TLS-серверов и клиентов; для h2c (без TLS) нужен golang.org/x/net/http2/h2c. В Go 1.24 появилась настройка Server.HTTP2/Transport.HTTP2 и защита от атаки Rapid Reset через ограничение числа одновременных потоков.

Коротко. HTTP/3 (RFC 9114) — это HTTP/2-подобная модель потоков поверх QUIC, а QUIC работает поверх UDP. Главное отличие: потоки независимы на транспортном уровне, поэтому потеря пакета в одном потоке не тормозит остальные — head-of-line blocking уровня TCP исчезает. Плюс объединённый криптографический и транспортный хендшейк (1-RTT, а с 0-RTT — вообще без задержки при повторном подключении) и миграция соединения по Connection ID при смене IP (переход Wi-Fi → LTE не рвёт сессию).

Глубже. TLS 1.3 встроен в QUIC, незашифрованного HTTP/3 не существует. Сжатие заголовков — QPACK, вариант HPACK, устойчивый к внеочередной доставке. Обнаружение поддержки идёт через заголовок Alt-Svc: h3=":443" или через HTTPS-запись в DNS: клиент сначала подключается по HTTP/2 и запоминает, что можно перейти на h3. Минусы, которые стоит назвать: UDP местами блокируется корпоративными файрволами (нужен fallback на TCP), обработка идёт в user space и потому дороже по CPU, чем разгруженный в ядро TCP, а инструменты отладки менее зрелые. В Go в стандартной библиотеке HTTP/3 нет; на практике используют github.com/quic-go/quic-go и http3-обвязку к нему.

Коротко. Сравнение некорректно по уровням: gRPC — это RPC-фреймворк, который использует HTTP/2 как транспорт. Отличается он от «обычного HTTP+JSON API» тем, что контракт описан в .proto и код генерируется, данные бинарные (protobuf), поддерживаются четыре вида вызовов (unary, server-stream, client-stream, bidi), и есть встроенные дедлайны, метаданные, статус-коды и интерсепторы.

Глубже. Практические следствия: protobuf компактнее и быстрее в парсинге, чем JSON, а строгая схема ловит расхождения контрактов на этапе компиляции; за это платят человекочитаемостью (curl не поможет, нужен grpcurl) и необходимостью инфраструктуры для генерации и распространения proto-файлов. gRPC не работает напрямую из браузера — нужен gRPC-Web и прокси. Балансировка тоже отличается: одно долгоживущее HTTP/2-соединение мультиплексирует все вызовы, поэтому L4-балансировщик распределяет соединения, а не запросы, и нагрузка перекашивается — нужен клиентский балансинг или L7-прокси (Envoy, Linkerd). Собственные коды статуса gRPC (OK, INVALID_ARGUMENT, NOT_FOUND, DEADLINE_EXCEEDED, UNAVAILABLE) едут в HTTP-трейлере, а HTTP-статус при этом почти всегда 200.

Что сложнее реализовать? Общение по gRPC или HTTP

Заголовок раздела «Что сложнее реализовать? Общение по gRPC или HTTP»

Коротко. Простейший HTTP+JSON поднимается быстрее: в Go это net/http без единой зависимости, отладка через curl. gRPC требует toolchain (protoc/buf, плагины, кодогенерация, хранение и версионирование proto), но зато после настройки клиент и сервер получаются типобезопасными и рутинного кода меньше — на длинной дистанции и большом числе сервисов gRPC часто дешевле.

Глубже. Где сложность реально возникает: (1) распространение контрактов — отдельный репозиторий proto, buf-реестр, версионирование, правила обратной совместимости (нельзя менять номера полей и переиспользовать удалённые — их надо резервировать через reserved); (2) инфраструктура — балансировка долгоживущих HTTP/2-соединений, health checking, keepalive-настройки, поддержка в ingress; (3) наблюдаемость — бинарное тело не видно в логах, нужны интерсепторы и grpcurl с reflection; (4) браузерные клиенты — только через gRPC-Web/прокси. С HTTP+JSON сложность приходит с другой стороны: ручная сериализация, рассинхрон документации и кода, отсутствие типобезопасности между сервисами, «каждый API немного свой». Честный ответ на собеседовании — «стартовать проще на HTTP, поддерживать десятки внутренних сервисов проще на gRPC».

Коротко. WebSocket (апгрейд с HTTP через Upgrade: websocket), gRPC и gRPC-Web, SSE (Server-Sent Events, text/event-stream), WebDAV и CalDAV/CardDAV, SOAP и XML-RPC/JSON-RPC, OData, а также прикладные поверх HTTP: OAuth 2.0/OIDC, ACME (Let’s Encrypt), S3 API, GraphQL over HTTP, DNS-over-HTTPS.

Глубже. Стоит различать три случая. Первый — протоколы, которые используют HTTP как транспорт сообщений, сохраняя модель запрос-ответ (JSON-RPC, SOAP, GraphQL, S3): это по сути соглашение о формате тела и URL. Второй — те, что расширяют семантику самого HTTP новыми методами и заголовками (WebDAV: PROPFIND, MKCOL, LOCK). Третий — те, что используют HTTP только для установления соединения и затем переходят на собственный протокол поверх того же TCP: WebSocket (101 Switching Protocols) и CONNECT-туннели прокси. SSE стоит особняком — это чистый HTTP-ответ с бесконечным телом в формате data: ...\n\n, односторонний сервер→клиент, зато переживает прокси и автоматически переподключается; в Go он делается через http.Flusher (Go 1.20+: http.NewResponseController(w).Flush()).

Backend (REST, Java/Kotlin или Go) - обработка входящих запросов и бизнес логика

Заголовок раздела «Backend (REST, Java/Kotlin или Go) - обработка входящих запросов и бизнес логика»

Коротко. Это фрагмент описания вакансии или архитектуры системы, а не вопрос. Обрывок исходника, вопрос не восстанавливается.

Глубже. Если это преамбула к обсуждению архитектуры, ожидаемый разговор — про слои: транспортный слой (HTTP-хендлеры: парсинг, валидация, коды ответа), слой бизнес-логики (чистые сервисы без знания о HTTP), слой доступа к данным (репозитории). Ключевая мысль, которую ценят: http.Request и http.ResponseWriter не должны просачиваться в бизнес-логику — иначе её нельзя переиспользовать из gRPC-хендлера, консьюмера очереди или CLI и невозможно нормально тестировать.

Коротко. Ссылка на материалы по подготовке, не вопрос. Обрывок исходника, вопрос не восстанавливается.

Коротко. См. выше — прикладной протокол передачи гипертекста, построенный по модели «запрос-ответ», без состояния между запросами, поверх TCP (или QUIC для HTTP/3), с фиксированным набором методов, кодов статуса и заголовков.

Глубже. Единственное, что стоит добавить к предыдущим формулировкам: слово «гипертекст» в названии давно не отражает применение — HTTP сегодня универсальный транспорт для API, файлов, стриминга видео (HLS/DASH), телеметрии и даже DNS. И «протокол без состояния» относится к самому протоколу, а не к приложению: сервер вполне может иметь состояние, просто HTTP не обязывает его помнить предыдущие запросы клиента.

Коротко. Структура одинаковая — стартовая строка, заголовки, пустая строка, тело, — но первая строка разная: в запросе это МЕТОД target HTTP/версия, в ответе HTTP/версия КОД причина (status line). Часть заголовков специфична для направления: Host, Accept, Authorization — у запроса; Server, Set-Cookie, Location, WWW-Authenticate — у ответа.

Глубже. В Go это отражено двумя типами: http.Request (поля Method, URL, Host, Body) и http.Response (StatusCode, Status, Body). Полезная деталь: http.Request используется и на сервере, и на клиенте, но семантика полей отличается — например, на сервере URL содержит только путь и query, а Host лежит в отдельном поле; на клиенте URL должен быть абсолютным. Тело у запроса опционально (GET обычно без тела), у ответа отсутствует для 204, 304 и для ответов на HEAD. Общие для обоих направлений заголовки — сущностные (Content-Type, Content-Length, Content-Encoding) и связи (Connection, Transfer-Encoding).

Что такое cookies и как они используются в HTTP?

Заголовок раздела «Что такое cookies и как они используются в HTTP?»

Коротко. Cookie — небольшая пара «имя=значение», которую сервер отдаёт браузеру заголовком Set-Cookie, а браузер затем автоматически прикладывает к каждому подходящему запросу в заголовке Cookie. Это механизм добавления состояния (сессии, авторизации, настроек) поверх stateless-протокола.

Глубже. Атрибуты определяют область применения и безопасность: Domain и Path — куда отправлять; Expires/Max-Age — срок жизни (без них cookie сессионная и живёт до закрытия браузера); Secure — только по HTTPS; HttpOnly — недоступна из JS; SameSite=Strict|Lax|None — отправлять ли при кросс-сайтовых переходах (в современных браузерах по умолчанию Lax, а None требует Secure). Ограничения браузеров: около 4 КБ на cookie и порядка 50 штук на домен, поэтому в cookie кладут идентификатор сессии, а данные хранят на сервере. Два подхода: непрозрачный session id + серверное хранилище (легко отзывать, нужен Redis/БД) либо подписанный/зашифрованный токен внутри cookie (stateless, но отзыв сложен). В Go: r.Cookie("name"), r.Cookies(), http.SetCookie(w, c); на клиенте автоматическое хранение включается через http.Client{Jar: jar} с net/http/cookiejar.

Коротко. На стороне клиента: браузер держит их в собственном хранилище (обычно SQLite-файл в профиле пользователя) и подставляет в заголовок Cookie при каждом запросе к подходящему домену и пути. Сервер cookie не хранит — он хранит связанные с ними данные сессии, если использует серверные сессии.

Глубже. Изоляция хранилища идёт по домену (с учётом Domain, Public Suffix List и, в современных браузерах, по partition key для сторонних cookie), а не по вкладке. В HTTP-клиентах вне браузера cookie не сохраняются автоматически: Go-шный http.Client без Jar просто игнорирует Set-Cookie, и это регулярный источник багов в интеграционных тестах авторизации. Сессионные cookie (без Expires/Max-Age) живут в памяти процесса браузера, персистентные — на диске. Для чувствительных значений важно помнить: cookie-файл читается любым процессом с правами пользователя, поэтому долгоживущие токены в cookie — компромисс, а не абсолютная защита.

Что такое cookies в контексте семантики HTTP-запроса?

Заголовок раздела «Что такое cookies в контексте семантики HTTP-запроса?»

Коротко. См. выше про cookies. В контексте семантики запроса cookie — это обычный заголовок Cookie, то есть часть метаданных запроса; протокол не придаёт ему особого смысла, состояние в него вкладывает приложение.

Глубже. Семантическая тонкость, ради которой обычно и задают вопрос: cookie превращает формально идентичные запросы в разные по результату, и это ломает наивное кэширование — поэтому ответы, зависящие от cookie, обязаны нести Cache-Control: private (или no-store) и Vary, иначе общий кэш (CDN, прокси) отдаст одному пользователю персональный ответ другого. Вторая тонкость: cookie отправляется браузером автоматически, включая кросс-сайтовые запросы, — отсюда CSRF, и отсюда же SameSite. Третья: cookie не участвует в определении идемпотентности или безопасности метода — GET с cookie остаётся безопасным методом с точки зрения HTTP, даже если приложение внутри что-то меняет (что само по себе анти-паттерн).

Коротко. REST — архитектурный стиль поверх HTTP с ресурсами, URI, стандартными методами и обычно JSON; gRPC — контрактный RPC на protobuf поверх HTTP/2 с кодогенерацией и стримингом. REST берут для публичных API, браузерных клиентов и там, где важны читаемость и совместимость с кэшами и прокси; gRPC — для внутреннего service-to-service общения, где важны производительность, строгий контракт и потоковые вызовы.

Глубже. Более точные критерии выбора. За REST: клиент — браузер или сторонний разработчик; нужно HTTP-кэширование и работа через любые прокси; нужна отладка curl’ом; API меняется публично и требует человекочитаемой документации. За gRPC: десятки внутренних сервисов, где рассинхрон контрактов дорог; высокий RPS и чувствительность к латентности и объёму трафика; нужны двунаправленные потоки; полиглотная среда, где генерация клиентов на нескольких языках экономит много ручной работы. Гибрид тоже нормален: gRPC внутри, REST-фасад наружу через grpc-gateway или Connect. Что не является аргументом: «gRPC быстрее» без цифр — на мелких сообщениях разница с JSON часто теряется на фоне сети и БД, а бенчмарк надо делать на своих данных.

Коротко. Поверх TCP для версий 0.9, 1.0, 1.1 и 2 (по умолчанию порт 80, для HTTPS — TLS поверх TCP на порту 443). HTTP/3 работает поверх QUIC, а QUIC — поверх UDP.

Глубже. Требование к транспорту в спецификации сформулировано как «надёжный поток с сохранением порядка» — TCP ему удовлетворяет. QUIC даёт те же гарантии на уровне отдельного потока, но реализует их сам в user space поверх UDP, добавляя мультиплексирование независимых потоков и встроенный TLS 1.3. Полезно уметь назвать модель OSI/TCP-IP: HTTP — прикладной уровень (L7), TLS — между прикладным и транспортным (условно L6/представления), TCP/UDP — транспортный (L4), IP — сетевой (L3). Исторический курьёз: HTTP не привязан к TCP жёстко — существуют реализации поверх Unix-сокетов (Docker API) и in-memory-транспортов (в Go — httptest.Server и кастомный RoundTripper).

Коротко. HTTP/1.0 — по одному запросу на TCP-соединение, соединение закрывается после ответа, заголовок Host необязателен. HTTP/1.1 — постоянные соединения (keep-alive по умолчанию), обязательный Host (виртуальный хостинг), Transfer-Encoding: chunked, кэширование через Cache-Control/ETag, диапазонные запросы, пайплайнинг (на практике не прижился). HTTP/2 — бинарный формат, мультиплексирование потоков в одном соединении, сжатие заголовков HPACK, приоритеты.

Глубже. Проблема, оставшаяся в HTTP/1.1, — head-of-line blocking на уровне ответов: соединение переиспользуется, но ответы должны идти строго в порядке запросов, поэтому один медленный ответ блокирует остальные. Пайплайнинг пытался слать несколько запросов не дожидаясь ответов, но из-за багов промежуточных прокси браузеры его отключили. Обходным путём стало открытие 6 параллельных соединений на хост и хаки уровня фронтенда (спрайты, инлайнинг, доменное шардирование) — все они в HTTP/2 становятся вредными и их надо убирать. HTTP/2 снимает HOL на уровне HTTP, но не на уровне TCP. Ещё различия: в HTTP/2 имена заголовков обязаны быть в нижнем регистре, а вместо стартовой строки используются псевдозаголовки :method, :path, :scheme, :authority; в Go net/http абстрагирует это, и один и тот же http.Handler работает поверх любой версии.

Коротко. В HTTP/1.1 обязателен ровно один заголовок запроса — Host (без него сервер обязан вернуть 400). Всё остальное обязательно условно: если есть тело, нужен Content-Length либо Transfer-Encoding: chunked; в HTTP/2/3 роль Host играет псевдозаголовок :authority.

Глубже. «Условно обязательные» стоит перечислить: Content-Type — обязателен по смыслу, если тело есть и его формат неочевиден (без него сервер угадывает или отвечает 415); WWW-Authenticate — обязателен в ответе 401; Allow — в ответе 405; Location — в ответах 3xx и в 201 Created; Retry-After — рекомендован при 429 и 503; Date — сервер обязан его отдавать, если у него есть часы. У ответа формально обязательных заголовков нет вообще: HTTP/1.1 204 No Content\r\n\r\n — валидный ответ. В Go net/http сам добавляет Date, Content-Length (или chunked) и Content-Type по автоопределению, если вы его не задали.

Коротко. GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, TRACE, CONNECT. Ключевые свойства — безопасность (не изменяет состояние: GET, HEAD, OPTIONS, TRACE) и идемпотентность (повтор даёт тот же эффект: все безопасные плюс PUT и DELETE); неидемпотентны POST и PATCH.

Глубже. Свойства методов — не формальность, а практика: только безопасные методы разрешено автоматически ретраить и предзагружать, и только идемпотентные ретраит транспорт после сетевой ошибки (Go-шный http.Transport повторяет запрос, если соединение из пула оказалось разорвано, — но только когда тело можно перечитать и метод идемпотентен). Кэшируемы по умолчанию ответы на GET и HEAD; POST кэшируем только при явных заголовках. TRACE в проде обычно отключают (атака Cross-Site Tracing), CONNECT используется прокси для туннелирования TLS. Для неидемпотентных операций, которые нужно безопасно повторять, вводят ключ идемпотентности — заголовок вроде Idempotency-Key с серверным дедупом, — это стандартная практика в платёжных API.

Коротко. PUT заменяет ресурс целиком телом запроса и идемпотентен; PATCH применяет частичное изменение и в общем случае неидемпотентен. Практически: PUT /users/1 с неполным телом должен обнулить отсутствующие поля, PATCH /users/1 меняет только переданные.

Глубже. Формат тела PATCH не задан протоколом — надо выбирать: JSON Merge Patch (RFC 7386, application/merge-patch+json, где null означает удаление поля) или JSON Patch (RFC 6902, application/json-patch+json, список операций add/remove/replace). Merge Patch не умеет различать «не передано» и «удалить» иначе как через null и не работает с элементами массивов; JSON Patch выразительнее, но многословнее. Идемпотентность PATCH зависит от операции: replace идемпотентен, а {"op":"add","path":"/tags/-","value":"x"} — нет. Для конкурентных изменений применяют условные запросы: клиент присылает If-Match: "etag", сервер отвечает 412 Precondition Failed, если ресурс изменился, — это оптимистическая блокировка средствами HTTP. Ещё одно отличие: PUT может создавать ресурс по указанному клиентом URI (201 Created), PATCH несуществующий ресурс обычно не создаёт (404).

Коротко. HEAD — то же, что GET, но сервер возвращает только статус и заголовки без тела; используется для проверки существования ресурса, его размера (Content-Length) и свежести (ETag, Last-Modified) без скачивания. OPTIONS сообщает, какие возможности доступны для ресурса: в ответ идёт заголовок Allow со списком методов, а в браузерном мире OPTIONS — это CORS preflight.

Глубже. По HEAD: заголовки должны быть ровно те же, что вернул бы GET (включая Content-Length), поэтому сервер обязан «как бы» сформировать ответ. В Go при обработке HEAD net/http сам отбрасывает записанное в тело, а http.ServeContent/http.ServeFile обрабатывают его корректно. По OPTIONS: preflight-запрос браузер шлёт автоматически перед «непростым» кросс-доменным запросом (нестандартный метод, кастомные заголовки, Content-Type: application/json), с заголовками Origin, Access-Control-Request-Method, Access-Control-Request-Headers; сервер обязан ответить 204/200 с Access-Control-Allow-Origin, -Methods, -Headers и, желательно, Access-Control-Max-Age для кэширования preflight. Особая форма OPTIONS * относится ко всему серверу, а не к конкретному ресурсу.

С какими ограничениями HTTP приходилось сталкиваться?

Заголовок раздела «С какими ограничениями HTTP приходилось сталкиваться?»

Коротко. Это вопрос про личный опыт: ждут конкретных историй, а не теории. Каркас ответа — назвать ограничение, объяснить, как оно проявилось в вашей системе, и что вы с ним сделали.

Глубже. Готовый набор ограничений, из которых можно собрать честный ответ. Лимиты на размер: заголовки ограничены сервером (в Go MaxHeaderBytes, по умолчанию 1 МБ; в nginx large_client_header_buffers), длина URL на практике ограничена прокси (порядка 8 КБ) — отсюда переход с GET с гигантским query на POST с телом. Отсутствие пуша от сервера: HTTP запрос-ответ, поэтому для реалтайма нужны SSE, WebSocket или long polling. HOL blocking в HTTP/1.1 и лимит в 6 соединений на хост в браузере. Stateless: приходится тащить сессию в cookie/токен, что усложняет отзыв прав. Отсутствие встроенных дедлайнов и ретраев — всё руками, и без сквозного контекста бюджеты не соблюдаются. Промежуточные прокси, которые буферизуют ответ и ломают стриминг (в nginx лечится proxy_buffering off и X-Accel-Buffering: no) или обрывают idle-соединения раньше, чем клиентский пул (классическая гонка с http: server closed idle connection в Go — лечится IdleConnTimeout меньше, чем у балансировщика). Плюс лимит cookie в 4 КБ и сложности с CORS.

Коротко. REST — архитектурный стиль (Рой Филдинг, 2000), а REST API — интерфейс, построенный по его ограничениям: клиент-сервер, отсутствие состояния сессии на сервере, кэшируемость, единообразный интерфейс (ресурсы адресуются URI, манипулируются через стандартные методы, представления самоописываемы), слоистость и опционально code-on-demand.

Глубже. Единообразие интерфейса — то, что чаще всего понимают неверно. Оно означает: ресурс — существительное во множественном числе (/orders/42/items), действие выражается методом, а не глаголом в URL (POST /orders вместо /createOrder), результат — стандартный код статуса, представление помечено Content-Type. Шестое ограничение — HATEOAS (ответ содержит ссылки на доступные переходы) — в реальных API почти не встречается, и по модели зрелости Ричардсона большинство «REST API» остаются на уровне 2. Про stateless стоит сказать точно: сервер не хранит состояние клиентской сессии между запросами, а состояние ресурсов, разумеется, хранит. Плюсы стиля: кэшируемость на любом уровне (браузер, CDN, прокси), простое горизонтальное масштабирование, предсказуемость для сторонних разработчиков. Минусы: over-/under-fetching (лечится частично fields-параметрами или GraphQL), N+1 запросов на связанные ресурсы, слабая типизация контракта (лечится OpenAPI).

Коротко. См. выше про HTTP-протокол: прикладной stateless-протокол «запрос-ответ» поверх TCP/QUIC, определяющий методы, коды статуса, заголовки и тело.

Глубже. Если вопрос задают повторно или в конце блока, обычно хотят услышать не определение, а картину целиком в двух предложениях: «HTTP задаёт семантику — что мы просим и что получили; версии протокола задают только то, как эти сообщения кодируются на проводе; всё, что выглядит как состояние, приложение добавляет само поверх cookies и токенов».

Каким образом HTTP запрос доходит от клиента до сервера? Описать весь флоу.

Заголовок раздела «Каким образом HTTP запрос доходит от клиента до сервера? Описать весь флоу.»

Коротко. Разбор URL → поиск в кэшах DNS (браузер, ОС, резолвер) и, при промахе, рекурсивный запрос к DNS до получения IP → ARP/маршрутизация до нужного узла → TCP-хендшейк с сервером (или с балансировщиком) → TLS-хендшейк с проверкой сертификата и ALPN → отправка HTTP-запроса → на стороне сервера: балансировщик/reverse proxy → приложение → БД → формирование ответа → ответ проходит обратно по тому же соединению → клиент читает статус, заголовки, тело.

Глубже. Детали, которые превращают ответ в «сеньорский». На сетевом уровне: пакет уходит через default gateway, MAC-адрес шлюза узнаётся по ARP, дальше маршрутизация по BGP, по пути возможен NAT (порт источника переписывается) и MTU/фрагментация. На уровне балансировки: L4-балансировщик просто проксирует TCP, L7 (nginx, Envoy) терминирует TLS и разбирает HTTP, добавляя X-Forwarded-For/Forwarded — в Go это важно, потому что r.RemoteAddr покажет адрес прокси, а не клиента. На стороне Go-сервера: net.Listener.Accept отдаёт соединение, http.Server запускает горутину на соединение, читает и парсит запрос, находит хендлер в ServeMux, вызывает цепочку middleware. Ответ пишется в bufio.Writer поверх сокета, WriteHeader фиксирует статус, после чего заголовки менять поздно. При keep-alive соединение возвращается в пул, и следующий запрос стартует сразу с шага отправки.

В чем отличие 80 и 443 портов, принимающих клиентские запросы?

Заголовок раздела «В чем отличие 80 и 443 портов, принимающих клиентские запросы?»

Коротко. 80 — порт HTTP по умолчанию (открытый текст), 443 — HTTPS (HTTP внутри TLS). Технически это просто номера портов из соглашения IANA; принципиальная разница в том, что на 443 перед HTTP выполняется TLS-хендшейк.

Глубже. Практические следствия. На 80-м обычно оставляют только редирект на HTTPS (301 на https:// + HSTS) и обработку ACME-challenge для выпуска сертификатов. На 443 в TLS-хендшейке через SNI виден запрашиваемый хост, что позволяет одному IP обслуживать много доменов с разными сертификатами, а через ALPN согласуется h2 или http/1.1. Ничто не мешает запустить HTTPS на 8443, а HTTP на 8080 — тогда порт придётся указывать в URL явно. В Linux порты ниже 1024 привилегированные: процессу нужен root или capability CAP_NET_BIND_SERVICE, поэтому Go-сервисы в контейнерах слушают 8080/8443, а маппинг на 80/443 делает оркестратор.

Коротко. См. выше про HTTP/2: бинарный фрейминг вместо текста, мультиплексирование множества потоков в одном TCP-соединении вместо одного запроса за раз, сжатие заголовков HPACK, приоритеты потоков; семантика методов и кодов не изменилась.

Глубже. Если этот вопрос идёт после общего про HTTP/2, добавьте эксплуатационную сторону: HTTP/2 меняет профиль нагрузки на сервер — вместо шести соединений от клиента приходит одно с сотней потоков, поэтому важны настройки MaxConcurrentStreams и защита от Rapid Reset (CVE-2023-44487, когда клиент открывает и мгновенно сбрасывает потоки, заставляя сервер бесконечно создавать обработчики; в Go это закрыто в 1.21.x и уточнено настройками в 1.24). Также HTTP/2 плохо сочетается с L4-балансировкой: одно долгоживущее соединение прилипает к одному бэкенду.

За счет чего осуществляется стриминг данных в HTTP1? Какая нужна инструкция для этого?

Заголовок раздела «За счет чего осуществляется стриминг данных в HTTP1? Какая нужна инструкция для этого?»

Коротко. За счёт Transfer-Encoding: chunked — тело передаётся кусками, каждый предваряется своей длиной в hex, конец обозначается нулевым чанком. Это позволяет начать отдачу, не зная итогового размера, то есть не выставляя Content-Length.

Глубже. На проводе это выглядит так:

HTTP/1.1 200 OK
Content-Type: text/event-stream
Transfer-Encoding: chunked
1a\r\n
data: first message\n\n\r\n
0\r\n
\r\n

В Go net/http включает chunked автоматически, если хендлер не выставил Content-Length и пишет больше буфера; чтобы данные ушли клиенту немедленно, нужен flush:

func stream(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-store")
rc := http.NewResponseController(w) // Go 1.20+
for i := 0; i < 10; i++ {
select {
case <-r.Context().Done():
return
default:
}
fmt.Fprintf(w, "data: tick %d\n\n", i)
if err := rc.Flush(); err != nil {
return
}
time.Sleep(time.Second)
}
}

Подводные камни: WriteTimeout у сервера обрубит долгий стрим — снимайте дедлайн через rc.SetWriteDeadline(time.Time{}); промежуточный nginx по умолчанию буферизует ответ и стрим «залипает» — нужен proxy_buffering off или заголовок X-Accel-Buffering: no; сжатие gzip тоже буферизует. В HTTP/2 Transfer-Encoding запрещён — там потоковость обеспечивается самими DATA-фреймами, но код на Flusher работает одинаково. Альтернативный способ «стриминга» в HTTP/1 — диапазонные запросы Range/206 Partial Content, но это скачивание по частям по инициативе клиента, а не push.

Какие знаешь заголовки в HTTP и что они означают?

Заголовок раздела «Какие знаешь заголовки в HTTP и что они означают?»

Коротко. Основные группы: адресация и согласование контента (Host, Accept, Accept-Encoding, Accept-Language, Content-Type, Content-Length, Content-Encoding), аутентификация (Authorization, WWW-Authenticate, Cookie, Set-Cookie), кэширование (Cache-Control, ETag, If-None-Match, Last-Modified, If-Modified-Since, Expires, Vary, Age), управление соединением (Connection, Keep-Alive, Transfer-Encoding, Upgrade), редиректы и метаданные (Location, Retry-After, Date, Server, User-Agent, Referer), CORS (Origin, Access-Control-*) и безопасность (Strict-Transport-Security, Content-Security-Policy, X-Content-Type-Options).

Глубже. Заголовки, о которых стоит уметь говорить подробнее. Vary указывает, по каким заголовкам запроса различается ответ, — без Vary: Accept-Encoding CDN может отдать gzip-ответ клиенту, не поддерживающему сжатие. ETag + If-None-Match дают условные запросы и 304 Not Modified без тела — самый дешёвый способ ревалидации. Cache-Control различает no-cache (кэшировать можно, но каждый раз ревалидируй) и no-store (не сохранять вообще) — это классическая путаница. X-Forwarded-For/Forwarded несут исходный IP клиента за прокси и доверять им можно только от своих прокси. Content-Type c charset важен для текстовых типов; X-Content-Type-Options: nosniff запрещает браузеру угадывать тип и защищает от MIME-sniffing-атак. В Go имена заголовков канонизируются (http.CanonicalHeaderKey), поэтому r.Header.Get("content-type") работает, а прямой доступ к мапе r.Header["content-type"] — нет.

Перед отправкой/обработкой запроса логировать URL и метод;

Заголовок раздела «Перед отправкой/обработкой запроса логировать URL и метод;»

Коротко. Это требование из тестового задания к логирующему middleware, а не вопрос: перед вызовом основного обработчика (или перед отправкой исходящего запроса) надо записать в лог метод и URL.

Глубже. На сервере это делается обёрткой http.Handler, на клиенте — собственным http.RoundTripper:

func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
slog.InfoContext(r.Context(), "request", "method", r.Method, "url", r.URL.String())
next.ServeHTTP(w, r)
})
}

Практические оговорки: не логируйте полный URL как есть, если в query бывают токены или персональные данные — вырезайте чувствительные параметры; добавляйте корреляционный идентификатор (X-Request-Id) и кладите его в контекст, чтобы связать записи; для клиента используйте RoundTripper, а не обёртку над каждым вызовом, — тогда логи покроют и ретраи, и редиректы.

После получения/отправки ответа логировать статус код и тело ответа.

Заголовок раздела «После получения/отправки ответа логировать статус код и тело ответа.»

Коротко. Тоже пункт задания. Сложность в том, что http.ResponseWriter не позволяет прочитать записанные статус и тело, поэтому его надо обернуть своей структурой, перехватывающей WriteHeader и Write.

Глубже. Рабочая обёртка и её использование:

type capturingWriter struct {
http.ResponseWriter
status int
body bytes.Buffer
}
func (c *capturingWriter) WriteHeader(code int) {
c.status = code
c.ResponseWriter.WriteHeader(code)
}
func (c *capturingWriter) Write(b []byte) (int, error) {
if c.status == 0 {
c.status = http.StatusOK // неявный 200 при первой записи
}
c.body.Write(b) // в проде ограничьте объём!
return c.ResponseWriter.Write(b)
}
func LogResponse(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
cw := &capturingWriter{ResponseWriter: w}
start := time.Now()
next.ServeHTTP(cw, r)
slog.InfoContext(r.Context(), "response",
"status", cw.status,
"bytes", cw.body.Len(),
"duration", time.Since(start),
)
})
}

Что важно проговорить: полное тело логировать в проде нельзя — это и утечка персональных данных, и рост объёма логов, и лишняя аллокация; ограничивайте размер (первые N килобайт), маскируйте чувствительные поля и включайте тело только для ошибок или по семплированию. Вторая тонкость: обёртка «теряет» дополнительные интерфейсы http.Flusher, http.Hijacker, io.ReaderFrom — с Go 1.20+ это частично решается через http.ResponseController, который умеет доставать нужные возможности из вложенного писателя (обёртка должна реализовать метод Unwrap() http.ResponseWriter). На клиентской стороне тело ответа надо читать через io.TeeReader или буферизовать и подменять resp.Body, иначе основной код получит уже вычитанный поток.

Есть два сервиса. Как они могут взаимодействовать? ( Sync (grpc/rest) / async (brokers) )

Заголовок раздела «Есть два сервиса. Как они могут взаимодействовать? ( Sync (grpc/rest) / async (brokers) )»

Коротко. Двумя способами: синхронно — вызов «запрос-ответ» через REST/HTTP или gRPC, когда вызывающий ждёт результат; асинхронно — через брокер сообщений (Kafka, RabbitMQ, NATS) или очередь, когда отправитель публикует событие и не ждёт ответа. Выбор определяется тем, нужен ли результат немедленно и допустимо ли временное отставание.

Глубже. Синхронный вызов прост и даёт немедленную консистентность, но связывает доступность: если B лежит, A тоже деградирует, и цепочка вызовов перемножает вероятности отказа — отсюда таймауты, ретраи, circuit breaker, bulkhead. Асинхронный обмен развязывает сервисы во времени: брокер сглаживает пики, потребитель может лежать и догнать позже; платой становится eventual consistency, необходимость идемпотентных обработчиков (доставка at-least-once), контроль порядка сообщений и сложность отладки. Есть промежуточные варианты: синхронный приём с немедленным 202 Accepted и фоновой обработкой; паттерн transactional outbox, чтобы запись в БД и публикация события были атомарными; sagas для распределённых бизнес-транзакций. Практическое правило: команды, требующие ответа пользователю здесь и сейчас, — синхронно; уведомления, интеграции, расчёты, рассылки, репликация данных между сервисами — асинхронно.

Что происходит после открытия любого сайта в браузерере? (dns, http или https запрос по строчкам, ответ, его статус, рендеринг html).

Заголовок раздела «Что происходит после открытия любого сайта в браузерере? (dns, http или https запрос по строчкам, ответ, его статус, рендеринг html).»

Коротко. Браузер разбирает URL, проверяет HSTS-список и кэш, резолвит домен через DNS, устанавливает TCP-соединение и TLS-сессию, отправляет GET / HTTP/1.1 с заголовками Host, User-Agent, Accept, получает ответ со статусом (обычно 200 OK и Content-Type: text/html), парсит HTML в DOM, параллельно догружает CSS, JS и картинки, строит CSSOM и дерево рендера, выполняет layout и paint, исполняет JS и показывает страницу.

Глубже. Ключевые подробности по этапам. DNS-резолв идёт по цепочке кэшей: браузер → ОС → резолвер провайдера → корневые/TLD/авторитативные серверы. Перед подключением браузер проверяет HSTS preload — если домен там есть, http:// мгновенно превращается в https:// без запроса. Соединение: TCP-хендшейк, затем TLS с SNI и ALPN, возможно сразу HTTP/2 или переход на HTTP/3 по ранее полученному Alt-Svc. Запрос выглядит буквально как GET / HTTP/1.1, Host: example.com, Accept: text/html,..., Accept-Encoding: gzip, br, Cookie: .... Ответ может быть не 200: 301/302 — редирект по Location и всё начинается заново; 304 — берём из кэша; 4xx/5xx — страница ошибки. Рендеринг: HTML-парсер строит DOM инкрементально, синхронный <script> без async/defer блокирует парсинг, CSS блокирует рендеринг (render-blocking), затем происходит layout (расчёт геометрии), paint и composite. После загрузки срабатывают события DOMContentLoaded и load. Отдельно упомяните preconnect/preload, приоритеты загрузки ресурсов в HTTP/2 и то, что критический путь рендеринга — это то, ради чего меряют метрики LCP/FCP.

Коротко. См. выше про разницу REST и gRPC. Сжато: REST — ресурсы, HTTP-методы, JSON, человекочитаемость, кэшируемость, работа из браузера; gRPC — методы сервиса, protobuf, HTTP/2, кодогенерация, стриминг, меньше трафика и латентности, но нужен toolchain и прокси для браузера.

Глубже. Табличное сравнение по осям, которое удобно проговорить вслух: контракт — OpenAPI (описание, часто пишется отдельно и рассинхронизируется) против .proto (источник правды, из которого генерится код); формат — текстовый JSON против бинарного protobuf; транспорт — любой HTTP против HTTP/2 обязательно; модель вызова — CRUD над ресурсами против произвольных методов и четырёх видов стриминга; ошибки — коды HTTP против кодов gRPC в трейлере; кэширование — стандартное HTTP-кэширование против «своими руками»; отладка — curl/браузер против grpcurl/reflection; эволюция схемы — конвенции и версии в URL против правил совместимости protobuf с номерами полей.

Коротко. Вопрос про личный опыт, ответ «да» без деталей ничего не даёт. Каркас: назвать конкретный сервис и его домен, перечислить, какие решения принимали (структура ресурсов, версионирование, пагинация, обработка ошибок, аутентификация), и упомянуть один нетривиальный случай с объяснением выбора.

Глубже. Что интервьюер надеется услышать: как вы версионируете API (префикс /v1 в пути, заголовок, или через media type — и почему); как устроена пагинация (offset против keyset и почему на больших таблицах offset деградирует); как оформлены ошибки (единый формат, RFC 9457, коды); как сделана идемпотентность для повторных POST; как документируется контракт (OpenAPI, генерация клиентов, contract-тесты); как тестируется (httptest.Server, таблицы тестов, golden files). Типичная ошибка — рассказ уровня «делали CRUD на gin, роуты, хендлеры» без единого проектного решения.

Коротко. RPC ориентирован на действия: клиент вызывает удалённую процедуру (createOrder, cancelOrder), URL и транспорт — деталь реализации. REST ориентирован на ресурсы: сущность адресуется URI, а действие выражается стандартным HTTP-методом (POST /orders, DELETE /orders/42), плюс REST опирается на HTTP-семантику — коды статуса, кэширование, идемпотентность.

Глубже. Практические следствия. В RPC естественно выражаются операции, которые не ложатся на CRUD («пересчитать прогноз», «отправить письмо») — в REST их приходится либо моделировать как ресурс-действие (POST /orders/42/cancellations), либо честно признавать RPC-эндпоинт. RPC обычно даёт более строгий контракт и кодогенерацию, REST — универсальность и инфраструктурную поддержку (кэши, прокси, браузеры, стандартные инструменты). Идемпотентность в REST выражена методом, в RPC — только договорённостью. И честная ремарка: большинство «REST API» на практике — это RPC с ресурсными URL, уровень 2 по модели зрелости Ричардсона; называть это ошибкой не стоит, но понимать разницу нужно.

Коротко. В REST — как правило текстом: JSON (реже XML, form-urlencoded, multipart для файлов), в теле HTTP-сообщения, с указанием Content-Type. В gRPC — бинарно: protobuf-сообщения, упакованные в length-prefixed-кадры (1 байт флага сжатия + 4 байта длины big-endian + сериализованное сообщение) внутри DATA-фреймов HTTP/2, с Content-Type: application/grpc+proto.

Глубже. Почему protobuf компактнее: в JSON каждое поле несёт своё имя строкой и все числа записаны текстом, а protobuf кодирует поле как «номер + тип» в одном varint-теге и использует varint для целых, поэтому маленькие числа занимают 1 байт; поля со значением по умолчанию в proto3 вообще не передаются. Обратная сторона — сообщение нечитаемо без схемы, и разобрать его в логах или в tcpdump без .proto нельзя. gRPC поддерживает и другие кодеки (application/grpc+json), а сжатие настраивается отдельно (grpc-encoding: gzip) — в отличие от REST, где сжатие тела делает сам HTTP через Content-Encoding. Ещё разница: метаданные в gRPC (аналог заголовков) едут в HEADERS-фреймах, а бинарные значения требуют суффикса -bin в имени ключа и base64-кодирования.

Коротко. HTTP — прикладной протокол запрос-ответ для передачи представлений ресурсов между клиентом и сервером: клиент указывает URI и метод, сервер отвечает кодом статуса, заголовками и телом. Он stateless, расширяем через заголовки и служит универсальным транспортом и для веб-страниц, и для API, и для межсервисного взаимодействия.

Глубже. Практическая ценность HTTP не в «передаче данных» — это умеет и голый TCP, — а в наборе согласованных соглашений, которые понимают все промежуточные узлы. Из-за того что метод и заголовки стандартизованы, CDN знает, что GET можно закэшировать, а POST — нет; балансировщик знает, что запрос можно безопасно переотправить на другой бэкенд, если он идемпотентен; браузер знает, что при 401 надо показать форму, а при 301 — переписать закладку. Плюс HTTP согласует представление (Accept, Content-Type, Accept-Encoding), управляет условными запросами (ETag/If-None-Match, Last-Modified/If-Modified-Since), диапазонами (Range), аутентификацией (Authorization, WWW-Authenticate) и временем жизни кэша (Cache-Control). Отдельный бонус — вездесущность: 443/tcp открыт в любом корпоративном периметре, поэтому поверх HTTP строят и WebSocket (через Upgrade), и gRPC (поверх HTTP/2), и long-polling, и SSE.

Коротко. HTTPS — это HTTP, установленный поверх TLS-сессии. TLS даёт конфиденциальность (симметричное AEAD-шифрование), целостность (тег аутентификации и счётчики последовательности, защищающие от подмены и повтора) и аутентификацию сервера (сертификат X.509, проверенный по цепочке до доверенного корня, плюс сверка имени хоста с SAN). Ключи вырабатываются эфемерным ECDH, поэтому есть forward secrecy: компрометация приватного ключа сервера не расшифровывает записанный ранее трафик.

Глубже. В TLS 1.3 (RFC 8446) хендшейк занимает 1-RTT: клиент в ClientHello сразу присылает key_share, сервер отвечает ServerHello со своей долей, дальше всё, включая сертификат, уже зашифровано. Набор шифров сведён к AEAD (AES-GCM, ChaCha20-Poly1305), выкинуты RSA key transport, CBC, сжатие и ренеготиация — то есть целые классы атак (BEAST, CRIME, POODLE, Logjam) закрыты на уровне дизайна. Сессии возобновляются через PSK-тикеты, есть 0-RTT — но 0-RTT данные подвержены replay, поэтому в них допустимы только идемпотентные запросы. Аутентификация сервера — не задача самого TLS-рукопожатия, а задача приложения: проверить, что сертификат валиден по времени, подписан доверенным CA и его SAN совпадает с хостом из URL. В Go это делает crypto/tls автоматически, и главный антипаттерн на ревью — tls.Config{InsecureSkipVerify: true}, который убивает аутентификацию и превращает HTTPS в шифрование «до кого-то». Вокруг базового TLS строятся HSTS (Strict-Transport-Security, чтобы не было downgrade на http://), Certificate Transparency, OCSP stapling, а для взаимной проверки — mTLS (tls.Config{ClientAuth: tls.RequireAndVerifyClientCert, ClientCAs: pool}). Из свежего в Go: с 1.24 по умолчанию включён гибридный постквантовый обмен ключами X25519MLKEM768, а в crypto/tls появилась клиентская поддержка Encrypted Client Hello, который прячет SNI. Важно помнить и границы: TLS не скрывает IP-адрес и объём трафика, а SNI без ECH виден в открытом виде, поэтому «HTTPS = полная анонимность» — неверно.

Коротко. REST — архитектурный стиль (диссертация Роя Филдинга, 2000) с набором ограничений: клиент-сервер, stateless, кэшируемость, единообразный интерфейс, слоистость и опциональный code-on-demand. OpenAPI — формальная спецификация для описания HTTP API в YAML/JSON. Swagger — историческое имя этой же спецификации (версия 2.0) и сегодня — набор инструментов SmartBear вокруг неё: Swagger UI, Swagger Editor, Swagger Codegen.

Глубже. Путаница возникает потому, что в 2015 году спецификация Swagger 2.0 была передана в Linux Foundation и переименована в OpenAPI Specification; версии 3.0.x и 3.1 — это уже «OpenAPI», а слово «Swagger» корректно применять только к инструментам. OpenAPI 3.1 важен тем, что стал полностью совместим с JSON Schema 2020-12 — до этого в 3.0 был свой диалект с расхождениями (nullable, отсутствие examples в схемах и т. п.). Про REST на собесе полезно упомянуть модель зрелости Ричардсона: уровень 0 — один endpoint в стиле RPC поверх POST; уровень 1 — появились ресурсы; уровень 2 — используются HTTP-методы и коды по назначению (здесь находится 99% «REST API» в индустрии); уровень 3 — HATEOAS, когда ответ содержит ссылки на доступные переходы. Формально REST без HATEOAS — не REST, и честный ответ звучит так: «то, что мы обычно называем REST, — это уровень 2, RPC поверх HTTP с ресурсной адресацией». В Go экосистема такая: генерация серверных стабов и клиентов из спеки — oapi-codegen и ogen; обратный подход (код → спека) — go-swagger или аннотации swaggo/swag; валидация запросов по спеке в рантайме — kin-openapi.

Коротко. Правило одно: изменения должны быть аддитивными. Добавлять необязательные поля в запрос и новые поля в ответ можно, удалять или переименовывать поля, делать необязательное обязательным, сужать типы и убирать значения из enum запроса — нельзя. Ломающие изменения проводят через новую версию API или через приём expand-contract, а факт наличия ломающего изменения проверяют в CI автоматическим диффом спеки.

Глубже. Полезно держать в голове асимметрию направлений: у запроса «расширение» ломает клиентов (новое обязательное поле — старый клиент его не пришлёт), а у ответа «расширение» ломает клиентов только если они читают строго; наоборот, добавление нового значения в enum ответа — ломающее изменение для сгенерированных клиентов с закрытыми enum, хотя интуитивно кажется безобидным. Отсюда практики: писать «толерантных читателей» (Postel’s law — игнорировать неизвестные поля), не переиспользовать одну схему одновременно для запроса и ответа, не делать additionalProperties: false без нужды, а enum в ответах либо документировать как открытые, либо использовать строку с валидацией на сервере. Процедура expand-contract (parallel change) выглядит так: сначала добавляем новое поле рядом со старым и заполняем оба, потом мигрируем потребителей, затем помечаем старое deprecated: true и отдаём заголовок Sunset с датой, и только после истечения срока удаляем. Версионирование чаще всего делают в пути (/v1/..., /v2/...) — это грубо, но прозрачно для роутинга и логов; альтернативы — версия в media type (Accept: application/vnd.acme.v2+json) или в заголовке, они элегантнее, но неудобны для кэширования и отладки. Инструментально всё это закрывается диффом спеки в pipeline: oasdiff умеет отдельно классифицировать breaking changes и валить сборку, есть также openapi-diff и Optic. И самое надёжное — контрактные тесты со стороны потребителей (consumer-driven contracts, Pact), потому что спека может остаться совместимой, а поведение поменяться.

Какие REST-овые методы знаете? В чем между ними разница? Можно ли сделать так, что GET будет делать DELETE, а POST будет делать PUT?

Заголовок раздела «Какие REST-овые методы знаете? В чем между ними разница? Можно ли сделать так, что GET будет делать DELETE, а POST будет делать PUT?»

Коротко. Основные методы: GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE, CONNECT. Различаются они по трём свойствам из RFC 9110: безопасность (не меняет состояние: GET, HEAD, OPTIONS, TRACE), идемпотентность (повтор даёт тот же эффект: безопасные + PUT и DELETE) и кэшируемость (по умолчанию GET и HEAD). Технически заставить GET удалять ресурс можно — сервер волен обработать любой метод как угодно, — но это грубое нарушение семантики, и инфраструктура сломает вам жизнь.

Глубже. Разница между PUT и POST: PUT — полная замена ресурса по известному клиенту URI, идемпотентен (два одинаковых PUT дают одно состояние); POST — «обработай это по своим правилам», обычно создание подчинённого ресурса, URI назначает сервер и возвращает в Location с кодом 201, не идемпотентен. PATCH — частичное изменение, в общем случае не идемпотентен (например, {"op":"add"} в JSON Patch), формат тела задаётся отдельно: application/merge-patch+json (RFC 7386) или application/json-patch+json (RFC 6902). HEAD — тот же GET без тела, для проверки существования и метаданных. OPTIONS — узнать поддерживаемые методы, в браузерах — preflight CORS. TRACE и CONNECT в API практически не встречаются, CONNECT используется прокси для туннелирования TLS.

Теперь про «GET, который удаляет». Ничто в HTTP не мешает написать такой обработчик, но: браузеры и антивирусы префетчат ссылки, поисковые краулеры ходят по ним, CDN и прокси кэшируют ответы GET и могут вернуть закэшированный результат вместо повторного удаления, а клиенты и балансировщики автоматически ретраят идемпотентные запросы при сетевых ошибках. Классическая история — Google Web Accelerator, который «удалил» кучу данных в админках, где действия висели на GET-ссылках. В Go это тоже осязаемо: http.Transport может молча переотправить запрос, если соединение из пула оказалось разорвано, и делает это для запросов, которые считает повторяемыми (без тела либо с заданным GetBody) — при неидемпотентной семантике на GET получите двойное применение эффекта. Симметрично «POST, который делает PUT» — менее опасно (POST не кэшируется и не ретраится), и это ровно то, что делают легаси-клиенты через X-HTTP-Method-Override: PUT, когда прокси режет нестандартные методы; это признанный костыль, но именно костыль. Правильный ответ на собесе: технически возможно, семантически недопустимо, потому что HTTP-методы — это контракт не только с вашим клиентом, но и со всей промежуточной инфраструктурой.

Заголовок раздела «Однако, после перезапуска источник начнет с прошлой “подтвержденной” позиции, задаваемой cookie. Поэтому каждое значение cookie, которое вернул вызов Next, после сохранения данных в приемнике, должно быть фиксировано вызовом Commit, причем строго в той же последовательности, в которой их вернул Next»

Обрывок исходника, вопрос не восстанавливается. Это фрагмент описания контракта курсорного источника данных (at-least-once доставка с подтверждением позиции), а не вопрос по HTTP; слово «cookie» здесь означает непрозрачный токен позиции, а не HTTP-cookie.

Коротко. net/http/httptest даёт два независимых инструмента: httptest.NewRecorder() — фейковый http.ResponseWriter, который просто записывает статус, заголовки и тело в память, чтобы вызвать хендлер напрямую без сети; и httptest.NewServer(handler) — настоящий HTTP-сервер на 127.0.0.1 со случайным портом, к которому ходят реальным клиентом. Плюс httptest.NewRequest для сборки серверного *http.Request без парсинга сети.

Глубже. ResponseRecorder — это структура с полями Code, HeaderMap и Body *bytes.Buffer; метод Result() возвращает *http.Response со снимком заголовков, сделанным в момент WriteHeader, поэтому заголовки, выставленные после записи тела, в Result() не попадут — это самая частая ловушка. Recorder не реализует http.Hijacker, не делает chunked-кодирования, не применяет Content-Length и не проверяет корректность порядка вызовов, то есть тестирует хендлер, а не HTTP-стек. httptest.NewRequest(method, target, body) собирает запрос в «серверном» виде: заполняет RemoteAddr значением 192.0.2.1:1234, ставит Host: example.com, если в target только путь, и паникует на некорректном target — в Go 1.24 добавили вариант NewRequestWithContext. httptest.NewServer поднимает реальный net.Listener, отдаёт srv.URL, а srv.Close() дожидается завершения активных запросов (поэтому дедлок при блокирующем хендлере — типичная причина зависшего теста). Для TLS есть NewTLSServer с самоподписанным сертификатом: ходить туда нужно клиентом srv.Client(), который уже доверяет этому сертификату, либо добавить srv.Certificate() в свой пул. Для HTTP/2 берут httptest.NewUnstartedServer, выставляют srv.EnableHTTP2 = true и вызывают srv.StartTLS(). Выбор между двумя подходами простой: Recorder — быстрый юнит-тест одного хендлера и middleware-цепочки; NewServer — интеграционный тест, когда важны реальные редиректы, cookie-jar, таймауты, TLS или вы тестируете свой HTTP-клиент (тогда сервер выступает заглушкой чужого API).

package api
import (
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func handler(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id") // паттерны ServeMux с wildcard — Go 1.22+
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(map[string]string{"id": id})
}
func TestHandler_Recorder(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("GET /items/{id}", handler)
req := httptest.NewRequest(http.MethodGet, "/items/42", nil)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
res := rec.Result()
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
t.Fatalf("got %d, want 200", res.StatusCode)
}
body, _ := io.ReadAll(res.Body)
if !strings.Contains(string(body), `"42"`) {
t.Fatalf("unexpected body: %s", body)
}
}
// Тот же пакет: интеграционный вариант через реальный сервер.
func TestClient_AgainstServer(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusTeapot)
}))
defer srv.Close()
resp, err := srv.Client().Get(srv.URL + "/any")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusTeapot {
t.Fatalf("got %d, want 418", resp.StatusCode)
}
}

Верно ли утверждение, что HTTPS - это всего лишь HTTP поверх TLS?

Заголовок раздела «Верно ли утверждение, что HTTPS - это всего лишь HTTP поверх TLS?»

Коротко. В первом приближении да: семантика HTTP не меняется, меняется только то, что байты идут через TLS-сессию. Но «всего лишь» — упрощение: HTTPS определён как отдельная URI-схема со своим портом 443, своими требованиями к проверке подлинности сервера и своим origin в браузерной модели безопасности, а в HTTP/3 никакого «TLS поверх TCP» вообще нет — там TLS 1.3 встроен в QUIC.

Глубже. Что именно добавляется сверх «просто TLS». Во-первых, схема https:// обязывает клиента проверить сертификат и сверить имя хоста — сам по себе TLS позволяет и анонимные шифронаборы, и произвольные сертификаты, требование валидации приходит из спецификации HTTPS (исторически RFC 2818, сейчас RFC 9110 §4.2.2). Во-вторых, меняется порядок установления соединения: через прокси клиент сначала делает CONNECT host:443 и только потом внутри туннеля начинает TLS. В-третьих, версия HTTP теперь выбирается на этапе TLS через расширение ALPN (h2, http/1.1), а не через Upgrade; HTTP/2 в браузерах существует только по TLS и требует TLS 1.2+ с ограниченным списком шифров. В-четвёртых, для браузера http://example.com и https://example.com — разные origin: разные cookie-политики (Secure, __Host- префикс), разный доступ к «мощным» API (geolocation, service worker), блокировка mixed content. Наконец, промежуточные кэши перестают видеть содержимое, поэтому кэширование переезжает на терминатор TLS. Отдельный нюанс для Go: в стандартной библиотеке HTTP/2 включается автоматически при TLS, а с Go 1.24 появились поля Protocols у http.Server и http.Transport, которыми можно явно перечислить HTTP1, HTTP2 и UnencryptedHTTP2 (h2c). Так что формулировка «HTTPS = HTTP over TLS» — хорошая первая фраза, но за ней должны следовать оговорки про схему, ALPN, origin и HTTP/3.

Какие версии есть у HTTP и какие между ними отличия?

Заголовок раздела «Какие версии есть у HTTP и какие между ними отличия?»

Коротко. HTTP/0.9 (только GET, без заголовков), HTTP/1.0 (заголовки, коды статусов, соединение на запрос), HTTP/1.1 (обязательный Host, keep-alive по умолчанию, chunked, кэширование, Range), HTTP/2 (бинарные фреймы, мультиплексирование потоков в одном TCP-соединении, HPACK-сжатие заголовков), HTTP/3 (то же самое поверх QUIC/UDP с встроенным TLS 1.3). Семантика методов и кодов во всех версиях начиная с 1.1 одна и та же — различается только формат передачи по проводу.

Глубже. HTTP/1.1 решал проблему стоимости TCP-соединений через persistent connections, но остался с head-of-line blocking на уровне протокола: в одном соединении ответы идут строго по порядку, pipelining на практике не взлетел из-за багов прокси. Обходили это шестью параллельными соединениями на хост, доменным шардингом и склейкой ресурсов. HTTP/2 (RFC 9113) переводит всё в бинарные фреймы (HEADERS, DATA, SETTINGS, WINDOW_UPDATE, RST_STREAM), вводит независимые потоки внутри одного соединения, псевдозаголовки :method, :path, :scheme, :authority вместо стартовой строки, обязательный нижний регистр имён заголовков, HPACK со статической и динамической таблицами и покадровый flow control. Протокольный HOL-блокинг он снимает, но транспортный остаётся: потеря одного TCP-сегмента тормозит все потоки сразу, поэтому при потерях HTTP/2 может проигрывать HTTP/1.1. Server Push формально есть, но по факту умер — Chrome убрал его поддержку, вместо него используют 103 Early Hints. HTTP/3 (RFC 9114) решает транспортный HOL: QUIC поверх UDP даёт независимые от потерь стримы, объединённый хендшейк (TLS 1.3 встроен, соединение поднимается за 1-RTT, при возобновлении — 0-RTT), миграцию соединения по Connection ID при смене сети, и QPACK вместо HPACK — чтобы сжатие заголовков не создавало собственную зависимость по порядку. Обнаружение обычно через заголовок Alt-Svc или DNS-запись HTTPS/SVCB. Со стороны Go: net/http из коробки поддерживает HTTP/1.1 и HTTP/2 (реализация в golang.org/x/net/http2, встраиваемая в стандартную библиотеку); HTTP/3 в стандартной библиотеке на Go 1.24 нет — используют сторонний quic-go, при этом в стандартной библиотеке уже появился экспериментальный пакет crypto/tls с поддержкой QUIC-интерфейсов и net/quic во внутренней разработке. Отдельный практический момент: h2c (HTTP/2 без TLS) в Go до 1.24 требовал обёртки h2c.NewHandler из x/net, с 1.24 включается через Protocols с флагом UnencryptedHTTP2.

  • Говорят «HTTPS — это другой протокол». Нет, это тот же HTTP внутри TLS; отличается транспортная обёртка, порт и схема, а семантика идентична.
  • Путают 401 и 403: 401 — «не знаю, кто ты» (нужен WWW-Authenticate), 403 — «знаю, но нельзя».
  • Называют HTTP/2 «быстрее, потому что бинарный». Основной выигрыш — мультиплексирование и HPACK, а не бинарность как таковая; и HOL blocking на уровне TCP в HTTP/2 остаётся.
  • Утверждают, что HTTP/3 — это «HTTP поверх UDP». Точнее: поверх QUIC, который сам даёт надёжность, порядок внутри потока и встроенный TLS 1.3, а UDP — лишь способ доставки датаграмм.
  • Считают, что PUT и PATCH взаимозаменяемы. PUT заменяет ресурс целиком и идемпотентен, PATCH меняет частично и в общем случае нет.
  • Забывают, что в Go у http.Client и http.Server таймауты по умолчанию нулевые, то есть бесконечные, и что тело ответа надо и закрывать, и дочитывать, иначе соединение не вернётся в пул.
  • Говорят «cookie хранятся на сервере». Cookie хранит клиент; сервер хранит только связанные данные сессии, если он вообще их хранит.
  • Отдают 200 OK с полем error в теле — это ломает клиентов, кэши и мониторинг; ошибка должна быть выражена кодом статуса.
  • Сравнивают gRPC и HTTP как равнозначные альтернативы, не оговорив, что gRPC работает поверх HTTP/2.
  • Говорят «HTTP/2 добавил новые методы/коды» — нет, семантика (методы, статусы, заголовки) общая, менялся только транспортный синтаксис.
  • Считают, что HTTP/3 «просто HTTP/2 по UDP»: главное отличие — устранение транспортного head-of-line blocking и встроенный в QUIC TLS 1.3, а не сам факт UDP.
  • Путают безопасность и идемпотентность: DELETE не безопасен (меняет состояние), но идемпотентен; POST не идемпотентен, а PUT — да.
  • Утверждают, что «REST — это JSON по HTTP». REST — стиль с ограничениями; формат тела в нём вообще не зафиксирован, а большинство реальных API — уровень 2 по Ричардсону, без HATEOAS.
  • Называют Swagger и OpenAPI синонимами без оговорок: Swagger 2.0 — старое имя спецификации, сейчас это набор инструментов, а спецификация называется OpenAPI (3.0/3.1).
  • Считают добавление нового значения в enum ответа безопасным изменением — для строго сгенерированных клиентов это breaking change.
  • Думают, что InsecureSkipVerify: true «просто отключает предупреждение»: он выключает аутентификацию сервера целиком и делает MITM тривиальным.
  • Тестируют хендлер через httptest.NewRecorder и удивляются, что заголовки, выставленные после WriteHeader, не видны в Result().
  • Считают, что TLS скрывает всё: IP, размеры и тайминги пакетов, а без ECH и SNI остаются наблюдаемыми.

Список исходных вопросов с привязкой к компаниям: ../questions/http.md