Effizientere Behandlung von HTTP-Fehlern + RFC7807
Ü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.
- Rückgabe eines geeigneten 에러 code
- Große Anzahl von 결과 로그 작성 수
- Ü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 에러 beiGET /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.
- Zum Beispiel, im Falle von
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.