Опубликовано: 24 июля 2022 г. 1:39 | Автор: echodiv | Категория: Разработка | 👁 588

Пишем middleware для стандартных net/http хэндлеров

Middleware или промежуточное ПО - распространенный шаблон, помогающий вынести во внешний слой общую логику для разных реализаций одного интерфейса. Думаю такое определение имеет право на жизнь.

Представим, что нам нужно написать веб-сервер на стандартном Go пакете net/http. Как же можно реализовать middleware?  

Напишем самый простой веб-сервер на Go

 

package main
 
import (
    "io"
    "log"
    "net/http"
)
 
func sayOK(w http.ResponseWriter, r *http.Request) {
    io.WriteString(w, "OK")
}
 
func main() {
    http.HandleFunc("/", sayOK)
 
    if err := http.ListenAndServe(":8080", nil); err != nil {
        log.Fatal(err)
    }
}

 

Сервер поднят на порту 8080, на входящий запрос мы просто отвечаем OK.

Допустим нам нужно реализовать авторизацию или проверить метод запроса перед передачей в хендлер. Суть одна, сделать какие-то действия с запросом до выполнения основного кода.

Для этого сделаем простую реализацию middleware. На вход она будет получать стандартную http.HandlerFunc, и отдавать функцию того же типа. В этом смысл, http.HandleFunc не должен заметить, что что-то поменялось.

 

func middlewareBefore(f http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        if r.Method != "GET" {
            w.WriteHeader(http.StatusMethodNotAllowed)
            return
        }
        f(w, r)
    }
}

 

Тут, чисто для примера, мы смотрим какой у нас запрос, если это не GET то вернём ошибку. Тут может быть авторизация, логирование, подсчёт времени выполнения. В общем всё, на что хватит фантазии, ресурсов и денег компании.

Модифицируем инициализацию веб-сервера и теперь он пропустит только GET запрос.

 

func main() {
    http.HandleFunc("/", middlewareBefore(sayOK))
 
    if err := http.ListenAndServe(":8080", nil); err != nil {
        log.Fatal(err)
    }
}

Но что нам делать если мы хотим залогировать ответ, статус код? Либо модифицировать ответ уже после выхода из нашей sayOK?

Нужно подменить http.ResponseWriter. Благо интерфейс достаточно простой, имеет всего три метода.

 

type FakeWriter struct {
    code int
    data []byte
}
 
func (f *FakeWriter) WriteHeader(code int) {
    f.code = code
}
 
func (f *FakeWriter) Header() http.Header {
    return map[string][]string{}
}
 
func (f *FakeWriter) Write(input []byte) (int, error) {
    f.data = input
    return len(f.data), nil
 
}

 

Тут простая реализация, но не обязательно ограничиваться этим. Подсовываем нашу обманку в основной код нашей ручки (хендлера) и после выхода из неё мы можем делать с ответом всё, что захотим. Например, дописать пару слов.

 

func middlewareAfter(f http.HandlerFunc) http.HandlerFunc {
    localWriter := new(FakeWriter)
    return func(w http.ResponseWriter, r *http.Request) {
        f(localWriter, r)
 
        if localWriter.code != 0 {
            w.WriteHeader(localWriter.code)
        }
 
        io.WriteString(w, string(localWriter.data)+" All right!")
    }
}

Таже модифицируем инициализацию сервера и меняется логика работы. Теперь в ответе есть на два слова больше.

func main() {
    http.HandleFunc("/", middlewareAfter(sayOK))
 
    if err := http.ListenAndServe(":8080", nil); err != nil {
        log.Fatal(err)
    }
}

Но это еще не всё, так как наши middlware не меняют сигнатур функций, мы можем их комбинировать.

func main() {
    http.HandleFunc("/", middlewareBefore(middlewareAfter(sayOK)))
 
    if err := http.ListenAndServe(":8080", nil); err != nil {
        log.Fatal(err)
    }
}

Теперь нам нужен только метод GET, и ответ содержит все три слова.