Как стать автором
Поиск
Написать публикацию
Обновить

Упрощаем написание HTTP обработчиков на Golang

Время на прочтение6 мин
Количество просмотров11K

При обработке входящего HTTP запроса требуется выполнить большое количество действий, таких как:


  • Логирование входящего HTTP запроса
  • Проверка на допустимость HTTP метода
  • Выполнение аутентификации (basic, MS AD, ...)
  • Проверка валидности token (при необходимости)
  • Считывание тела (body) входящего запроса
  • Считывание заголовка (header) входящего запроса
  • Собственно обработка запроса и формирование ответа
  • Установка HSTS Strict-Transport-Security
  • Установка Content-Type для исходящего ответа (response)
  • Логирование исходящего HTTP ответа
  • Запись заголовка (header) исходящего ответа
  • Запись тела исходящего ответа
  • Обработка и логирование ошибок
  • Обработка defer recovery для восстановления после возможной panic

Большинство этих действий являются типовыми и не зависят от типа запроса и его обработки.
Для продуктивной эксплуатации на каждое действие необходимо предусмотреть обработку ошибок и логирование.


Повторять все это в каждом HTTP обработчике крайне неэффективно.


Даже если вынести весь код в отдельные подфункции, все равно получается примерно по 80-100 строк кода на каждый HTTP обработчик без учета собственно обработки запроса и формирования ответа.


Ниже описан используемый мной подход, по упрощению написание HTTP обработчиков на Golang без использования кодогенераторов и сторонних библиотек.


Этот подход реализован в шаблоне backend сервера на Golang


Этот подход был скомпонован из различных источников и рекомендаций в интернете.
У любого разработчика, естественно, есть свои устоявшиеся приемы и наработки — буду рад, если кто-то поделится своими подходами.


Подход к организации HTTP обработчиков


На следующем рисунке показана упрощенная UML диаграмма выполнения HTTP обработчика на примере некоего EchoHandler.


Идея, заложенная в предлагаемый подход, достаточно простая:


  • На верхнем уровне необходимо внедрить defer фукнцию для восстановления после паники. На UML диаграмме — это анонимная функция RecoverWrap.func1, показаная красным цветом.
  • Весь типовой код необходимо вынести в отдельный обработчик. Этот обработчик встраивается в наш HTTP handler. На UML диаграмме это функция Process — показана синим цветом.
  • Собственно код функциональной обработки запроса и формирования ответа вынесен в анонимную функцию в нашем HTTP handler. На UML диаграмме это функция EchoHandler.func1 — показана зеленым цветом.

http_handler


Пример кода


Полный код приведен в репозитории.


Ниже пример кода для HTTP обработчика EchoHandler, который "зеркалит" вход на выход.


При регистрации обработчика в роутере, регистрируется не собственно обработчик EchoHandler, а анонимная функция обработки паники (она возвращается функцией RecoverWrap), которая уже вызывает наш обработчик EchoHandler.


router.HandleFunc("/echo", service.RecoverWrap(http.HandlerFunc(service.EchoHandler))).Methods("GET")

Текст функции RecoverWrap для регистрации анонимной функции обработки паники.
После объявления defer func() запускается собственно наш обработчик EchoHandler.


func (s *Service) RecoverWrap(handlerFunc http.HandlerFunc) http.HandlerFunc {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // объявляем функцию восстановления после паники
        defer func() {
            var myerr error
            r := recover()
            if r != nil {
                msg := "HTTP Handler recover from panic"
                switch t := r.(type) {
                case string:
                    myerr = myerror.New("8888", msg, t)
                case error:
                    myerr = myerror.WithCause("8888", msg, t)
                default:
                    myerr = myerror.New("8888", msg)
                }
                // расширенное логирование ошибки в контексте HTTP
                s.processError(myerr, w, http.StatusInternalServerError, 0)
            }
        }()

        // вызываем обработчик
        if handlerFunc != nil {
            handlerFunc(w, r)
        }
    })
}

Собственно, код обработчика EchoHandler. В общем случае, он запускает функцию типовой обработки HTTP запроса Process и передается ей в качестве параметра анонимную функцию обработки.


func (s *Service) EchoHandler(w http.ResponseWriter, r *http.Request) {
    // Запускаем универсальный обработчик HTTP запросов 
    _ = s.Process("POST", w, r, func(requestBuf []byte, reqID uint64) ([]byte, Header, int, error) {
        header := Header{} // заголовок ответа

        // Считаем параметры из заголовка входящего запроса и поместим их в заголовок ответа
        for key := range r.Header {
            header[key] = r.Header.Get(key)
        }
        // возвращаем буфер запроса в качестве ответа, заголовок ответа и статус 
        return requestBuf, header, http.StatusOK, nil
    })
}

Функция типовой обработки HTTP запроса Process. Входные параметры функции:


  • method string — HTTP метод обработчика используется для проверки входящего HTTP запроса
  • w http.ResponseWriter, r *http.Request — стандартные переменные для обработчика
  • fn func(requestBuf []byte, reqID uint64) ([]byte, Header, int, error) — собственно функция обработки, на вход она принимает буфер входящего запроса и уникальный номер HTTP request (для целей логирования), возвращает подготовленный буфер исходящего ответа, header исходящего ответа, HTTP статус и ошибку.

func (s *Service) Process(method string, w http.ResponseWriter, r *http.Request, fn func(requestBuf []byte, reqID uint64) ([]byte, Header, int, error)) error {
    var myerr error

    // Получить уникальный номер HTTP запроса
    reqID := GetNextRequestID()

    // Логируем входящий HTTP запрос
    if s.logger != nil {
        _ = s.logger.LogHTTPInRequest(s.сtx, r, reqID) // При сбое HTTP логирования, делаем системное логирование, но работу не останавливаем
        mylog.PrintfDebugMsg("Logging HTTP in request: reqID", reqID)
    }

    // Проверим разрешенный метод
    mylog.PrintfDebugMsg("Check allowed HTTP method: reqID, request.Method, method", reqID, r.Method, method)
    if r.Method != method {
        myerr = myerror.New("8000", "HTTP method is not allowed: reqID, request.Method, method", reqID, r.Method, method)
        mylog.PrintfErrorInfo(myerr)
        return myerr
    }

    // Если включен режим аутентификации без использования JWT токена, то проверять пользователя и пароль каждый раз
    mylog.PrintfDebugMsg("Check authentication method: reqID, AuthType", reqID, s.cfg.AuthType)
    if (s.cfg.AuthType == "INTERNAL" || s.cfg.AuthType == "MSAD") && !s.cfg.UseJWT {
        mylog.PrintfDebugMsg("JWT is of. Need Authentication: reqID", reqID)

        // Считаем из заголовка HTTP Basic Authentication
        username, password, ok := r.BasicAuth()
        if !ok {
            myerr := myerror.New("8004", "Header 'Authorization' is not set")
            mylog.PrintfErrorInfo(myerr)
            return myerr
        }
        mylog.PrintfDebugMsg("Get Authorization header: username", username)

        // Выполняем аутентификацию
        if myerr = s.checkAuthentication(username, password); myerr != nil {
            mylog.PrintfErrorInfo(myerr)
            return myerr
        }
    }

    // Если используем JWT - проверим токен
    if s.cfg.UseJWT {
        mylog.PrintfDebugMsg("JWT is on. Check JSON web token: reqID", reqID)

        // Считаем token из requests cookies
        cookie, err := r.Cookie("token")
        if err != nil {
            myerr := myerror.WithCause("8005", "JWT token does not present in Cookie. You have to authorize first.", err)
            mylog.PrintfErrorInfo(myerr)
            return myerr
        }

        // Проверим JWT в token
        if myerr = myjwt.CheckJWTFromCookie(cookie, s.cfg.JwtKey); myerr != nil {
            mylog.PrintfErrorInfo(myerr)
            return myerr
        }
    }

    // Считаем тело запроса
    mylog.PrintfDebugMsg("Reading request body: reqID", reqID)
    requestBuf, err := ioutil.ReadAll(r.Body)
    if err != nil {
        myerr = myerror.WithCause("8001", "Failed to read HTTP body: reqID", err, reqID)
        mylog.PrintfErrorInfo(myerr)
        return myerr
    }
    mylog.PrintfDebugMsg("Read request body: reqID, len(body)", reqID, len(requestBuf))

    // вызываем обработчик
    mylog.PrintfDebugMsg("Calling external function handler: reqID, function", reqID, fn)
    responseBuf, header, status, myerr := fn(requestBuf, reqID)
    if myerr != nil {
        mylog.PrintfErrorInfo(myerr)
        return myerr
    }

    // use HSTS Strict-Transport-Security
    if s.cfg.UseHSTS {
        w.Header().Add("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
    }

    // Логируем ответ в файл
    if s.logger != nil {
        mylog.PrintfDebugMsg("Logging HTTP out response: reqID", reqID)
        _ = s.logger.LogHTTPOutResponse(s.сtx, header, responseBuf, status, reqID) // При сбое HTTP логирования, делаем системное логирование, но работу не останавливаем
    }

    // Записываем заголовок ответа
    mylog.PrintfDebugMsg("Set HTTP response headers: reqID", reqID)
    if header != nil {
        for key, h := range header {
            w.Header().Set(key, h)
        }
    }

    // Записываем HTTP статус ответа
    mylog.PrintfDebugMsg("Set HTTP response status: reqID, Status", reqID, http.StatusText(status))
    w.WriteHeader(status)

    // Записываем тело ответа
    if responseBuf != nil && len(responseBuf) > 0 {
        mylog.PrintfDebugMsg("Writing HTTP response body: reqID, len(body)", reqID, len(responseBuf))
        respWrittenLen, err := w.Write(responseBuf)
        if err != nil {
            myerr = myerror.WithCause("8002", "Failed to write HTTP repsonse: reqID", err)
            mylog.PrintfErrorInfo(myerr)
            return myerr
        }
        mylog.PrintfDebugMsg("Written HTTP response: reqID, len(body)", reqID, respWrittenLen)
    }

    return nil
}
Теги:
Хабы:
Всего голосов 10: ↑4 и ↓6+3
Комментарии11

Публикации

Ближайшие события