Tratamento de erros HTTP de modo menos oneroso + RFC7807
Visão Geral
Ao gerar uma api http na linguagem Go, o aspecto mais tedioso reside no tratamento de erros. Um código representativo é o seguinte.
1func(w http.ResponseWriter, r *http.Request) {
2 err := doSomething()
3 if err != nil {
4 http.Error(w, err.Error(), http.StatusInternalServerError)
5 log.Printf("error occurred: %v", err)
6 return
7 }
8 // ...
9}
Se o número de APIs for reduzido, escrever desta forma não causará transtornos significativos. Contudo, à medida que o número de APIs aumenta e a lógica interna se torna mais complexa, os seguintes três pontos tornam-se problemáticos:
- Retorno de códigos de erro apropriados
- Grande volume de escrita de logs de resultado
- Transmissão de mensagens de erro explícitas
Conteúdo Principal
Retorno de Códigos de Erro Apropriados
Naturalmente, o item 1, o retorno de códigos de erro apropriados, constitui uma queixa pessoal minha. Embora desenvolvedores experientes encontrem e insiram os códigos adequados consistentemente, desenvolvedores menos experientes, como eu, podem enfrentar dificuldades em aplicar códigos de erro adequados de maneira metódica à medida que a lógica se complexifica e as iterações se multiplicam. Existem diversas abordagens para isso, sendo a mais representativa a elaboração de código que retorna erros apropriados após o projeto prévio do fluxo da lógica da API. Faça exatamente isso
Entretanto, este não parece ser o método otimizado para um desenvolvedor humano auxiliado por um IDE (ou Language Server). Além disso, dado que a própria REST API visa maximizar a utilização do significado contido no código de erro, uma abordagem alternativa pode ser proposta: criar uma nova implementação da interface de erro (error
) denominada HttpError
, configurada para armazenar StatusCode
e Message
. E fornecemos a seguinte função auxiliar:
1err := httperror.BadRequest("wrong format")
A função auxiliar BadRequest
retornará um HttpError
com StatusCode
definido como 400 e Message
definido como o valor passado como argumento. Além disso, funções auxiliares como NotImplement
, ServiceUnavailable
, Unauthorized
, PaymentRequired
, etc., certamente poderão ser consultadas e adicionadas por meio da funcionalidade de autocompletar. Isso é mais rápido do que verificar um documento de projeto preparado a cada vez e mais robusto do que inserir códigos de erro numericamente repetidamente. Os constantes de http.StatusCode
contêm todos eles? Silêncio
Grande Volume de Escrita de Logs de Resultado
É imperativo registrar logs quando ocorre um erro. Ao registrar logs sobre se uma API foi invocada e se a requisição foi bem-sucedida ou falhou, registrar logs em todos os pontos de término previstos desde o início aumenta a quantidade de código a ser escrito. Isso pode ser centralmente gerenciado ao envolver o próprio handler.
A seguir, apresentamos um exemplo de como envolver o router chi
.
1package chiwrap
2
3import (
4 "errors"
5 "net/http"
6
7 "github.com/go-chi/chi/v5"
8
9 "github.com/gosuda/httpwrap/httperror"
10)
11
12type Router struct {
13 router chi.Router
14 errCallback func(err error)
15}
16
17func NewRouter(errCallback func(err error)) *Router {
18 if errCallback == nil {
19 errCallback = func(err error) {}
20 }
21 return &Router{
22 router: chi.NewRouter(),
23 errCallback: errCallback,
24 }
25}
26
27type HandlerFunc func(writer http.ResponseWriter, request *http.Request) error
28
29func (r *Router) Get(pattern string, handler HandlerFunc) {
30 r.router.Get(pattern, func(writer http.ResponseWriter, request *http.Request) {
31 if err := handler(writer, request); err != nil {
32 he := &httperror.HttpError{}
33 switch errors.As(err, &he) {
34 case true:
35 http.Error(writer, he.Message, he.Code)
36 case false:
37 http.Error(writer, err.Error(), http.StatusInternalServerError)
38 }
39 r.errCallback(err)
40 }
41 })
42}
A estrutura struct Router é configurada para possuir internamente um chi.Router
, utilizando assim as funcionalidades deste último de forma integral. Ao examinar o método Get
, verifica-se se foi retornado o objeto estrutural HttpError
produzido pela função auxiliar sugerida anteriormente, retornando-o adequadamente, ou, no caso de ser um error
genérico, ele é repassado de forma unificada à função de callback de erro. Este callback é recebido via construtor.
O código a seguir demonstra a utilização deste pacote.
1package main
2
3import (
4 "bytes"
5 "context"
6 "errors"
7 "io"
8 "log"
9 "net/http"
10
11 "github.com/gosuda/httpwrap/httperror"
12 "github.com/gosuda/httpwrap/wrapper/chiwrap"
13)
14
15func main() {
16 ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
17 defer cancel()
18
19 r := chiwrap.NewRouter(func(err error) {
20 log.Printf("Router log test: Error occured: %v", err)
21 })
22 r.Get("/echo", func(writer http.ResponseWriter, request *http.Request) error {
23 name := request.URL.Query().Get("name")
24 if name == "" {
25 return httperror.BadRequest("name is required")
26 }
27
28 writer.Write([]byte("Hello " + name))
29 return nil
30 })
31
32 svr := http.Server{
33 Addr: ":8080",
34 Handler: r,
35 }
36 go func() {
37 if err := svr.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
38 log.Fatalf("Failed to start server: %v", err)
39 }
40 }()
41
42 <-ctx.Done()
43 svr.Shutdown(context.Background())
44}
O que acham? Ao simplesmente criar e retornar um HttpError
através de uma função auxiliar, é possível tratar a resposta no escopo superior com o código de erro e a mensagem adequados, enquanto se registra um callback para que logs apropriados sejam mantidos para cada serviço implementado. Adicionalmente, se necessário, é possível expandir essa funcionalidade para permitir um registro detalhado utilizando, por exemplo, um RequestID
.
Transmissão de Mensagens de Erro Explícitas
Para este fim, existe o documento RFC7807. O RFC7807 define e utiliza primariamente os seguintes elementos:
type
: Um URI que identifica o tipo de erro. Geralmente é um documento que descreve o erro.title
: Uma descrição concisa sobre qual é o erro.status
: Idêntico ao HTTP Status Code.detail
: Uma descrição detalhada e legível por humanos sobre o erro em questão.instance
: O URI onde o erro ocorreu. Por exemplo, se o erro ocorreu emGET /user/info
,/user/info
será o seu valor.extensions
: Elementos secundários configurados como um Objeto JSON para descrever o erro.- Por exemplo, no caso de
BadRequest
, a entrada do usuário pode ser incluída. - Ou, no caso de
TooManyRequest
, pode-se incluir o momento da última requisição.
- Por exemplo, no caso de
Para facilitar o uso disto, criamos um novo arquivo no pacote httperror
, no mesmo nível do HttpError
, para gerar a estrutura RFC7807Error
utilizando o padrão de encadeamento de métodos (method chaining).
1func NewRFC7807Error(status int, title, detail string) *RFC7807Error {
2 return &RFC7807Error{
3 Type: "about:blank", // Tipo padrão conforme RFC7807
4 Title: title,
5 Status: status,
6 Detail: detail,
7 }
8}
9
10func BadRequestProblem(detail string, title ...string) *RFC7807Error {
11 t := "Bad Request"
12 if len(title) > 0 && title[0] != "" {
13 t = title[0]
14 }
15 return NewRFC7807Error(http.StatusBadRequest, t, detail)
16}
17
18func (p *RFC7807Error) WithType(typeURI string) *RFC7807Error { ... }
19func (p *RFC7807Error) WithInstance(instance string) *RFC7807Error { ... }
20func (p *RFC7807Error) WithExtension(key string, value interface{}) *RFC7807Error { ... }
O valor "about:blank"
para Type
é o padrão, significando uma página inexistente. A seguir, apresentamos um exemplo de geração de erro para uma requisição inválida.
1problem := httperror.BadRequestProblem("invalid user id format", "Bad User Input")
2
3problem = problem.WithType("https://example.com/errors/validation")
4 .WithInstance("/api/users/abc")
5 .WithExtension("invalid_field", "user_id")
6 .WithExtension("expected_format", "numeric")
É possível gerar uma mensagem de erro estruturada para o usuário através de um simples encadeamento de métodos. Além disso, para aproveitar o router centralizado previamente elaborado, o seguinte método pode ser suportado.
1func (p *RFC7807Error) ToHttpError() *HttpError {
2 jsonBytes, err := json.Marshal(p)
3 if err != nil {
4 // Se a serialização falhar, retorna apenas o detalhe
5 return New(p.Status, p.Detail)
6 }
7 return New(p.Status, string(jsonBytes))
8}
Ao utilizar isto diretamente, a modificação do exemplo anterior resulta no seguinte.
1package main
2
3import (
4 "bytes"
5 "context"
6 "errors"
7 "io"
8 "log"
9 "net/http"
10
11 "github.com/gosuda/httpwrap/httperror"
12 "github.com/gosuda/httpwrap/wrapper/chiwrap"
13)
14
15func main() {
16 ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
17 defer cancel()
18
19 r := chiwrap.NewRouter(func(err error) {
20 log.Printf("Router log test: Error occured: %v", err)
21 })
22 r.Get("/echo", func(writer http.ResponseWriter, request *http.Request) error {
23 name := request.URL.Query().Get("name")
24 if name == "" {
25 return httperror.BadRequestProblem("name is required", "Bad User Input").
26 WithType("https://example.com/errors/validation").
27 WithInstance("/api/echo").
28 WithExtension("invalid_field", "name").
29 WithExtension("expected_format", "string").
30 WithExtension("actual_value", name).
31 ToHttpError()
32 }
33
34 writer.Write([]byte("Hello " + name))
35 return nil
36 })
37
38 svr := http.Server{
39 Addr: ":8080",
40 Handler: r,
41 }
42 go func() {
43 if err := svr.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
44 log.Fatalf("Failed to start server: %v", err)
45 }
46 }()
47
48 <-ctx.Done()
49 svr.Shutdown(context.Background())
50}
Conclusão
Ao processar erros utilizando um router centralizado desta maneira, é possível reduzir a carga associada à verificação contínua dos códigos de erro e à redação de mensagens de erro adequadas. Além disso, ao empregar o RFC7807 para fornecer mensagens de erro estruturadas, pode-se auxiliar o cliente na compreensão e tratamento dos erros. Por meio dessas metodologias, o tratamento de erros em APIs HTTP desenvolvidas na linguagem Go pode se tornar mais simplificado e consistente.
O código deste artigo pode ser verificado no repositório gosuda/httpwrap.