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

Основи HTTP — методи, статус-коди й ідемпотентність

Go SELF
Рівень 56 , Лекція 0
Відкрита

1. HTTP як контракт: запит → відповідь

Коли люди вперше чують «HTTP», вони часто думають: «Ну це ж про сайти». І так, сайти — теж. Але для розробника HTTP — насамперед дуже строгий спосіб домовитися між двома програмами: одна надсилає запит (client), інша відповідає (server). Цю домовленість називають контрактом. Вона охоплює не лише «який URL смикнути», а й «який метод», «які коди успіху й помилок», «який формат тіла» та «що вважається коректними вхідними даними».

У Go це особливо приємно: мова історично дуже сильна для серверів і мережевого коду. Навіть у матеріалах про еволюцію Go підкреслюють, що її проєктували саме для написання серверів.

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

Уявіть, що HTTP — це пошта.

  • Лист = запит (request).
  • Конверт = метадані (метод, шлях, заголовки).
  • Вміст листа = тіло запиту (наприклад, JSON).
  • Лист у відповідь = відповідь (response).
  • Штамп на відповіді = статус-код (200, 404, 500…), який коротко й однозначно говорить, що сталося.

Звучить трохи олдскульно, але як ментальна модель це працює ідеально: сервер не вгадує настрій, а відповідає формально.

Схема запиту та відповіді

sequenceDiagram
    participant C as Клієнт
    participant S as Сервер

    C->>S: HTTP-запит (method + path + headers + body)
    S->>S: перевірка + обчислення
    S-->>C: HTTP-відповідь (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 створює дублювальні зміни, то клієнти боятимуться ретраїв, а ви ловитимете «дивні» стани. Ідемпотентність — це не прикраса, а страховка від мережевої реальності.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ