Код вокруг каждого обработчика
Middleware — это код, который выполняется вокруг ваших обработчиков: до того, как запрос дойдёт до них, после того, как ответ ушёл от них, или вместо них. Логирование, аутентификация, ограничение частоты запросов, сжатие — всё это middleware.
Одна и та же идея встречается под тремя именами: веб-фреймворки говорят middleware, gRPC говорит interceptor, некоторые экосистемы говорят filter. Одно понятие, три словаря.
В Go middleware — это функция, которая принимает обработчик и возвращает обёрнутый обработчик, то есть в точности паттерн Decorator из группы паттернов проектирования, а стек таких функций ведёт себя как Chain of Responsibility.
Middleware для логирования
withLog оборачивает любой обработчик: печатает входящий запрос, даёт обработчику выполнить работу и печатает снова на выходе. Сам обработчик при этом ничего об этом не знает.
Обернуть один раз, логировать всё
Обёрнутый обработчик обслуживает два разных пути, и оба попадают в лог — логирование написано один раз, в обёртке, а не в каждом обработчике.
Вывод: программа на Go и захваченный вывод
package main
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
)
// Middleware: принимает обработчик, возвращает обёрнутый обработчик.
func withLog(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println("-> ", r.Method, r.URL.Path)
next.ServeHTTP(w, r)
fmt.Println("<- готово")
})
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /users", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Ана, Бен")
})
mux.HandleFunc("GET /notes", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "покупки, идеи")
})
server := httptest.NewServer(withLog(mux))
defer server.Close()
for _, path := range []string{"/users", "/notes"} {
resp, _ := http.Get(server.URL + path)
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
fmt.Println("тело:", strings.TrimSpace(string(body)))
}
}
Middleware, которое останавливает запрос
Middleware может также отказать: проверка аутентификации, не нашедшая корректного ключа, записывает 401 Unauthorized и никогда не вызывает следующий обработчик.
В цепочке withLog(withAuth(handler)) запрос проходит через слои по порядку, а ответ возвращается наружу через них же.
Слой аутентификации в цепочке
Запрос без ключа останавливается слоем аутентификации с кодом 401 — обработчик так и не выполняется. С ключом запрос проходит всю цепочку и добирается до данных.
Вывод: программа на Go и захваченный вывод
package main
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
)
func withLog(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println("->", r.Method, r.URL.Path)
next.ServeHTTP(w, r)
})
}
// Middleware аутентификации: может остановить запрос до обработчика.
func withAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("X-API-Key") != "secret123" {
w.WriteHeader(http.StatusUnauthorized)
fmt.Fprintln(w, "API-ключ отсутствует или неверный")
return
}
next.ServeHTTP(w, r)
})
}
func main() {
data := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "секретный отчёт")
})
server := httptest.NewServer(withLog(withAuth(data)))
defer server.Close()
client := &http.Client{}
for _, key := range []string{"", "secret123"} {
req, _ := http.NewRequest(http.MethodGet, server.URL+"/report", nil)
if key != "" {
req.Header.Set("X-API-Key", key)
}
resp, _ := client.Do(req)
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
fmt.Println(" статус:", resp.StatusCode, "-", strings.TrimSpace(string(body)))
}
}