HTTP hibák még kevésbé körülményes kezelése + RFC7807
Áttekintés
Go nyelvben http api létrehozásakor a legbosszantóbb dolog a hibakezelés. Tipikusan ilyen kód létezik.
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}
Ha csak néhány API van, akkor így írva sem lesz különösebb kényelmetlenség. Azonban, ahogy az API-k száma növekszik, és a belső logika bonyolultabbá válik, három dolog zavaróvá válik.
- Megfelelő hiba kód visszaadása
- Nagy számú eredmény log írása
- Világos hiba üzenetek küldése
Tárgyalás
Megfelelő hiba kód visszaadása
Természetesen az 1. pont, a megfelelő hiba kód visszaadása, személyes panaszom.
Egy tapasztalt fejlesztő megtalálja a megfelelő kódot, és minden alkalommal jól beilleszti, azonban hozzám hasonlóan, és még tapasztalatlan fejlesztők, ahogy a logika bonyolultabbá válik, és a ismétlések száma növekszik, nehézségekbe ütközhetnek a megfelelő hiba kódok rendszeres használatában.
Ezzel kapcsolatban többféle módszer is létezik, legtipikusabban a kód írása, amely előre megtervezett API logikai folyamat után ad vissza megfelelő hibát.Tegye így
Ez azonban nem tűnik optimális módszernek az IDE (vagy Language Server) segítségét igénybe vevő emberi fejlesztők számára.
Továbbá, mivel maga a REST API maximálisan kihasználja a hiba kódban foglalt jelentést, egy másik módszer is javasolható.go `HttpError` nevű error (`error`) interfész implementáció létrehozásával lehetővé teszi `StatusCode` és `Message` tárolását.
És a következő helper függvényeket biztosítja.
1err := httperror.BadRequest("wrong format")
go A `BadRequest` helper függvény `HttpError`-t fog visszaadni, amelynek `StatusCode`-ja 400, és `Message`-e a paraméterként kapott értékre van beállítva.
Ezen kívül természetesen NotImplement
, ServiceUnavailable
, Unauthorized
, PaymentRequired
stb. helper függvények az automatikus kiegészítés funkcióval kereshetők és hozzáadhatók lesznek.
Ez gyorsabb, mint a kész tervezési dokumentumot minden alkalommal ellenőrizni, és stabilabb lesz, mint a hiba kódokat minden alkalommal számmal beírni.A http.StatusCode
konstansokban mind benne van? Pszt
Nagy számú eredmény log írása
Hiba esetén természetesen logok kerülnek rögzítésre. Amikor az API hívásra kerül, és a kérés sikerességéről vagy sikertelenségéről logot rögzítünk, a logok rögzítése minden várható végpontnál a kezdetektől növeli az írandó kódsorok számát. Ezt a handler magát egyszer beburkolva központilag kezelhetővé tesszük.
A következő egy példa a chi
router beburkolására.
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}
A router struktúra belsőleg tartalmazza a chi.Router
-t, így úgy van konfigurálva, hogy a chi.Router
funkcióit változatlanul használja.
Ha megnézzük a Get
metódust, miután ellenőrizte, hogy a fent javasolt helper függvény által visszaadott HttpError
struktúra került-e visszaadásra, és megfelelően visszatért, error
esetén egységesen átadódik a hiba callback függvénynek.
Ez a callback a konstruktoron keresztül kerül bemenetként fogadásra.
A következő a csomag felhasználásával írt kód.
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}
Milyen?
Ha egyszerűen csak HttpError
-t készítünk helper függvénnyé és visszaadjuk, a felsőbb scope-ban, miközben megfelelő hiba kóddal és üzenettel ad vissza választ, callback regisztrálásával kezelhetővé válik, így minden implementált szolgáltatáshoz megfelelő logok maradhatnak.
Ezen felül, ha szükséges, bővítve, a RequestID
stb. használatával részletes logolás lesz lehetséges.
Világos hiba üzenetek küldése
Erre vonatkozó dokumentumként létezik az RFC7807. Az RFC7807 főként a következő elemeket definiálja és használja.
type
: A hiba típusát azonosító URI. Főként a hibát magyarázó dokumentum.title
: Egy egysoros leírás arról, hogy milyen hibáról van szó.status
: Megegyezik a HTTP Status Code-dal.detail
: Egy ember által olvasható részletes leírás a vonatkozó hibáról.instance
: A hiba bekövetkezésének URI-ja. Például, ha hiba történt aGET /user/info
-nál, akkor a/user/info
lesz az érték.extensions
: Másodlagos elemek, amelyek JSON Object formában épülnek fel a hiba magyarázatára.- Például,
BadRequest
esetén a felhasználó bemenete is szerepelhet. - Vagy
TooManyRequest
esetén a legutóbbi kérés időpontja is szerepelhet.
- Például,
Ennek könnyű használata érdekében az HttpError
-ral azonos helyen lévő httperror
csomagban új fájlt hozunk létre, RFC7807Error
struktúrát generálunk, és lehetővé tesszük a metódus láncolási mintával történő létrehozást.
1func NewRFC7807Error(status int, title, detail string) *RFC7807Error {
2 return &RFC7807Error{
3 Type: "about:blank", // Alapértelmezett type az RFC7807 szerint
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 { ... }
go A `Type` `"about:blank"` értéke az alapértelmezett.
Nem létező oldalt jelent.
Alább egy példa hibagenerálásra rossz kérés esetén.
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")
Egyszerű metódus láncolással strukturált hibaüzenetek hozhatók létre a felhasználó számára. Továbbá, a fentebb először megírt központosított router kihasználása érdekében a következő metódus támogatható.
1func (p *RFC7807Error) ToHttpError() *HttpError {
2 jsonBytes, err := json.Marshal(p)
3 if err != nil {
4 // Ha a marshaling sikertelen, visszatérés a detail használatára
5 return New(p.Status, p.Detail)
6 }
7 return New(p.Status, string(jsonBytes))
8}
Ezt változatlanul használva, ha a fenti példát módosítjuk, így néz ki.
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}
Konklúzió
Így, központosított router használatával a hibák kezelésekor csökkenthető a hiba kód minden alkalommal történő ellenőrzésének, és a megfelelő hiba üzenetek írásának terhe. Továbbá, az RFC7807 felhasználásával strukturált hiba üzenetek nyújtásával segíthetünk a kliensnek a hiba megértésében és kezelésében. Ezekkel a módszerekkel a Go nyelvben írt HTTP API-k hibakezelése még egyszerűbbé és következetesebbé tehető.
A cikkben szereplő kód a gosuda/httpwrap repositoryban tekinthető meg.