---
id: 982178b2b9c8c091685bdc7fbf942c43
author: snowmerak
title: Tratamento de erros HTTP de forma menos trabalhosa + RFC7807
description: Está cansado de lidar com erros na API em Go? Trate-os de forma mais simples e clara com a RFC 7807.
language: pt
date: 2025-05-19T20:56:37.945722346Z
path: /blog/posts/handling-http-errors-more-conveniently-+-rfc7807-z862df968
category: api
---

## Visão geral

Ao criar uma API HTTP em Go, a parte mais trabalhosa é o tratamento de erros. Um exemplo típico é o seguinte código.

```go
func(w http.ResponseWriter, r *http.Request) {
    err := doSomething()
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        log.Printf("error occurred: %v", err)
        return
    }
    // ...
}
```

Se a quantidade de APIs for pequena, escrever dessa forma não causará grandes inconveniências. No entanto, à medida que o número de APIs aumenta e a lógica interna se torna mais complexa, três aspectos passam a incomodar.

1. Retorno de códigos de erro apropriados
2. Quantidade excessiva de código para registro de logs
3. Envio de mensagens de erro claras

## Desenvolvimento

### Retorno de códigos de erro apropriados

Na verdade, o primeiro ponto, o retorno de códigos de erro apropriados, é uma queixa pessoal minha. Um desenvolvedor experiente encontrará o código adequado e o inserirá corretamente a cada vez, mas eu mesmo, assim como desenvolvedores ainda inexperientes, podemos ter dificuldade em utilizar de forma consistente os códigos de erro adequados à medida que a lógica se torna complexa e o número de chamadas aumenta. Existem diversos métodos para isso, sendo o mais representativo projetar previamente o fluxo lógico da API e, em seguida, escrever o código para retornar os erros apropriados. ~~Faça assim~~

Contudo, essa abordagem não parece ser a mais eficiente para um desenvolvedor humano que se beneficia do IDE (ou do Language Server). Além disso, como a própria REST API aproveita ao máximo o significado contido nos códigos de erro, pode-se propor outra forma. Criaremos uma nova implementação da interface de erro (`error`) chamada `HttpError`, que armazena `StatusCode` e `Message`. Em seguida, fornecemos funções auxiliares como a seguinte.

```go
err := httperror.BadRequest("wrong format")
```

A função auxiliar `BadRequest` retornará um `HttpError` cujo `StatusCode` é 400 e cujo `Message` é definido com o valor recebido como argumento. Além dela, naturalmente, funções auxiliares como `NotImplement`, `ServiceUnavailable`, `Unauthorized`, `PaymentRequired` etc. poderão ser consultadas e adicionadas com o recurso de autocompletar. Isso é mais rápido do que consultar um projeto preparado a cada vez e mais confiável do que digitar manualmente códigos de erro numéricos. ~~Dizem que tudo isso já está nas constantes de `http.StatusCode`? Shh~~

### Quantidade excessiva de código para registro de logs

Ao ocorrer um erro, é natural registrar um log. Ao registrar logs sobre a chamada da API e sobre o sucesso ou a falha da requisição, registrar logs em todos os pontos de saída previstos, desde o início, aumenta a quantidade de código a ser escrito. Ao envolver o próprio handler, é possível gerenciar isso de forma centralizada.

A seguir, um exemplo que envolve o roteador `chi`.

```go
package chiwrap

import (
	"errors"
	"net/http"

	"github.com/go-chi/chi/v5"

	"github.com/gosuda/httpwrap/httperror"
)

type Router struct {
	router      chi.Router
	errCallback func(err error)
}

func NewRouter(errCallback func(err error)) *Router {
	if errCallback == nil {
		errCallback = func(err error) {}
	}
	return &Router{
		router:      chi.NewRouter(),
		errCallback: errCallback,
	}
}

type HandlerFunc func(writer http.ResponseWriter, request *http.Request) error

func (r *Router) Get(pattern string, handler HandlerFunc) {
	r.router.Get(pattern, func(writer http.ResponseWriter, request *http.Request) {
		if err := handler(writer, request); err != nil {
			he := &httperror.HttpError{}
			switch errors.As(err, &he) {
			case true:
				http.Error(writer, he.Message, he.Code)
			case false:
				http.Error(writer, err.Error(), http.StatusInternalServerError)
			}
			r.errCallback(err)
		}
	})
}
```

A estrutura do roteador contém um `chi.Router` internamente, de modo que os recursos do `chi.Router` podem ser utilizados diretamente. Observando o método `Get`, ele verifica se foi retornado um `HttpError`, como as funções auxiliares propostas acima, respondendo de forma apropriada; caso contrário, em todos os casos de `error`, a função de callback de erro é chamada. Esse callback é recebido pelo construtor.

A seguir, o código escrito utilizando este pacote.

```go
package main

import (
	"bytes"
	"context"
	"errors"
	"io"
	"log"
	"net/http"

	"github.com/gosuda/httpwrap/httperror"
	"github.com/gosuda/httpwrap/wrapper/chiwrap"
)

func main() {
    ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
    defer cancel()

	r := chiwrap.NewRouter(func(err error) {
		log.Printf("Router log test: Error occured: %v", err)
	})
	r.Get("/echo", func(writer http.ResponseWriter, request *http.Request) error {
		name := request.URL.Query().Get("name")
		if name == "" {
			return httperror.BadRequest("name is required")
		}

		writer.Write([]byte("Hello " + name))
		return nil
	})

	svr := http.Server{
		Addr:    ":8080",
		Handler: r,
	}
	go func() {
		if err := svr.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
			log.Fatalf("Failed to start server: %v", err)
		}
	}()

    <-ctx.Done()
    svr.Shutdown(context.Background())
}
```

E então? Basta criar apenas o `HttpError` como função auxiliar e retorná-lo: o escopo superior responde com o código de erro e a mensagem apropriados, e cada serviço de implementação pode registrar os logs adequados, registrando um callback para tratá-los. Se necessário, pode-se ampliar isso para permitir registros detalhados utilizando, por exemplo, um `RequestID`.

### Envio de mensagens de erro claras

Para isso, existe o documento RFC7807. A RFC7807 define e utiliza principalmente os seguintes elementos.

- `type`: URI que identifica o tipo de erro. Geralmente é um documento que descreve o erro.
- `title`: Uma descrição curta sobre qual erro ocorreu.
- `status`: Idêntico ao HTTP Status Code.
- `detail`: Uma explicação detalhada do erro, legível por seres humanos.
- `instance`: URI em que o erro ocorreu. Por exemplo, se o erro ocorreu em `GET /user/info`, o valor será `/user/info`.
- `extensions`: Elemento secundário para descrever o erro, composto como um objeto JSON.
  - Por exemplo, no caso de `BadRequest`, pode conter a entrada do usuário.
  - Ou, no caso de `TooManyRequest`, pode conter o momento da requisição mais recente.

Para utilizar isso de forma simples, criamos um novo arquivo no pacote `httperror`, no mesmo local de `HttpError`, definimos a estrutura `RFC7807Error` e permitimos sua criação por meio do padrão de encadeamento de métodos.

```go
func NewRFC7807Error(status int, title, detail string) *RFC7807Error {
	return &RFC7807Error{
		Type:   "about:blank", // Default type as per RFC7807
		Title:  title,
		Status: status,
		Detail: detail,
	}
}

func BadRequestProblem(detail string, title ...string) *RFC7807Error {
	t := "Bad Request"
	if len(title) > 0 && title[0] != "" {
		t = title[0]
	}
	return NewRFC7807Error(http.StatusBadRequest, t, detail)
}

func (p *RFC7807Error) WithType(typeURI string) *RFC7807Error { ... }
func (p *RFC7807Error) WithInstance(instance string) *RFC7807Error { ... }
func (p *RFC7807Error) WithExtension(key string, value interface{}) *RFC7807Error { ... }
```

O `"about:blank"` em `Type` é o valor padrão, que significa uma página inexistente. Abaixo está um exemplo de criação de erro para uma requisição inválida.

```go
problem := httperror.BadRequestProblem("invalid user id format", "Bad User Input")

problem = problem.WithType("https://example.com/errors/validation")
         .WithInstance("/api/users/abc")
         .WithExtension("invalid_field", "user_id")
         .WithExtension("expected_format", "numeric")
```

Com um encadeamento simples de métodos, é possível criar mensagens de erro estruturadas para o usuário. Além disso, para utilizar o roteador centralizado apresentado anteriormente, podemos oferecer o seguinte método.

```go
func (p *RFC7807Error) ToHttpError() *HttpError {
	jsonBytes, err := json.Marshal(p)
	if err != nil {
		// If marshaling fails, fall back to just using the detail
		return New(p.Status, p.Detail)
	}
	return New(p.Status, string(jsonBytes))
}
```

Utilizando isso diretamente para modificar o exemplo acima, obtemos o seguinte.

```go
package main

import (
	"bytes"
	"context"
	"errors"
	"io"
	"log"
	"net/http"

	"github.com/gosuda/httpwrap/httperror"
	"github.com/gosuda/httpwrap/wrapper/chiwrap"
)

func main() {
    ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
    defer cancel()

	r := chiwrap.NewRouter(func(err error) {
		log.Printf("Router log test: Error occured: %v", err)
	})
	r.Get("/echo", func(writer http.ResponseWriter, request *http.Request) error {
		name := request.URL.Query().Get("name")
		if name == "" {
			return httperror.BadRequestProblem("name is required", "Bad User Input").
                WithType("https://example.com/errors/validation").
                WithInstance("/api/echo").
                WithExtension("invalid_field", "name").
                WithExtension("expected_format", "string").
                WithExtension("actual_value", name).
                ToHttpError()
		}

		writer.Write([]byte("Hello " + name))
		return nil
	})

	svr := http.Server{
		Addr:    ":8080",
		Handler: r,
	}
	go func() {
		if err := svr.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
			log.Fatalf("Failed to start server: %v", err)
		}
	}()

    <-ctx.Done()
    svr.Shutdown(context.Background())
}
```

## Conclusão

Ao tratar erros com um roteador centralizado desta forma, é possível reduzir o esforço de verificar códigos de erro e de escrever mensagens de erro apropriadas a cada vez. Além disso, ao utilizar a RFC7807 para fornecer mensagens de erro estruturadas, pode-se ajudar o cliente a compreender e tratar os erros. Por meio desses métodos, o tratamento de erros em APIs HTTP escritas em Go pode se tornar mais simples e consistente.

O código deste artigo pode ser consultado no repositório [gosuda/httpwrap](https://github.com/gosuda/httpwrap).
