---
id: 982178b2b9c8c091685bdc7fbf942c43
author: snowmerak
title: Gestire gli errori HTTP in modo meno gravoso + RFC7807
description: Trovi laboriosa la gestione degli errori nelle API in Go? Affrontala in modo più semplice e chiaro con RFC 7807.
language: it
date: 2025-05-19T20:56:37.945722346Z
path: /blog/posts/handling-http-errors-more-conveniently-+-rfc7807-z862df968
category: api
---

## Panoramica

Quando si creano API HTTP in Go, la parte più fastidiosa è la gestione degli errori. Un esempio tipico è questo codice.

```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 le API sono poche, scrivere il codice in questo modo non dovrebbe creare particolari problemi. Tuttavia, man mano che il numero delle API aumenta e la logica interna diventa più complessa, tre aspetti iniziano a risultare fastidiosi.

1. Restituzione di codici di errore appropriati
2. Numero eccessivo di scritture di log
3. Invio di messaggi di errore chiari

## Discussione principale

### Restituzione di codici di errore appropriati

Il punto 1, la restituzione di codici di errore appropriati, è certamente una mia lamentela personale. Uno sviluppatore esperto troverà sempre il codice adatto e lo inserirà correttamente ogni volta, ma io stesso e gli sviluppatori ancora inesperti possono avere difficoltà a usare in modo coerente i codici di errore corretti man mano che la logica diventa complessa e il numero di casi aumenta. Esistono diversi approcci a questo problema; il più comune è progettare in anticipo il flusso logico dell'API e scrivere il codice in modo che restituisca l'errore appropriato. ~~Fatelo pure~~

Tuttavia, questo non sembra l'approccio ottimale per uno sviluppatore umano che si avvale dell'IDE (o del Language Server). Inoltre, poiché le REST API stesse sfruttano al massimo il significato dei codici di errore, si può proporre un altro metodo. Creiamo un nuovo tipo che implementa l'interfaccia `error`, chiamato `HttpError`, che memorizza `StatusCode` e `Message`. Poi forniamo funzioni helper come la seguente.

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

La funzione helper `BadRequest` restituirà un `HttpError` con `StatusCode` impostato a 400 e `Message` impostato al valore ricevuto come argomento. Oltre a questa, si possono naturalmente aggiungere e consultare, tramite il completamento automatico, funzioni helper come `NotImplement`, `ServiceUnavailable`, `Unauthorized`, `PaymentRequired` e altre. Questo approccio è più rapido che consultare ogni volta un progetto predefinito, e più affidabile che inserire manualmente i codici numerici. ~~Dicono che sia tutto nella costante `http.StatusCode`? Shh~~

### Numero eccessivo di scritture di log

In caso di errore, naturalmente si scrive un log. Quando si registra se una chiamata API è riuscita o fallita, scrivere un log in ogni possibile punto di uscita, fin dall'inizio, aumenta il numero di righe di codice. Avvolgendo l'handler stesso, questa gestione può essere centralizzata.

Di seguito un esempio che avvolge il router `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 struttura del router contiene al suo interno `chi.Router`, quindi è configurata in modo da poter utilizzare direttamente le funzionalità di `chi.Router`. Osservando il metodo `Get`, si verifica se è stata restituita una struttura `HttpError`, come quella prodotta dalla funzione helper proposta poco fa, e la si restituisce in modo appropriato; nel caso di un `error` generico, questo viene inoltrato in modo uniforme alla funzione di callback per gli errori. Questo callback viene ricevuto tramite il costruttore.

Di seguito il codice scritto utilizzando questo pacchetto.

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

Che ne pensate? Se si crea semplicemente un `HttpError` tramite una funzione helper e lo si restituisce, il livello superiore può rispondere con il codice di errore e il messaggio appropriati, mentre si può registrare un callback che scrive il log adatto a ciascun servizio. Se necessario, si può ampliare il sistema, usando ad esempio `RequestID`, per ottenere una registrazione dettagliata.

### Invio di messaggi di errore chiari

A tal fine esiste il documento RFC7807. RFC7807 definisce e utilizza principalmente i seguenti elementi.

- `type`: URI che identifica il tipo di errore. Di solito è un documento che descrive l'errore.
- `title`: una breve descrizione del tipo di errore.
- `status`: uguale al codice di stato HTTP.
- `detail`: una descrizione dettagliata dell'errore, leggibile da una persona.
- `instance`: l'URI in cui si è verificato l'errore. Ad esempio, se l'errore si è verificato in `GET /user/info`, il valore sarà `/user/info`.
- `extensions`: elemento accessorio per descrivere l'errore, costituito da un oggetto JSON.
  - Ad esempio, in caso di `BadRequest`, possono essere inclusi gli input dell'utente.
  - Oppure, in caso di `TooManyRequest`, si può includere il momento della richiesta più recente.

Per usarlo facilmente, creiamo un nuovo file nel pacchetto `httperror`, nella stessa posizione di `HttpError`, definiamo la struttura `RFC7807Error` e rendiamo possibile la sua creazione tramite il pattern di method chaining.

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

Il valore `"about:blank"` di `Type` è quello predefinito. Indica una pagina inesistente. Di seguito un esempio di creazione di un errore per una richiesta non valida.

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

Con un semplice method chaining è possibile creare un messaggio di errore strutturato per l'utente. Inoltre, per utilizzare il router centralizzato presentato in precedenza, si può supportare il seguente metodo.

```go
func (p *RFC7807Error) ToHttpError() *HttpError {
	jsonBytes, err := json.Marshal(p)
	if err != nil {
		// Se la serializzazione fallisce, usa semplicemente il detail
		return New(p.Status, p.Detail)
	}
	return New(p.Status, string(jsonBytes))
}
```

Usando questo metodo così com'è, modifichiamo l'esempio precedente come segue.

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

## Conclusione

Gestire gli errori tramite un router centralizzato riduce il carico di dover verificare ogni volta i codici di errore e scrivere messaggi appropriati. Inoltre, utilizzando RFC7807 per fornire messaggi di errore strutturati, si aiuta il client a comprendere e gestire l'errore. Con questi metodi, la gestione degli errori nelle API HTTP scritte in Go può diventare più semplice e coerente.

Il codice di questo articolo è disponibile nel repository [gosuda/httpwrap](https://github.com/gosuda/httpwrap).
