Модуль 9. Middleware и валидация
Цели модуля
После модуля студент сможет:
- Понимать middleware-паттерн и его роль в HTTP-обработке
- Реализовывать Request ID middleware для корреляции запросов
- Писать структурированное логирование запросов
- Защищать сервис от panic через Recovery middleware
- Ограничивать время обработки через Timeout middleware
- Валидировать запросы с go-playground/validator
- Компонировать middleware в правильном порядке
- Маскировать PII в логах
9.1 Middleware-паттерн
Что такое middleware
Middleware - функция которая оборачивает http.Handler и выполняет код до и/или после обработки запроса.
Сигнатура middleware:
textfunc(http.Handler) http.Handler
Middleware принимает следующий handler в цепочке и возвращает новый handler с дополнительным поведением.
Базовая структура
gofunc MyMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Код ДО обработки запроса // ... next.ServeHTTP(w, r) // вызываем следующий handler // Код ПОСЛЕ обработки запроса // ... }) }
Пример: простой middleware
gofunc HelloWorld(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Println("before handler") next.ServeHTTP(w, r) fmt.Println("after handler") }) }
Применение middleware
go// Оборачиваем один handler handler := HelloWorld(http.HandlerFunc(myHandler)) // Оборачиваем mux mux := http.NewServeMux() // ... регистрация роутов wrappedMux := HelloWorld(mux) // Цепочка middleware handler = MiddlewareA(MiddlewareB(MiddlewareC(handler)))
Порядок выполнения
Middleware выполняется в порядке добавления (внешний -> внутренний):
gohandler = Logging(handler) // 3. выполняется последним handler = RequestID(handler) // 2. выполняется вторым handler = Recovery(handler) // 1. выполняется первым // Порядок выполнения запроса: // Recovery -> RequestID -> Logging -> Handler
Для запроса: Recovery выполняется первым, потом RequestID, потом Logging. Для ответа: порядок обратный.
9.2 Request ID middleware
Зачем нужен Request ID
Request ID - уникальный идентификатор запроса. Используется для:
- Корреляции логов одного запроса
- Трассировки запроса через сервисы
- Отладки проблем в production
- Связи с клиентом (клиент может передать свой ID)
Реализация
gopackage middleware import ( "context" "net/http" "github.com/google/uuid" ) type contextKey string const RequestIDKey contextKey = "request_id" func RequestID(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Получаем ID из заголовка или генерируем новый id := r.Header.Get("X-Request-ID") if id == "" { id = uuid.NewString() } // Добавляем в context ctx := context.WithValue(r.Context(), RequestIDKey, id) // Возвращаем в ответе w.Header().Set("X-Request-ID", id) next.ServeHTTP(w, r.WithContext(ctx)) }) } // Извлечение request ID из context func GetRequestID(ctx context.Context) string { if id, ok := ctx.Value(RequestIDKey).(string); ok { return id } return "" }
Использование
gofunc (h *Handler) GetRepo(w http.ResponseWriter, r *http.Request) { requestID := middleware.GetRequestID(r.Context()) h.logger.Info("getting repo", "request_id", requestID) // ... }
9.3 Logging middleware
Зачем нужно логирование запросов
Логирование запросов даёт видимость:
- Какие запросы приходят
- Сколько времени занимает обработка
- Какие ошибки возникают
- Какой клиент отправил запрос
Реализация
gopackage middleware import ( "log/slog" "net/http" "time" ) func Logging(logger *slog.Logger) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() // Оборачиваем ResponseWriter для перехвата статуса wrapped := &statusRecorder{ResponseWriter: w, statusCode: http.StatusOK} next.ServeHTTP(wrapped, r) duration := time.Since(start) logger.Info("request completed", "method", r.Method, "path", r.URL.Path, "status", wrapped.statusCode, "duration", duration.String(), "request_id", GetRequestID(r.Context()), "remote_addr", r.RemoteAddr, ) }) } } // statusRecorder перехватывает WriteHeader для логирования статуса type statusRecorder struct { http.ResponseWriter statusCode int } func (r *statusRecorder) WriteHeader(code int) { r.statusCode = code r.ResponseWriter.WriteHeader(code) }
Что логировать
| Поле | Зачем |
|---|---|
| method | HTTP метод запроса |
| path | Путь запроса |
| status | Код ответа |
| duration | Время обработки |
| request_id | Корреляция с другими логами |
| remote_addr | Адрес клиента |
Что НЕ логировать
- Тело запроса (может содержать PII)
- Заголовки с токенами (Authorization, Cookie)
- Query параметры с секретами
- Пароли, email, телефоны
9.4 Recovery middleware
Зачем нужен Recovery
Panic в хэндлере убьёт весь процесс. Recovery middleware:
- Ловит panic
- Логирует stack trace
- Возвращает 500 клиенту
- Процесс продолжает работать
Реализация
gopackage middleware import ( "log/slog" "net/http" "runtime/debug" ) func Recovery(logger *slog.Logger) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { if rec := recover(); rec != nil { logger.Error("panic recovered", "panic", rec, "stack", string(debug.Stack()), "request_id", GetRequestID(r.Context()), "path", r.URL.Path, ) http.Error(w, "internal server error", http.StatusInternalServerError) } }() next.ServeHTTP(w, r) }) } }
Важно
Recovery должен быть ПЕРВЫМ в цепочке middleware, чтобы поймать panic из любого middleware или хэндлера.
gohandler = Recovery(handler) // первый! handler = RequestID(handler) handler = Logging(handler)
9.5 Timeout middleware
Зачем нужен Timeout
Долгие запросы могут:
- Занять connection pool
- Потребить ресурсы сервера
- Привести к таймауту на клиенте
Timeout middleware ограничивает время обработки.
Реализация
gopackage middleware import ( "context" "net/http" "time" ) func Timeout(d time.Duration) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(r.Context(), d) defer cancel() r = r.WithContext(ctx) done := make(chan struct{}) go func() { next.ServeHTTP(w, r) close(done) }() select { case <-done: // обработчик завершился успешно case <-ctx.Done(): // таймаут http.Error(w, "request timeout", http.StatusGatewayTimeout) } }) } }
Альтернатива: http.TimeoutHandler
Стандартная библиотека предоставляет готовый TimeoutHandler:
gohandler = http.TimeoutHandler(handler, 30*time.Second, "request timeout")
Ограничение: http.TimeoutHandler буферизует ответ, что может быть проблемой для больших ответов или streaming.
Использование
gohandler = Timeout(30 * time.Second)(handler)
ВАЖНО: Timeout middleware должен проверять ctx.Done() в хэндлерах. Хэндлеры должны использовать r.Context() для операций.
9.6 Валидация запросов
Зачем нужна валидация
Валидация защищает от:
- Некорректных данных
- SQL injection (частично)
- Непредвиденного поведения
- Ошибок в бизнес-логике
go-playground/validator
Стандартная библиотека валидации в Go.
Установка:
bashgo get github.com/go-playground/validator/v10
Теги валидации
gotype CreateRepoRequest struct { Name string `json:"name" validate:"required,min=3,max=100"` OwnerID string `json:"owner_id" validate:"required,uuid"` IsPrivate bool `json:"is_private"` Tags []string `json:"tags" validate:"omitempty,max=10,dive,max=50"` }
| Тег | Назначение |
|---|---|
| required | Поле обязательно |
| min, max | Минимальная/максимальная длина |
| Формат email | |
| uuid | Формат UUID |
| url | Формат URL |
| omitempty | Пропустить если пусто |
| dive | Валидировать элементы массива |
| oneof | Одно из значений |
Использование validator
goimport "github.com/go-playground/validator/v10" var validate *validator.Validate func init() { validate = validator.New() } func (h *Handler) CreateRepo(w http.ResponseWriter, r *http.Request) { var req CreateRepoRequest if err := decodeJSON(r, &req); err != nil { h.writeError(w, http.StatusBadRequest, "invalid request body") return } // Валидация if err := validate.Struct(req); err != nil { validationErrors := formatValidationErrors(err) h.writeJSON(w, http.StatusBadRequest, ErrorResponse{ Code: "VALIDATION_ERROR", Message: "validation failed", Errors: validationErrors, }) return } // Обработка валидного запроса }
Форматирование ошибок валидации
gofunc formatValidationErrors(err error) []string { var errors []string var validationErrors validator.ValidationErrors if errors.As(err, &validationErrors) { for _, e := range validationErrors { errors = append(errors, fmt.Sprintf( "field '%s' failed validation: %s", e.Field(), e.Tag(), )) } } return errors }
ErrorResponse с деталями
gotype ErrorResponse struct { Code string `json:"code"` Message string `json:"message"` Errors []string `json:"errors,omitempty"` } // Пример ответа { "code": "VALIDATION_ERROR", "message": "validation failed", "errors": [ "field 'Name' failed validation: required", "field 'OwnerID' failed validation: uuid" ] }
Валидация на уровне usecase
Валидация тегов - только формат. Бизнес-правила в usecase:
gofunc (uc *CreateRepoUseCase) Execute(ctx context.Context, req CreateRepoRequest) (*models.Repo, error) { // Валидация формата (в handler) // Валидация бизнес-правил (здесь) if req.Name == "admin" { return nil, &ValidationError{ Field: "name", Message: "name 'admin' is reserved", } } // Проверка существования владельца user, err := uc.userService.GetUser(ctx, req.OwnerID) if err != nil { return nil, fmt.Errorf("get user: %w", err) } if user == nil { return nil, &ValidationError{ Field: "owner_id", Message: "user not found", } } // Создание репо repo := req.ToDomain() if err := uc.repoService.Create(ctx, repo); err != nil { return nil, fmt.Errorf("create repo: %w", err) } return repo, nil }
9.7 Композиция middleware
Функция для композиции
gofunc Chain(handler http.Handler, middlewares ...func(http.Handler) http.Handler) http.Handler { // Применяем в обратном порядке чтобы первый middleware был внешним for i := len(middlewares) - 1; i >= 0; i-- { handler = middlewares[i](handler) } return handler }
Использование
gohandler := Chain( mux, middleware.Recovery(logger), middleware.RequestID, middleware.Logging(logger), middleware.Timeout(30*time.Second), )
Правильный порядок middleware
text1. Recovery - ловит panic из всех остальных 2. RequestID - добавляет ID для корреляции 3. Logging - логирует запрос (видит request ID) 4. Timeout - ограничивает время обработки 5. Auth - проверяет аутентификацию 6. RateLimit - ограничивает частоту запросов 7. Handler - бизнес-логика
Пример из шаблона
go// internal/app/server.go func BuildRouter(h *v1.Handler, logger *slog.Logger) http.Handler { mux := http.NewServeMux() v1.RegisterRoutes(mux, h) // Применяем middleware в правильном порядке handler := middleware.Recovery(logger)(mux) handler = middleware.RequestID(handler) handler = middleware.Logging(logger)(handler) handler = middleware.Timeout(30*time.Second)(handler) return handler }
PII masking
Что такое PII
PII (Personally Identifiable Information) - персональные данные:
- Телефон
- ФИО
- Адрес
- Паспортные данные
Зачем маскировать
Логи могут быть доступны широкому кругу лиц. Утечка логов = утечка PII. Маскирование защищает данные.
Реализация маскирования
gopackage middleware import ( "regexp" ) var ( emailRegex = regexp.MustCompile(`[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}`) phoneRegex = regexp.MustCompile(`\+?[0-9]{10,15}`) ) func MaskPII(s string) string { s = emailRegex.ReplaceAllString(s, "***@***.***") s = phoneRegex.ReplaceAllString(s, "***") return s }
Использование в логировании
gologger.Info("user action", "email", MaskPII(user.Email), "action", "login", ) // Результат: email=***@***.***
Привязка к шаблону проекта
Где middleware в шаблоне
textinternal/handlers/http/middleware/ ├── request_id.go # Request ID middleware ├── logging.go # Логирование запросов ├── recovery.go # Перехват panic ├── timeout.go # Таймаут запросов ├── pii.go # Маскирование PII └── prometheus.go # Метрики запросов
Что изучаем на примере шаблона
- Каждый middleware в отдельном файле
- Middleware принимают logger как зависимость
- Request ID передаётся через context
- statusRecorder для перехвата статуса
- Правильный порядок применения
Задание: изучите internal/handlers/http/middleware/
- Какие middleware реализованы?
- В каком порядке они применяются?
- Как передаётся request ID?
- Как логируются запросы?
💱 Компромиссы
- Кастомный
Timeoutvshttp.TimeoutHandler: свой middleware не буферизует ответ и дружит со стримингом, но после отмены context горутина хендлера продолжает работать; готовыйhttp.TimeoutHandlerпроще, но буферизует весь ответ — плохо для больших ответов и streaming. - Валидация тегами vs бизнес-правила в usecase: теги
min=3,uuidдекларативны и проверяют формат бесплатно, но не видят контекста; правила вроде «name 'admin' зарезервирован» требуют похода в БД и живут в usecase. - Клиентский
X-Request-IDvs свойuuid: доверие к заголовку клиента связывает логи всех сервисов в одну трассу, но кривой или злой клиент может подсунуть мусор в логи; генерацияuuid.NewString()безопасна, но трасса обрывается на входе в сервис. - Recovery vs падение пода:
recover()возвращает клиенту 500 и держит процесс живым, но превращает баг в тихую ошибку, которую видно только по stack trace в логах; без recovery panic роняет весь под — отказ каскадом, зато проблема видна сразу. - Полные логи vs маскирование PII: точный email в логе ускоряет поиск
инцидента, но утечка логов становится утечкой персональных данных;
MaskPIIзащищает пользователя, но искать по исходным значениям уже нельзя.
Практическое задание
Задание 1: Request ID middleware
Реализуйте middleware который добавляет request ID:
gofunc RequestID(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { id := r.Header.Get("X-Request-ID") if id == "" { id = uuid.NewString() } ctx := context.WithValue(r.Context(), RequestIDKey, id) w.Header().Set("X-Request-ID", id) next.ServeHTTP(w, r.WithContext(ctx)) }) }
Задание 2: Logging middleware
Реализуйте логирование запросов:
gofunc Logging(logger *slog.Logger) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() wrapped := &statusRecorder{ResponseWriter: w, statusCode: 200} next.ServeHTTP(wrapped, r) logger.Info("request completed", "method", r.Method, "path", r.URL.Path, "status", wrapped.statusCode, "duration", time.Since(start).String(), "request_id", GetRequestID(r.Context()), ) }) } }
Задание 3: Recovery middleware
Реализуйте перехват panic:
gofunc Recovery(logger *slog.Logger) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { if rec := recover(); rec != nil { logger.Error("panic recovered", "panic", rec, "stack", string(debug.Stack()), ) http.Error(w, "internal server error", 500) } }() next.ServeHTTP(w, r) }) } }
Задание 4: Timeout middleware
Реализуйте ограничение времени обработки:
gofunc Timeout(d time.Duration) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(r.Context(), d) defer cancel() r = r.WithContext(ctx) done := make(chan struct{}) go func() { next.ServeHTTP(w, r) close(done) }() select { case <-done: case <-ctx.Done(): http.Error(w, "request timeout", 504) } }) } }
Задание 5: Валидация запросов
Реализуйте валидацию CreateRepoRequest:
gotype CreateRepoRequest struct { Name string `json:"name" validate:"required,min=3,max=100"` OwnerID string `json:"owner_id" validate:"required,uuid"` IsPrivate bool `json:"is_private"` } func (h *Handler) CreateRepo(w http.ResponseWriter, r *http.Request) { var req CreateRepoRequest if err := decodeJSON(r, &req); err != nil { h.writeError(w, 400, "invalid request body") return } if err := validate.Struct(req); err != nil { h.writeValidationError(w, err) return } // обработка }
Задание 6: Композиция middleware
Соберите цепочку middleware:
gofunc BuildRouter(h *Handler, logger *slog.Logger) http.Handler { mux := http.NewServeMux() RegisterRoutes(mux, h) handler := Recovery(logger)(mux) handler = RequestID(handler) handler = Logging(logger)(handler) handler = Timeout(30*time.Second)(handler) return handler }
Задание 7: Тесты для middleware
Напишите тесты:
gofunc TestRequestID(t *testing.T) { handler := RequestID(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { id := GetRequestID(r.Context()) assert.NotEmpty(t, id) })) req := httptest.NewRequest("GET", "/", nil) w := httptest.NewRecorder() handler.ServeHTTP(w, req) assert.NotEmpty(t, w.Header().Get("X-Request-ID")) }
Quiz "Проверь себя"
✅ Итоги модуля
Студент должен:
- Понимать middleware-паттерн
- Реализовывать Request ID, Logging, Recovery, Timeout middleware
- Валидировать запросы с go-playground/validator
- Компонировать middleware в правильном порядке
- Маскировать PII в логах
- Тестировать middleware с httptest
- Ответить на все вопросы Quiz
🔗 Мостик к Модулю 10
Следующий модуль: Модуль 10. Работа с PostgreSQL: основы - database/sql vs pgx, connection pooling, SQL-запросы, prepared statements, SQL injection prevention. Привязка к internal/adapters/postgres/ шаблона.