JavaRush /Курсы /Go SELF /Основы HTTP — методы, статус‑коды, идемпотентность

Основы HTTP — методы, статус‑коды, идемпотентность

Go SELF
56 уровень , 0 лекция
Открыта

1. HTTP как контракт: запрос → ответ

Когда люди впервые слышат «HTTP», они часто думают: «ну это же про сайты». И да, сайты тоже. Но для разработчика HTTP — это прежде всего очень строгий способ договориться между двумя программами: одна отправляет запрос (client), другая отвечает (server). Этот договор называется контрактом, и он включает не только «какой URL дернуть», но и «какой метод», «какие коды успеха/ошибок», «какой формат тела», «что считается корректным вводом».

В Go это особенно приятно: Go исторически очень сильный язык для серверов и сетевого кода. Даже в материалах про эволюцию Go подчёркивается, что Go проектировали для написания серверов.

Ментальная модель «конверта»

Представьте, что HTTP — это почта.

  • Письмо = запрос (request).
  • Конверт = метаданные (метод, путь, заголовки).
  • Содержимое письма = тело запроса (например JSON).
  • Ответное письмо = ответ (response).
  • Марка на ответе = статус‑код (200, 404, 500…), который говорит «что произошло» коротко и однозначно.

Звучит слегка олдскульно, но как ментальная модель работает идеально: сервер не «угадывает по настроению», а отвечает формально.

Схема запроса и ответа

sequenceDiagram
    participant C as Client
    participant S as Server

    C->>S: HTTP Request (method + path + headers + body)
    S->>S: validate + compute
    S-->>C: HTTP Response (status + headers + body)

Самая важная мысль из этой схемы: на выходе всегда есть статус‑код, а тело ответа может быть, а может не быть. И клиент должен уметь жить по статус‑коду, а не пытаться «парсить текст ошибки глазами».

Endpoint: метод + путь

Чтобы перестать говорить про HTTP как про магию, удобно ввести маленький термин: endpoint — это конкретная «точка входа» в API. И состоит она из двух частей: HTTP‑метод (что делаем) и путь (с чем работаем).

Например, «получить список задач» часто выглядит как GET /api/v1/tasks.

Сейчас мы не запускаем сервер и не пишем обработчики. Мы делаем то, что делают взрослые инженеры в начале проекта: фиксируем контракт. И для этого можно даже написать пару структур в Go — не потому что нам «надо код», а чтобы мысли стали точнее.

Мини‑тип для описания endpoint

package main

type Endpoint struct {
	Method string
	Path   string
}

var listTasks = Endpoint{Method: "GET", Path: "/api/v1/tasks"}
var createTask = Endpoint{Method: "POST", Path: "/api/v1/tasks"}

Здесь важна не «красота кода», а дисциплина мышления: endpoint — это именно пара (method, path). Если вы меняете метод, вы меняете смысл. Если вы меняете путь, вы меняете ресурс.

2. HTTP‑методы: смысл действия и обещания клиенту

Про методы удобно думать как про глаголы с очень строгими правилами. Если вы используете методы «как попало», то клиенту приходится угадывать поведение, а API становится похож на квест «угадай кнопку». Если же вы придерживаетесь стандартной семантики, клиент (и другие программисты) понимают поведение без чтения исходников сервера. Это экономит часы, нервы и количество сообщений «а почему тут 200, когда ошибка?».

Ниже — практичная таблица, которую реально держат в голове, когда проектируют API.

Таблица базовой семантики методов

Метод Идея (по‑человечески) Обычно про что Идемпотентность (идея)
GET
«Дай данные» чтение да
POST
«Создай / выполни действие» создание/команда обычно нет
PUT
«Замени целиком» полная замена ресурса да
PATCH
«Измени частично» частичное изменение обычно да (если спроектировано аккуратно)
DELETE
«Удали» удаление да

Пара комментариев, чтобы это не выглядело как сухая теория.

GET почти всегда безопасен в том смысле, что не должен менять состояние на сервере. Если GET вдруг создаёт задачу, списывает деньги и отправляет письмо маме — это не GET, это сюжет для фильма ужасов.

POST часто используют для создания: «создать задачу», «создать пользователя». Но иногда POST — это «команда», которая не укладывается в «заменить состояние». Например: «запустить пересчёт отчёта». В нашем учебном домене задач мы постараемся держать POST как «создать задачу», потому что так проще.

PUT и PATCH: если вы не уверены, не торопитесь их применять. Но понимать смысл важно: PUT — это «вот новая версия ресурса целиком», PATCH — «вот маленькое изменение».

DELETE — удалить. И тут начинается философия: что значит «удалить», если ресурс уже удалён? Обычно договор делают так, чтобы повтор DELETE не ломал систему.

3. Статус‑коды: язык ответа сервера без гадания по тексту

Если метод — это «что мы хотим», то статус‑код — это «что получилось». И это не второстепенная деталь, а часть контракта: клиент пишет свою логику, ориентируясь на коды. В хорошем API клиенту не нужно «прочитать строку ошибки» и понять, что вы имели в виду. Он видит 404 и понимает «не найдено». Он видит 400 и понимает «я что‑то отправил не так».

Здесь есть очень удобная ментальная модель: классы кодов.

  • 2xx — успех,
  • 4xx — проблема в запросе (клиент виноват или хотя бы клиент может исправить),
  • 5xx — проблема на сервере (клиент не виноват).

Маленькая функция «класс кода»

package main

func statusClass(code int) int {
	return code / 100
}

// 200 -> 2
// 404 -> 4
// 500 -> 5

Да, это школьная математика. Но она помогает: увидели statusClass == 4 — значит, это «ошибка запроса». Увидели statusClass == 5 — «что-то сломалось у нас».

Минимальный набор кодов, который нужно уверенно понимать

В рамках нашего будущего API задач нам хватит небольшой «базовой пятёрки» (плюс один):

Код Имя Когда обычно используется Есть ли тело ответа
200
OK успешное чтение/изменение обычно да
201
Created ресурс создан обычно да (часто возвращают созданный объект)
204
No Content успех без тела нет (и это важно!)
400
Bad Request неверный ввод/формат/валидация обычно да (ошибка в JSON)
404
Not Found ресурс не найден обычно да
500
Internal Server Error внутренняя ошибка сервера обычно да (но сообщение стабильное и безопасное)

Критичный нюанс про 204: это не «200, но без настроения». Это явный сигнал: «успех, тела нет». Если вы вернёте 204 и при этом отправите JSON — некоторые клиенты его просто проигнорируют, а некоторые будут вести себя непредсказуемо. То есть вы сами себе устроите баг, а потом будете героически его чинить.

Быстрый хелпер «это успех?»

package main

func is2xx(code int) bool {
	return code >= 200 && code <= 299
}

4. Идемпотентность: повторили запрос — мир не стал хуже

Слово «идемпотентность» звучит так, будто его придумали, чтобы пугать студентов на контрольных. На практике идея очень простая: если вы повторите запрос, итоговый эффект не должен измениться. Это не значит, что «ничего не меняется». Это значит: «повтор не добавит лишнего эффекта».

Пример из жизни: кнопка «удалить задачу». Если пользователь нажал её дважды (или сеть дёрнулась, и клиент повторил запрос), задача должна остаться удалённой, а не «удалиться два раза» (что бы это ни значило).

Идемпотентность важна из‑за реального мира: сеть нестабильна, соединения рвутся, клиент может не получить ответ и решить повторить запрос. Если вы проектируете API так, чтобы повтор был безопасен, вы уменьшаете количество странных дублей и «призрачных» состояний.

Грубое правило как первое приближение

Часто делают так: считают, что GET/PUT/PATCH/DELETE должны быть идемпотентными, а POST — нет (потому что «создать» дважды = два ресурса). Это не абсолютная истина, но хорошая стартовая дисциплина.

package main

func isIdempotent(method string) bool {
	switch method {
	case "GET", "PUT", "PATCH", "DELETE":
		return true
	default:
		return false
	}
}

Маленькая демонстрация, чтобы «пощупать»

package main

import "fmt"

func main() {
	fmt.Println(isIdempotent("GET"))    // true
	fmt.Println(isIdempotent("POST"))   // false
	fmt.Println(isIdempotent("DELETE")) // true
}

Это, конечно, не «реальная безопасность API». Но это хороший способ перестать путать идемпотентность с «ничего не меняет».

5. Привязываем к домену: черновик HTTP‑контракта для задач

Сейчас мы сделаем важный шаг: переведём абстрактные HTTP‑слова в конкретный словарь нашего приложения. Мы уже давно живём в домене задач (tasks): добавляем задачу, смотрим список, отмечаем выполненной. В HTTP‑стиле мы хотим, чтобы путь описывал сущность, а метод описывал действие.

Договоримся, что API версионируем через путь. Это выглядит так: /api/v1/.... Версия в пути — это способ сказать: «контракт зафиксирован, и если мы его сломаем, то сделаем это в другой версии, а не внезапно в пятницу вечером».

Таблица endpoint’ов как черновик контракта

Смысл операции Метод Путь Успех
Список задач
GET
/api/v1/tasks
200
Создать задачу
POST
/api/v1/tasks
201
Получить задачу
GET
/api/v1/tasks/{id}
200
Отметить «done»
PATCH
/api/v1/tasks/{id}/done
200 (или 204, если решим без тела)
Удалить задачу
DELETE
/api/v1/tasks/{id}
204

Обратите внимание: мы пока не обсуждаем JSON‑форматы и единый формат ошибок (это тоже часть контракта, но это отдельная тема). Здесь мы тренируем «скелет»: метод, путь, статус успеха — чтобы у нас не было POST /getTask и GET /deleteTask. Мы пишем API так, чтобы его можно было читать как английский (ну или как очень странный английский).

Чуть кода, чтобы зафиксировать методы и не писать строки руками

Ручные строки "GET" и "POST" работают, но легко ошибиться. Поэтому даже на раннем этапе полезно завести константы.

package main

type Method string

const (
	MethodGet    Method = "GET"
	MethodPost   Method = "POST"
	MethodPut    Method = "PUT"
	MethodPatch  Method = "PATCH"
	MethodDelete Method = "DELETE"
)

А теперь endpoint’ы уже можно описывать «типизированно»:

package main

type Endpoint struct {
	Method Method
	Path   string
}

var listTasks = Endpoint{Method: MethodGet, Path: "/api/v1/tasks"}
var deleteTask = Endpoint{Method: MethodDelete, Path: "/api/v1/tasks/{id}"}

6. Типичные ошибки при проектировании HTTP‑API

Ошибка №1: менять состояние через GET.
Обычно это происходит «из удобства»: «ну я же просто по ссылке открою…». Но GET — это чтение. Если GET меняет состояние, вы ломаете кэширование, ломаете повторные запросы и удивляете всех клиентов. В какой‑то момент вы поймаете баг «у нас задача создаётся сама» — и это будет не магия, а GET.

Ошибка №2: всегда отвечать 200 и прятать ошибки в тексте.
Иногда делают так: статус 200, а внутри JSON поле "error": "...". Для клиента это ужасно: ему нужно парсить тело, чтобы понять, успех это или нет. Правильнее, чтобы успех был 2xx, ошибка запроса была 4xx, а внутренняя ошибка сервера была 5xx. Тело ошибки может быть структурированным, но код — первичен.

Ошибка №3: путать 400 и 404.
Если клиент прислал id = "abc" и вы не можете разобрать его как число — это не «не найдено», это «неверный запрос», то есть 400. А 404 — это когда id корректный, но такого ресурса нет. Это различие сильно упрощает жизнь клиентам и тестам.

Ошибка №4: отдавать тело ответа при 204.
204 означает «успех, но тела нет». Если вы всё же отправите JSON, часть клиентов его проигнорирует. Получится API, которое «иногда работает». А «иногда работает» — это особый вид боли, потому что отлаживать нечего: оно же «иногда».

Ошибка №5: не думать про идемпотентность и повторы.
В реальном мире запросы повторяются. Если вы проектируете операции так, что повтор DELETE внезапно превращается в 500, или повтор PATCH создаёт дублирующие изменения, то клиенты будут бояться ретраев, а вы будете ловить «странные» состояния. Идемпотентность — это не украшение, а страховка от сетевой реальности.

1
Задача
Go SELF, 56 уровень, 0 лекция
Недоступна
Класс ответа
Класс ответа
1
Задача
Go SELF, 56 уровень, 0 лекция
Недоступна
Идемпотентный метод
Идемпотентный метод
1
Задача
Go SELF, 56 уровень, 0 лекция
Недоступна
Черновик контракта
Черновик контракта
1
Задача
Go SELF, 56 уровень, 0 лекция
Недоступна
Симулятор контракта
Симулятор контракта
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ