При обработке входящего 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 обработчика 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
}