GoSuda

Effizientere Behandlung von HTTP-Fehlern + RFC7807

By snowmerak
views ...

Übersicht

Bei der Erstellung von http api in der Go-Sprache ist die Fehlerbehandlung das lästigste. Stellvertretend gibt es diesen Code.

1func(w http.ResponseWriter, r *http.Request) {
2    err := doSomething()
3    if err != nil {
4        http.Error(w, err.Error(), http.StatusInternalServerError)
5        log.Printf("error occurred: %v", err)
6        return
7    }
8    // ...
9}

Wenn es nur wenige API gibt, wird es nicht besonders unbequem sein, es auf diese Weise zu schreiben. Wenn jedoch die Anzahl der API zunimmt und die interne Logik komplexer wird, werden drei Dinge störend.

  1. Rückgabe eines geeigneten 에러 code
  2. Große Anzahl von 결과 로그 작성 수
  3. Übermittlung einer klaren 에러 메시지

Hauptteil

Rückgabe eines geeigneten 에러 code

Natürlich ist Punkt 1, die Rückgabe eines geeigneten 에러 code, eine persönliche Beschwerde von mir. Ein erfahrener Entwickler wird den passenden code suchen und ihn jedes Mal gut einfügen, aber ich und noch unerfahrene Entwickler können Schwierigkeiten haben, bei komplexer werdender Logik und zunehmenden Wiederholungen einen geeigneten 에러 code regelmäßig zu verwenden. Es gibt mehrere Methoden dafür, und die repräsentativste Methode ist wohl, den API-Logikfluss im Voraus zu entwerfen und dann den Code so zu schreiben, dass ein geeigneter 에러 zurückgegeben wird. Tun Sie das

Dies scheint jedoch nicht der optimalen Methode für menschliche Entwickler zu entsprechen, die Unterstützung von IDE (oder Language Server) erhalten. Darüber hinaus kann, da REST API selbst die Bedeutung von 에러 code maximal nutzt, ein anderer Ansatz vorgeschlagen werden. Es wird eine neue Implementierung der error-Schnittstelle namens HttpError erstellt, die StatusCode und Message speichert. Und die folgenden Helferfunktionen werden bereitgestellt.

1err := httperror.BadRequest("wrong format")

Die BadRequest-Helferfunktion gibt einen HttpError zurück, bei dem der StatusCode auf 400 und die Message auf den als Argument übergebenen Wert gesetzt ist. Darüber hinaus können natürlich auch Helferfunktionen wie NotImplement, ServiceUnavailable, Unauthorized, PaymentRequired usw. über die Autovervollständigungsfunktion abgefragt und hinzugefügt werden. Dies ist schneller als jedes Mal das vorbereitete Design-Dokument zu überprüfen und stabiler, als jedes Mal den 에러 code als Zahl einzugeben. http.StatusCode Konstanten sind alle da? Pssst

Große Anzahl von 결과 로그 작성 수

Wenn ein 에러 auftritt, wird natürlich ein 로그 hinterlassen. Beim Hinterlassen eines 로그, ob der API-Aufruf erfolgreich war oder fehlschlug, erhöht sich die Anzahl der zu schreibenden Codezeilen, wenn 로그 von Anfang an an allen erwarteten Endpunkten hinterlassen wird. Dies kann zentral verwaltet werden, indem der Handler selbst einmal umhüllt wird.

Das Folgende ist ein Beispiel für die Umhüllung des chi Routers.

 1package chiwrap
 2
 3import (
 4	"errors"
 5	"net/http"
 6
 7	"github.com/go-chi/chi/v5"
 8
 9	"github.com/gosuda/httpwrap/httperror"
10)
11
12type Router struct {
13	router      chi.Router
14	errCallback func(err error)
15}
16
17func NewRouter(errCallback func(err error)) *Router {
18	if errCallback == nil {
19		errCallback = func(err error) {}
20	}
21	return &Router{
22		router:      chi.NewRouter(),
23		errCallback: errCallback,
24	}
25}
26
27type HandlerFunc func(writer http.ResponseWriter, request *http.Request) error
28
29func (r *Router) Get(pattern string, handler HandlerFunc) {
30	r.router.Get(pattern, func(writer http.ResponseWriter, request *http.Request) {
31		if err := handler(writer, request); err != nil {
32			he := &httperror.HttpError{}
33			switch errors.As(err, &he) {
34			case true:
35				http.Error(writer, he.Message, he.Code)
36			case false:
37				http.Error(writer, err.Error(), http.StatusInternalServerError)
38			}
39			r.errCallback(err)
40		}
41	})
42}

Die Router-Struktur enthält chi.Router intern, sodass sie so konfiguriert ist, dass sie die Funktionen von chi.Router unverändert verwendet. Betrachtet man die Get-Methode, wird überprüft, ob die HttpError-Struktur, die von der oben vorgeschlagenen Helferfunktion zurückgegeben wird, zurückgegeben wurde, und dann entsprechend zurückgegeben, und im Falle eines error wird sie einheitlich an die 에러-Callback-Funktion übergeben. Dieser Callback wird über den Konstruktor empfangen.

Das Folgende ist Code, der unter Verwendung dieses Pakets geschrieben wurde.

 1package main
 2
 3import (
 4	"bytes"
 5	"context"
 6	"errors"
 7	"io"
 8	"log"
 9	"net/http"
10
11	"github.com/gosuda/httpwrap/httperror"
12	"github.com/gosuda/httpwrap/wrapper/chiwrap"
13)
14
15func main() {
16    ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
17    defer cancel()
18
19	r := chiwrap.NewRouter(func(err error) {
20		log.Printf("Router log test: Error occured: %v", err)
21	})
22	r.Get("/echo", func(writer http.ResponseWriter, request *http.Request) error {
23		name := request.URL.Query().Get("name")
24		if name == "" {
25			return httperror.BadRequest("name is required")
26		}
27
28		writer.Write([]byte("Hello " + name))
29		return nil
30	})
31
32	svr := http.Server{
33		Addr:    ":8080",
34		Handler: r,
35	}
36	go func() {
37		if err := svr.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
38			log.Fatalf("Failed to start server: %v", err)
39		}
40	}()
41
42    <-ctx.Done()
43    svr.Shutdown(context.Background())
44}

Wie ist das? Wenn einfach nur HttpError als Helferfunktion erstellt und zurückgegeben wird, kann im übergeordneten Scope eine Antwort mit dem passenden 에러 code und der passenden 메시지 zurückgegeben und durch Registrierung eines Callbacks eine entsprechende Protokollierung für jeden implementierten Dienst ermöglicht werden. Bei Bedarf kann dies zusätzlich erweitert werden, um eine detaillierte Protokollierung unter Verwendung von RequestID o.ä. zu ermöglichen.

Übermittlung einer klaren 에러 메시지

Als Dokument hierfür dient RFC7807. RFC7807 definiert und verwendet hauptsächlich die folgenden Elemente:

  • type: Ein URI, der den 에러-Typ identifiziert. Dies ist hauptsächlich ein Dokument, das den 에러 beschreibt.
  • title: Eine einzeilige Beschreibung, um welchen 에러 es sich handelt.
  • status: Identisch mit HTTP Status Code.
  • detail: Eine für Menschen lesbare detaillierte Beschreibung des betreffenden 에러.
  • instance: Der URI, bei dem der 에러 aufgetreten ist. Wenn z. B. ein 에러 bei GET /user/info aufgetreten ist, wäre /user/info dieser Wert.
  • extensions: Zusätzliche Elemente zur Beschreibung des 에러, die als JSON Object strukturiert sind.
    • Zum Beispiel, im Falle von BadRequest, kann die Eingabe des Benutzers enthalten sein.
    • Oder im Falle von TooManyRequest, kann der Zeitpunkt der jüngsten Anfrage enthalten sein.

Um dies einfach zu verwenden, wird eine neue Datei im httperror-Paket, dem gleichen Speicherort wie HttpError, erstellt und die RFC7807Error-Struktur erzeugt, die über den Method Chaining Pattern erstellt werden kann.

 1func NewRFC7807Error(status int, title, detail string) *RFC7807Error {
 2	return &RFC7807Error{
 3		Type:   "about:blank", // Default type as per RFC7807
 4		Title:  title,
 5		Status: status,
 6		Detail: detail,
 7	}
 8}
 9
10func BadRequestProblem(detail string, title ...string) *RFC7807Error {
11	t := "Bad Request"
12	if len(title) > 0 && title[0] != "" {
13		t = title[0]
14	}
15	return NewRFC7807Error(http.StatusBadRequest, t, detail)
16}
17
18func (p *RFC7807Error) WithType(typeURI string) *RFC7807Error { ... }
19func (p *RFC7807Error) WithInstance(instance string) *RFC7807Error { ... }
20func (p *RFC7807Error) WithExtension(key string, value interface{}) *RFC7807Error { ... }

"about:blank" für Type ist der Standardwert. Es bedeutet eine nicht existierende Seite. Unten ist ein Beispiel für die Erstellung eines 에러 für eine ungültige Anfrage.

1problem := httperror.BadRequestProblem("invalid user id format", "Bad User Input")
2
3problem = problem.WithType("https://example.com/errors/validation")
4         .WithInstance("/api/users/abc")
5         .WithExtension("invalid_field", "user_id")
6         .WithExtension("expected_format", "numeric")

Mit einfachem Method Chaining können strukturierte 에러-Nachrichten für den Benutzer erstellt werden. Darüber hinaus kann die folgende Methode unterstützt werden, um den bereits oben geschriebenen zentralisierten Router zu nutzen.

1func (p *RFC7807Error) ToHttpError() *HttpError {
2	jsonBytes, err := json.Marshal(p)
3	if err != nil {
4		// If marshaling fails, fall back to just using the detail
5		return New(p.Status, p.Detail)
6	}
7	return New(p.Status, string(jsonBytes))
8}

Wenn das obige Beispiel unverändert unter Verwendung dessen geändert wird, sieht es so aus.

 1package main
 2
 3import (
 4	"bytes"
 5	"context"
 6	"errors"
 7	"io"
 8	"log"
 9	"net/http"
10
11	"github.com/gosuda/httpwrap/httperror"
12	"github.com/gosuda/httpwrap/wrapper/chiwrap"
13)
14
15func main() {
16    ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
17    defer cancel()
18
19	r := chiwrap.NewRouter(func(err error) {
20		log.Printf("Router log test: Error occured: %v", err)
21	})
22	r.Get("/echo", func(writer http.ResponseWriter, request *http.Request) error {
23		name := request.URL.Query().Get("name")
24		if name == "" {
25			return httperror.BadRequestProblem("name is required", "Bad User Input").
26                WithType("https://example.com/errors/validation").
27                WithInstance("/api/echo").
28                WithExtension("invalid_field", "name").
29                WithExtension("expected_format", "string").
30                WithExtension("actual_value", name).
31                ToHttpError()
32		}
33
34		writer.Write([]byte("Hello " + name))
35		return nil
36	})
37
38	svr := http.Server{
39		Addr:    ":8080",
40		Handler: r,
41	}
42	go func() {
43		if err := svr.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
44			log.Fatalf("Failed to start server: %v", err)
45		}
46	}()
47
48    <-ctx.Done()
49    svr.Shutdown(context.Background())
50}

Schlussfolgerung

Durch die Verwendung eines solchen zentralisierten Routers zur Behandlung von 에러 kann die Last reduziert werden, jedes Mal den 에러 code zu überprüfen und eine geeignete 에러 메시지 zu schreiben. Darüber hinaus kann durch die Nutzung von RFC7807 und Bereitstellung einer strukturierten 에러 메시지 dem Client geholfen werden, den 에러 zu verstehen und zu verarbeiten. Durch diese Methode kann die 에러-Behandlung von mit Go-Sprache geschriebenen HTTP API einfacher und konsistenter gestaltet werden.

Der Code dieses Artikels kann im gosuda/httpwrap Repository eingesehen werden.