---
id: 982178b2b9c8c091685bdc7fbf942c43
author: snowmerak
title: Gérer les erreurs HTTP de manière moins fastidieuse + RFC7807
description: Vous trouvez la gestion des erreurs dans une API Go fastidieuse ? Traitez-les de manière plus simple et plus claire grâce à RFC7807.
language: fr
date: 2025-05-19T20:56:37.945722346Z
path: /blog/posts/handling-http-errors-more-conveniently-+-rfc7807-z862df968
category: api
---

## Aperçu

Lorsqu'on crée une API HTTP en Go, la gestion des erreurs est ce qu'il y a de plus fastidieux. Un exemple typique est le code suivant.

```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
    }
    // ...
}
```

Si l'API ne comporte que quelques endpoints, écrire le code de cette manière ne pose pas vraiment de problème. Cependant, à mesure que le nombre d'API augmente et que la logique interne se complexifie, trois points deviennent gênants.

1. Renvoi de codes d'erreur appropriés
2. Nombre excessif d'écritures de journaux
3. Envoi de messages d'erreur clairs

## Développement

### Renvoi de codes d'erreur appropriés

Le premier point, le renvoi de codes d'erreur appropriés, est effectivement une gêne personnelle. Un développeur expérimenté trouvera le code adapté et l'insérera correctement à chaque fois, mais les développeurs encore inexpérimentés, moi y compris, peuvent avoir du mal à utiliser systématiquement le code d'erreur approprié à mesure que la logique se complexifie et que le nombre d'appels augmente. Il existe plusieurs approches à ce problème, la plus courante consistant à concevoir à l'avance le flux logique de l'API, puis à écrire le code de manière à renvoyer l'erreur appropriée. ~~Faites comme ça~~

Toutefois, cette approche ne semble pas optimale pour un développeur humain qui bénéficie de l'aide d'un IDE (ou d'un Language Server). De plus, comme l'API REST exploite au maximum la signification portée par les codes d'erreur, on peut proposer une autre méthode. Il s'agit de créer une nouvelle implémentation de l'interface `error`, nommée `HttpError`, qui stocke un `StatusCode` et un `Message`. On fournit ensuite les fonctions auxiliaires suivantes.

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

La fonction auxiliaire `BadRequest` renvoie un `HttpError` dont le `StatusCode` est défini à 400 et le `Message` à la valeur reçue en argument. Outre celle-ci, on peut bien sûr consulter et ajouter d'autres fonctions auxiliaires telles que `NotImplement`, `ServiceUnavailable`, `Unauthorized` ou `PaymentRequired` grâce à la complétion automatique. Cette approche est plus rapide que la consultation systématique d'une spécification de conception, et plus fiable que la saisie manuelle des codes d'erreur sous forme numérique. ~~Vous me direz que tout est dans les constantes `http.StatusCode` ? Chut~~

### Nombre excessif de lignes de journalisation

Lorsqu'une erreur survient, il est naturel de journaliser l'événement. Pourtant, lorsqu'on consigne si l'appel à une API a abouti ou échoué, placer une journalisation à chaque point de sortie prévu, depuis le début, alourdit considérablement le code. En enveloppant le handler lui-même, on peut centraliser cette gestion.

Voici un exemple qui enveloppe le routeur `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)
		}
	})
}
```

La structure du routeur contient un `chi.Router` en interne, de sorte que les fonctionnalités de `chi.Router` restent directement utilisables. Dans la méthode `Get`, on vérifie si l'erreur renvoyée est une structure `HttpError`, comme celle renvoyée par les fonctions auxiliaires proposées précédemment, puis on répond en conséquence. Si ce n'est pas le cas, l'erreur est transmise de manière uniforme à la fonction de rappel d'erreur. Ce callback est reçu via le constructeur.

Voici le code écrit à l'aide de ce package.

```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())
}
```

Qu'en pensez-vous ? Si l'on se contente de créer un `HttpError` via une fonction auxiliaire et de le renvoyer, la couche supérieure peut répondre avec le code d'erreur et le message appropriés, tandis qu'un callback enregistré permet de journaliser ce qui convient à chaque service. Si nécessaire, on peut étendre cette approche en utilisant un `RequestID` ou un mécanisme similaire pour obtenir une journalisation détaillée.

### Envoi de messages d'erreur clairs

Le document de référence pour cela est la RFC7807. La RFC7807 définit principalement les éléments suivants.

- `type`: URI identifiant le type d'erreur. Il s'agit généralement d'un document décrivant l'erreur.
- `title`: description concise en une ligne de la nature de l'erreur.
- `status`: identique au code de statut HTTP.
- `detail`: description détaillée de l'erreur, lisible par un humain.
- `instance`: URI à laquelle l'erreur s'est produite. Par exemple, si l'erreur survient lors de `GET /user/info`, la valeur sera `/user/info`.
- `extensions`: éléments secondaires, sous forme d'objet JSON, décrivant l'erreur.
  - Par exemple, dans le cas d'un `BadRequest`, la saisie de l'utilisateur peut être incluse.
  - Ou, dans le cas d'un `TooManyRequest`, l'instant de la requête la plus récente peut être inclus.

Pour faciliter son utilisation, on crée un nouveau fichier dans le package `httperror`, au même endroit que `HttpError`, on y définit la structure `RFC7807Error`, et on permet sa construction selon le motif de chaînage de méthodes.

```go
func NewRFC7807Error(status int, title, detail string) *RFC7807Error {
	return &RFC7807Error{
		Type:   "about:blank", // Type par défaut selon 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 { ... }
```

La valeur `"about:blank"` de `Type` est la valeur par défaut. Elle désigne une page inexistante. Voici un exemple de création d'erreur pour une requête incorrecte.

```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")
```

Grâce à un simple chaînage de méthodes, on peut générer un message d'erreur structuré destiné à l'utilisateur. Par ailleurs, afin d'utiliser le routeur centralisé présenté précédemment, on peut prendre en charge la méthode suivante.

```go
func (p *RFC7807Error) ToHttpError() *HttpError {
	jsonBytes, err := json.Marshal(p)
	if err != nil {
		// Si la sérialisation échoue, utiliser simplement le detail
		return New(p.Status, p.Detail)
	}
	return New(p.Status, string(jsonBytes))
}
```

En utilisant ce code tel quel, l'exemple précédent devient le suivant.

```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())
}
```

## Conclusion

En traitant les erreurs à l'aide d'un routeur centralisé, on réduit la charge liée à la vérification des codes d'erreur et à la rédaction de messages d'erreur appropriés à chaque fois. De plus, en fournissant des messages d'erreur structurés grâce à la RFC7807, on aide le client à comprendre les erreurs et à y réagir. Cette méthode permet de rendre la gestion des erreurs des API HTTP écrites en Go plus simple et plus cohérente.

Le code de cet article est disponible dans le dépôt [gosuda/httpwrap](https://github.com/gosuda/httpwrap).
