---
id: 982178b2b9c8c091685bdc7fbf942c43
author: snowmerak
title: HTTP-Fehler bequemer behandeln + RFC7807
description: Nervt dich die Fehlerbehandlung in Go-APIs? Mit RFC 7807 lässt sie sich einfacher und klarer gestalten.
language: de
date: 2025-05-19T20:56:37.945722346Z
path: /blog/posts/handling-http-errors-more-conveniently-+-rfc7807-z862df968
category: api
---

## Überblick

Beim Erstellen einer HTTP-API mit der Programmiersprache Go ist die Fehlerbehandlung das lästigste Element. Typischerweise sieht der Code so aus.

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

Solange die Anzahl der APIs gering ist, dürfte es kaum unbequem sein, auf diese Weise zu schreiben. Mit wachsender Anzahl von APIs und zunehmender Komplexität der internen Logik treten jedoch drei Punkte störend in den Vordergrund.

1. Rückgabe geeigneter Fehlercodes
2. Anzahl der Schreibvorgänge für Ergebnisprotokolle
3. Übermittlung klarer Fehlermeldungen

## Hauptteil

### Rückgabe geeigneter Fehlercodes

Der erste Punkt, die Rückgabe geeigneter Fehlercodes, ist zwar eine persönliche Unzufriedenheit meinerseits. Erfahrene Entwickler werden stets den passenden Code finden und konsequent einsetzen. Ich selbst und auch unerfahrene Entwickler haben jedoch mit zunehmender Komplexität der Logik und steigender Anzahl von Aufrufen oft Schwierigkeiten, geeignete Fehlercodes regelmäßig und einheitlich zu verwenden. Dafür gibt es verschiedene Ansätze, der repräsentativste besteht darin, den Ablauf der API-Logik im Voraus zu entwerfen und den Code anschließend so zu schreiben, dass passende Fehler zurückgegeben werden. ~~Machen Sie es so~~

Dies erscheint jedoch nicht als optimale Methode für menschliche Entwickler, die von einer IDE (oder einem Language Server) unterstützt werden. Da REST-APIs zudem die Bedeutung von Fehlercodes möglichst umfassend nutzen, lässt sich ein anderer Ansatz vorschlagen. Man erstellt eine neue Implementierung des Interfaces `error` namens `HttpError`, die `StatusCode` und `Message` speichert. Außerdem stellt man folgende Hilfsfunktionen bereit.

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

Die Hilfsfunktion `BadRequest` gibt ein `HttpError` zurück, dessen `StatusCode` auf 400 und dessen `Message` auf den übergebenen Wert gesetzt ist. Darüber hinaus können selbstverständlich weitere Hilfsfunktionen wie `NotImplement`, `ServiceUnavailable`, `Unauthorized` oder `PaymentRequired` über die automatische Vervollständigung abgefragt und ergänzt werden. Dies ist schneller als das wiederholte Nachschlagen in einem vorbereiteten Entwurf und zuverlässiger als die jedes Mal manuelle Eingabe numerischer Fehlercodes. ~~Alles bereits in der Konstante `http.StatusCode` enthalten? Pst~~

### Anzahl der Schreibvorgänge für Ergebnisprotokolle

Bei einem Fehler wird naturgemäß ein Protokoll geschrieben. Beim Protokollieren, ob ein API-Aufruf erfolgreich oder fehlgeschlagen ist, wächst die Menge des zu schreibenden Codes erheblich, wenn an allen erwarteten Endpunkten vom Beginn an Protokolleinträge gesetzt werden. Indem man den Handler selbst einmal umhüllt, lässt sich dies zentral verwalten.

Das folgende Beispiel zeigt eine Umhüllung des Routers `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)
		}
	})
}
```

Die Routerstruktur enthält `chi.Router` intern, sodass die Funktionalität von `chi.Router` unverändert genutzt werden kann. In der Methode `Get` wird geprüft, ob das von der oben vorgeschlagenen Hilfsfunktion zurückgegebene Strukturobjekt `HttpError` ist, und es wird entsprechend geantwortet. Im Fall eines `error` wird dieser einheitlich an die Fehler-Callback-Funktion übergeben. Dieser Callback wird über den Konstruktor entgegengenommen.

Das folgende Beispiel zeigt Code, der dieses Paket verwendet.

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

Wie finden Sie das? Wenn man lediglich `HttpError` als Hilfsfunktion erstellt und zurückgibt, kann der übergeordnete Bereich mit einem passenden Fehlercode und einer passenden Meldung antworten, während durch die Registrierung eines Callbacks jeder Dienst ein geeignetes Protokoll schreiben kann. Bei Bedarf lässt sich dies erweitern, sodass beispielsweise über `RequestID` eine detaillierte Protokollierung möglich wird.

### Übermittlung klarer Fehlermeldungen

Hierfür gibt es das Dokument RFC7807. RFC7807 definiert und verwendet hauptsächlich folgende Elemente.

- `type`: Ein URI, der den Fehlertyp identifiziert. In der Regel ist dies ein Dokument, das den Fehler beschreibt.
- `title`: Eine kurze einzeilige Beschreibung des Fehlers.
- `status`: Entspricht dem HTTP-Statuscode.
- `detail`: Eine für Menschen lesbare, ausführliche Beschreibung des jeweiligen Fehlers.
- `instance`: Der URI, bei dem der Fehler aufgetreten ist. Tritt beispielsweise bei `GET /user/info` ein Fehler auf, so lautet der Wert `/user/info`.
- `extensions`: Ergänzende Elemente zur Beschreibung des Fehlers, die als JSON-Objekt aufgebaut sind.
  - Bei `BadRequest` können beispielsweise die Eingaben des Benutzers enthalten sein.
  - Bei `TooManyRequest` kann etwa der Zeitpunkt der letzten Anfrage enthalten sein.

Um dies einfach zu verwenden, erstellt man im Paket `httperror`, an derselben Stelle wie `HttpError`, eine neue Datei, definiert die Struktur `RFC7807Error` und ermöglicht die Erstellung im Methodenverkettungsmuster.

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

Der Wert `"about:blank"` für `Type` ist der Standardwert. Er bedeutet, dass keine Seite existiert. Im Folgenden ein Beispiel für die Erstellung eines Fehlers bei einer ungültigen Anfrage.

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

Mit einfacher Methodenverkettung lässt sich eine strukturierte Fehlermeldung für den Benutzer erzeugen. Um außerdem den zuvor geschriebenen zentralisierten Router zu nutzen, kann man zusätzlich folgende Methode bereitstellen.

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

Verwendet man dies unverändert, um das obige Beispiel anzupassen, ergibt sich folgender Code.

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

## Fazit

Durch die Fehlerbehandlung über einen zentralisierten Router wird die Belastung verringert, bei jedem Aufruf Fehlercodes zu prüfen und passende Fehlermeldungen zu formulieren. Zudem kann durch die Verwendung von RFC7807 eine strukturierte Fehlermeldung bereitgestellt werden, die Clients beim Verstehen und Verarbeiten des Fehlers unterstützt. Auf diese Weise lässt sich die Fehlerbehandlung in in Go geschriebenen HTTP-APIs deutlich einfacher und einheitlicher gestalten.

Den Code dieses Artikels finden Sie im Repository [gosuda/httpwrap](https://github.com/gosuda/httpwrap).
