---
id: 982178b2b9c8c091685bdc7fbf942c43
author: snowmerak
title: Het afhandelen van HTTP-fouten op een minder vervelende manier + RFC7807
description: Vindt u de foutafhandeling in uw Go API vervelend? Verwerk fouten dan eenvoudiger en duidelijker met RFC 7807.
language: nl
date: 2025-05-19T20:56:37.945722346Z
path: /blog/posts/handling-http-errors-more-conveniently-+-rfc7807-z862df968
category: api
---

## Overzicht

Bij het maken van een HTTP API in de Go-taal is foutafhandeling het meest vervelend. Een typisch voorbeeld van zo'n code is het volgende.

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

Als er maar enkele API's zijn, levert het schrijven op deze manier weinig problemen op. Naarmate het aantal API's toeneemt en de interne logica complexer wordt, gaan echter drie zaken storen.

1. Het teruggeven van een passende foutcode
2. Het grote aantal regels voor logging
3. Het verzenden van een duidelijke foutmelding

## Hoofdtekst

### Het teruggeven van een passende foutcode

Het eerste punt, het teruggeven van een passende foutcode, is weliswaar een persoonlijke klacht van mij. Ervaren ontwikkelaars zoeken de juiste code en vullen die steeds correct in, maar ik en ook minder ervaren ontwikkelaars kunnen het moeilijk vinden om consequent de juiste foutcode te gebruiken naarmate de logica complexer wordt en het aantal aanroepen toeneemt. Hiervoor bestaan verschillende oplossingen, waarvan de meest voor de hand liggende is om de logische stroom van de API vooraf te ontwerpen en vervolgens de code zo te schrijven dat de juiste fout wordt teruggegeven. ~~Doe dat maar~~

Dit lijkt echter niet de optimale aanpak voor een menselijke ontwikkelaar die gebruikmaakt van een IDE (of Language Server). Omdat de REST API zelf de betekenis van foutcodes zo goed mogelijk benut, kan ik een andere methode voorstellen. Maak een nieuwe implementatie van de `error`-interface met de naam `HttpError`, die `StatusCode` en `Message` opslaat. Stel daarnaast de volgende hulpfuncties beschikbaar.

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

De hulpfunctie `BadRequest` retourneert een `HttpError` waarvan `StatusCode` is ingesteld op 400 en `Message` op de meegegeven waarde. Daarnaast kunnen hulpfuncties zoals `NotImplement`, `ServiceUnavailable`, `Unauthorized` en `PaymentRequired` uiteraard via automatische aanvulling worden opgezocht en toegevoegd. Dit is sneller dan telkens het ontwerpdocument raadplegen, en betrouwbaarder dan elke keer een numerieke foutcode invoeren. ~~Dat staat allemaal in de `http.StatusCode`-constanten? Sst~~

### Het aantal logregels

Bij een fout wordt er uiteraard een logregel geschreven. Wanneer je logt of een API-aanroep is geslaagd of mislukt, betekent het vanaf het begin loggen op elk verwacht eindpunt dat er veel code moet worden geschreven. Door de handler zelf te omhullen, kan de logging centraal worden beheerd.

Het volgende is een voorbeeld van het omhullen van de `chi`-router.

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

De routerstruct bevat een `chi.Router` intern, zodat de functionaliteit van `chi.Router` ongewijzigd beschikbaar blijft. In de methode `Get` wordt gecontroleerd of de `HttpError`-struct die door de hulpfuncties die ik hierboven heb voorgesteld wordt geretourneerd. Die wordt dan op de juiste manier teruggegeven. In alle andere gevallen, waarin het een gewone `error` is, wordt deze gezamenlijk doorgegeven aan de foutcallbackfunctie. Deze callback wordt via de constructor ontvangen.

Het volgende is code die dit pakket gebruikt.

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

Wat vindt u ervan? Als je alleen een `HttpError` maakt via een hulpfunctie en die retourneert, kan de bovenliggende scope antwoorden met een passende foutcode en foutmelding, terwijl je via een geregistreerde callback in elke service een passende log kunt bijhouden. Indien nodig kan dit verder worden uitgebreid, bijvoorbeeld met een `RequestID`, zodat gedetailleerde logging mogelijk wordt.

### Het verzenden van duidelijke foutmeldingen

Hiervoor bestaat het document RFC7807. RFC7807 definieert en gebruikt voornamelijk de volgende elementen.

- `type`: een URI die het fouttype identificeert. Meestal is dit een document dat de fout beschrijft.
- `title`: een beknopte omschrijving van de aard van de fout in één regel.
- `status`: komt overeen met de HTTP-statuscode.
- `detail`: een gedetailleerde beschrijving van de fout die door mensen leesbaar is.
- `instance`: de URI waarin de fout is opgetreden. Als de fout bijvoorbeeld optrad bij `GET /user/info`, dan is de waarde `/user/info`.
- `extensions`: een aanvullend element om de fout nader te beschrijven, opgebouwd als JSON-object.
  - Bij een `BadRequest` kan bijvoorbeeld de invoer van de gebruiker worden opgenomen.
  - Bij een `TooManyRequest` kan bijvoorbeeld ook het tijdstip van het meest recente verzoek worden opgenomen.

Om dit eenvoudig te gebruiken, maak je in het pakket `httperror`, op dezelfde locatie als `HttpError`, een nieuw bestand aan, definieer je de struct `RFC7807Error` en maak je het mogelijk om deze op te bouwen via een methodeketen.

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

De `"about:blank"` van `Type` is de standaardwaarde. Deze betekent een niet-bestaande pagina. Hieronder volgt een voorbeeld van het maken van een fout voor een ongeldig verzoek.

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

Met eenvoudige methodeketens kan een gestructureerde foutmelding voor de gebruiker worden gemaakt. Om de eerder geschreven gecentraliseerde router te gebruiken, kan ook de volgende methode worden ondersteund.

```go
func (p *RFC7807Error) ToHttpError() *HttpError {
	jsonBytes, err := json.Marshal(p)
	if err != nil {
		// Als marshaling mislukt, gebruik dan alleen de detail
		return New(p.Status, p.Detail)
	}
	return New(p.Status, string(jsonBytes))
}
```

Als je dit direct gebruikt en het bovenstaande voorbeeld hiermee aanpast, ziet het er zo uit.

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

## Conclusie

Door fouten af te handelen via een gecentraliseerde router, wordt de last om bij elke aanroep de foutcode te controleren en een passende foutmelding te schrijven kleiner. Door RFC7807 te gebruiken om gestructureerde foutmeldingen te leveren, helpt u de client bovendien om de fout te begrijpen en ermee om te gaan. Op deze manier kunnen we de foutafhandeling van HTTP API's die in Go zijn geschreven eenvoudiger en consistenter maken.

De code van dit artikel is te vinden in de repository [gosuda/httpwrap](https://github.com/gosuda/httpwrap).
