JavaRush /Курси /Go SELF /404 і 405 — коректні відповіді та де вони формуються

404 і 405 — коректні відповіді та де вони формуються

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

1. 404 і 405: схожі зовні, але означають різне

Коли ви починаєте писати API, перші помилки часто виглядають однаково: «Я надіслав запит, а у відповідь отримав не те». Дуже хочеться всі такі випадки автоматично називати «не знайдено» і ставити 404 куди завгодно. Але 404 і 405 — це різні діагнози. Якщо їх плутати, клієнт, а іноді й ви через два дні, починає лікувати ангіну зеленкою.

404 Not Found означає: «за цим шляхом сервер не знає, що робити» або «ресурсу з такими параметрами не існує». Тобто «не знайдено» — це або про маршрутизацію, або про дані.

405 Method Not Allowed означає: «шлях я розумію, але метод не той». Тобто двері є, але ви прийшли не з тим ключем: наприклад, ви надіслали POST /api/v1/tasks/1, а сервер на цьому шляху очікує лише GET.

Порівняймо в невеликій таблиці:

Ситуація Що це означає за змістом Який статус очікується
Клієнт прийшов на шлях, якого в API немає «Такої сторінки або кінцевої точки немає»
404
Клієнт прийшов на правильний шлях, але методом, який тут заборонений «Шлях правильний, але цей метод не можна використовувати»
405
Клієнт прийшов на правильний шлях і метод, але просить неіснуючий ресурс (id не знайдено) «Кінцева точка є, але об’єкта немає»
404

Зверніть увагу на останній пункт: 404 буває і на рівні маршрутизації, і на рівні даних. І це нормально. Важливо лише розуміти, хто саме вирішив повернути 404: роутер (ServeMux) чи ваш handler.

2. Де формуються 404/405 у net/http

Коли ви пишете сервер на Go, зручно уявляти, що запит проходить через два «контрольні пункти».

Спочатку запит потрапляє в ServeMux: він дивиться на шлях, а якщо ви використовуєте method-aware patterns, то ще й на метод, і вирішує, який обробник має обслуговувати запит. Якщо відповідного обробника немає, саме тут і з’являється 404.

Якщо обробника знайдено, запит передається у вашу функцію, а далі вже ви вирішуєте, що повернути. Тут ви можете повернути 200, 400, 404 (якщо ресурс не знайдено), 500 тощо.

Для наочності — мінісхема:

flowchart TD
    A[HTTP-запит: метод + шлях] --> B{ServeMux шукає обробника}
    B -->|Немає відповідного| C[404 Not Found
на рівні маршрутизації] B -->|Є відповідний| D[Ваш обробник] D --> E{Перевірки всередині обробника} E -->|Ресурс не знайдено| F[404 Not Found
на рівні даних] E -->|Метод не підтримано
якщо перевіряєте вручну| G[405 Method Not Allowed] E -->|Усе гаразд| H[2xx — успіх]

Невеликий фрагмент навчального сервера

Продовжімо ідею навчального API «таск-трекера» (системи завдань). Мінімально зареєструймо пару маршрутів:

package main

import (
	"net/http"
)

func main() {
	mux := http.NewServeMux()

	mux.HandleFunc("GET /api/v1/tasks", listTasks)
	mux.HandleFunc("GET /api/v1/tasks/{id}", getTask)

	_ = http.ListenAndServe(":8080", mux)
}

Тут важливо не те, що роблять listTasks і getTask, а те, що ми явно кажемо ServeMux, які шляхи та методи існують. Тепер:

  • GET /api/v1/tasks — існує
  • POST /api/v1/tasks — не існує, поки ми його не зареєструємо
  • GET /api/v1/aliens — не існує
  • GET /api/v1/tasks/123 — існує як маршрут, а що буде з даними — вирішить обробник

3. Два види 404: маршрутизація та дані

404 багато хто сприймає як «ой, сервер зламався й нічого не знайшов». Але насправді 404 — це цілком коректна й корисна відповідь. У хороших API 404 — це спосіб сказати клієнту: «ви просите те, чого в нас немає, і це очікувано».

Спочатку розберімо 404 на рівні маршрутизації. Це ситуація, коли ServeMux не знайшов відповідного обробника. Наприклад, у вас немає маршруту "GET /api/v1/projects", і запит на нього — це просто невідома точка входу. У цьому випадку до вашого коду справа навіть не дійшла, і 404 сформував роутер.

А тепер 404 на рівні даних. Припустімо, маршрут є: "GET /api/v1/tasks/{id}" зареєстровано. Але id=999 у вас у пам’яті або в базі відсутній. Тоді маршрут знайдено, обробник уже запущено, але ви вирішуєте: «ресурсу немає» — отже, знову 404, тільки вже з іншої причини.

Цей сенс добре видно в прикладі з практики обробки помилок у Go: коли запис не знаходиться у сховищі, обробник повертає користувачу 404 із повідомленням "Record not found".

Мініприклад: 404 «завдання не знайдено» в обробнику

Зробімо просте сховище в пам’яті. Для навчання — ідеально, для продакшну — спірно, але зараз не про це.

package main

var tasks = map[int]string{
	1: "купити молоко",
	2: "вивчити Go",
}

І обробник getTask, який поверне 404, якщо ключа немає:

package main

import (
	"fmt"
	"net/http"
	"strconv"
)

func getTask(w http.ResponseWriter, r *http.Request) {
	id, err := strconv.Atoi(r.PathValue("id"))
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		return
	}
	title, ok := tasks[id]
	if !ok {
		w.WriteHeader(http.StatusNotFound)
		return
	}
	fmt.Fprintln(w, title) // наприклад: купити молоко
}

Тут сталося важливе: маршрут існує, обробник викликано, але 404 з’явився тому, що даних не знайдено.

І це нормальний сценарій: клієнт міг запросити завдання за id, яке вже видалили, або просто помилитися. Це не внутрішня помилка сервера, тож тут не 500.

4. 405 Method Not Allowed: шлях правильний, метод — ні

405 — це відповідь рівня «я вас зрозумів, але так не можна». І вона помітно полегшує життя клієнту. Бо коли ви відповідаєте 404 на неправильний метод, клієнт думає: «шляху немає» і починає виправляти URL. Хоча URL був правильний — проблема була в методі.

Є два основні способи отримати 405 у Go-сервері:

  • Перший спосіб — ви використовуєте method-aware patterns ("GET /path", "POST /path"), і тоді частину роботи за вас бере ServeMux: він знає, що шлях існує, але метод не підходить, і може повернути 405.
  • Другий спосіб — ви реєструєте шлях без методу або одним обробником на всі методи, а всередині обробника самі робите switch за r.Method і на невідомі методи відповідаєте 405.

Варіант A: різні обробники для різних методів

package main

import "net/http"

func registerTaskRoutes(mux *http.ServeMux) {
	mux.HandleFunc("GET /api/v1/tasks", listTasks)
	mux.HandleFunc("POST /api/v1/tasks", createTask)
}

Тепер шлях один (/api/v1/tasks), але зареєстровано два варіанти. Якщо клієнт надішле PUT /api/v1/tasks, це вже неправильний метод.

Варіант B: один обробник і ручний switch за r.Method

Іноді так роблять для простих випадків або якщо хочуть тримати логіку в одному місці:

package main

import (
	"fmt"
	"net/http"
)

func tasksEndpoint(w http.ResponseWriter, r *http.Request) {
	switch r.Method {
	case http.MethodGet:
		fmt.Fprintln(w, "list tasks")
	case http.MethodPost:
		w.WriteHeader(http.StatusCreated)
	default:
		w.Header().Set("Allow", "GET, POST")
		w.WriteHeader(http.StatusMethodNotAllowed)
	}
}

Зверніть увагу: за ручного 405 потрібно пам’ятати про заголовок Allow. Це не «опція для педантів», а частина змісту 405: «ось які методи дозволені».

Заголовок Allow: як читати й навіщо він потрібен

Заголовок Allow — це чесна підказка клієнту: які методи допустимі для цього ресурсу. Він особливо корисний, коли одна й та сама точка API означає різні дії залежно від методу. Наприклад, GET /api/v1/tasks — «отримати список», а POST /api/v1/tasks — «створити завдання».

Якщо сервер відповідає 405, добрим тоном і, по суті, стандартним очікуванням є додати Allow. Тоді клієнт може автоматично повторити запит правильним методом або принаймні вивести зрозумілу помилку користувачу чи розробнику.

У мінімальному вигляді це виглядає так:

package main

import "net/http"

func methodNotAllowed(w http.ResponseWriter, allow string) {
	w.Header().Set("Allow", allow)
	w.WriteHeader(http.StatusMethodNotAllowed)
}

І в обробнику:

package main

import "net/http"

func onlyGet(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		methodNotAllowed(w, "GET")
		return
	}
	w.WriteHeader(http.StatusOK)
}

Як швидко перевірити Allow через curl

Коли ви налагоджуєте 405, корисно дивитися не лише тіло відповіді, а й заголовки:

curl -i -X PUT http://localhost:8080/api/v1/tasks

Ключ -i показує заголовки. Якщо ви бачите HTTP/1.1 405 Method Not Allowed і рядок на кшталт Allow: GET, POST, отже сервер каже: «шлях я знаю, але PUT тут не можна».

5. Навіщо розрізняти 404 і 405

Якби ми писали програму лише для себе, можна було б сказати: «та байдуже, яка різниця, усе одно помилка». Але API майже завжди використовує хтось інший: фронтенд, мобільний застосунок, скрипт або інший сервіс. І тут різниця стає дуже практичною.

Коли клієнт отримує 404, він зазвичай робить висновок: «URL неправильний» або «ресурсу немає». Коли отримує 405, робить висновок: «метод неправильний, спробую інший». Це різні гілки поведінки. Якщо ви плутаєте статуси, ви буквально змушуєте клієнта лагодити не те місце.

Ще один важливий момент — налагодження. 404 від роутера і 404 від обробника виглядають однаково за статусом, але причини в них різні. У першому випадку ви забули зареєструвати маршрут або помилилися в шаблоні шляху. У другому — усе зареєстровано, але бізнес-логіка каже: «немає такого об’єкта».

Щоб налагодження було менш «магічним», корисно хоча б тимчасово логувати метод і шлях в обробниках. Так, старий добрий fmt.Printf іноді рятує краще за філософські роздуми:

package main

import (
	"fmt"
	"net/http"
)

func debug(w http.ResponseWriter, r *http.Request) {
	fmt.Printf("method=%s path=%s\n", r.Method, r.URL.Path) // method=GET path=/api/v1/tasks/1
	w.WriteHeader(http.StatusOK)
}

Навіть один такий рядок іноді миттєво показує проблему: наприклад, клієнт ходить на /api/v1/tasks/1/ із кінцевим слешем, а ви зареєстрували /api/v1/tasks/{id} без завершального слеша. І ви дивитеся на 404 і думаєте: «чому?!», хоча відповідь проста: рядки шляху різні.

6. Типові помилки

На 404/405 новачки наступають дуже стабільно, ніби це не статуси, а граблі з автопідігрівом. Причина зазвичай у тому, що ми думаємо: «сервер має зрозуміти, що я мав на увазі». Але комп’ютер — істота буквальна: якщо ви сказали PUT, він вірить, що ви хотіли саме PUT, а не «ну, взагалі-то я хотів оновити завдання, але не пам’ятаю як».

Помилка №1: віддавати 404 замість 405, бо «так простіше».
Так простіше рівно до першого клієнта, який починає «лагодити URL» і витрачати час на неправильну діагностику. Якщо шлях існує, але метод заборонено, краще чесно сказати 405 і, якщо формуєте відповідь вручну, додати Allow. Це робить API передбачуваним.

Помилка №2: плутати 404 «маршрут не знайдено» і 404 «ресурс не знайдено».
Обидві відповіді коректні, але важливо тримати в голові, де саме вона виникла. Якщо 404 прийшов від роутера, отже проблема в реєстрації маршрутів або в шаблоні шляху. Якщо 404 прийшов із обробника, отже код відпрацював і вирішив, що даних немає, наприклад, id відсутній у сховищі.

Помилка №3: робити ручну перевірку методу і забувати Allow.
Код w.WriteHeader(405) виглядає «достатньо», але за змістом 405 майже завжди має на увазі: «ось дозволені методи». Якщо ви не вкажете Allow, клієнту складніше автоматично зрозуміти, що робити далі, а вам — складніше налагоджувати.

Помилка №4: відповідати 500 на неправильний метод.
Неправильний метод — це помилка запиту клієнта, а не «сервер зламався». 500 залишаємо для ситуацій, коли сервер справді не зміг обробити коректний запит: помилка бази, panic, баги логіки. Метод PATCH замість POST — не баг сервера, а неправильне використання API.

Помилка №5: реєструвати маршрути не там і запускати сервер з іншим mux.
Іноді маршрути вішають на один ServeMux, а ListenAndServe запускають з іншим або взагалі з nil, що означає DefaultServeMux. Підсумок виглядає як «увесь час 404». Ліки прості: тримайте один mux := http.NewServeMux() і передавайте саме його серверу, як у прикладах.

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