Menej obťažujúce spracovanie HTTP chýb + RFC7807
Prehľad
Pri vytváraní http API v jazyku Go je najotravnejšou vecou spracovanie chýb. Typickým príkladom je nasledujúci kód.
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}
Ak API obsahuje len niekoľko rozhraní, písanie kódu týmto spôsobom nemusí byť obzvlášť nepohodlné. Avšak, s narastajúcim počtom API a komplexnosťou internej logiky sa tri aspekty stávajú rušivými.
- Vrátenie vhodného chybového kódu
- Veľký počet logovacích záznamov výsledkov
- Odosielanie jasných chybových správ
Hlavná časť
Vrátenie vhodného chybového kódu
Samozrejme, bod 1, vrátenie vhodného chybového kódu, je mojou osobnou sťažnosťou. Skúsení vývojári pravdepodobne vždy nájdu a použijú správny kód, ale ja a neskúsení vývojári môžeme mať problémy s pravidelným používaním vhodných chybových kódov, keď sa logika stáva komplexnejšou a počet opakovaní narastá. Existuje na to niekoľko prístupov, pričom najbežnejším je navrhnúť tok logiky API vopred a následne napísať kód na vrátenie vhodných chýb. Urobte tak
Toto sa však nezdá byť optimálnym prístupom pre ľudského vývojára, ktorý využíva pomoc IDE (alebo Language Server). Navyše, vzhľadom na to, že samotné REST API maximálne využíva význam chybových kódov, možno navrhnúť iný prístup. Vytvoríme novú implementáciu rozhrania error s názvom HttpError, ktorá bude ukladať StatusCode a Message. Následne poskytneme nasledujúcu pomocnú funkciu.
1err := httperror.BadRequest("wrong format")
Pomocná funkcia BadRequest vráti HttpError s StatusCode nastaveným na 400 a Message nastaveným na hodnotu prijatú ako argument. Okrem toho bude samozrejme možné vyhľadať a pridať ďalšie pomocné funkcie ako NotImplement, ServiceUnavailable, Unauthorized, PaymentRequired prostredníctvom funkcie automatického dopĺňania. Toto bude rýchlejšie ako neustále kontrolovať pripravenú špecifikáciu a stabilnejšie ako zakaždým zadávať chybové kódy číselne. Konštanty http.StatusCode obsahujú všetko? Pššt
Veľký počet logovacích záznamov výsledkov
Pri výskyte chyby sa samozrejme zaznamená log. Ak sa API volá a zaznamenáva sa log o tom, či bola požiadavka úspešná alebo neúspešná, zaznamenávanie logov od začiatku do všetkých očakávaných koncových bodov zvyšuje množstvo kódu, ktorý je potrebné napísať. Toto je možné centralizovať obalením samotného handlera.
Nasleduje príklad obalenia routera chi.
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}
Štruktúra Router má interný chi.Router, čo umožňuje použitie funkcií chi.Router tak, ako sú. Metóda Get kontroluje, či bola vrátená štruktúra HttpError, ktorú navrhla pomocná funkcia vyššie, a ak áno, vráti ju vhodným spôsobom. Ak ide o error, je jednotne odovzdaná funkcii spätného volania pre chyby. Toto spätné volanie sa prijíma prostredníctvom konštruktora.
Nasleduje kód napísaný s využitím tohto balíka.
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}
Čo na to poviete? Ak sa HttpError vráti iba ako pomocná funkcia, je možné ju spracovať registráciou spätného volania, ktoré umožňuje vrátiť vhodný kód chyby a správu v nadradenom rozsahu a zaznamenať príslušné logy pre každú implementovanú službu. V prípade potreby je možné ju rozšíriť o detailné logovanie pomocou RequestID a podobne.
Odosielanie jasných chybových správ
Pre tento účel existuje dokument RFC7807. RFC7807 zvyčajne definuje a používa nasledujúce prvky:
type: URI identifikujúce typ chyby. Zvyčajne ide o dokument popisujúci chybu.title: Jednoriadkový popis chyby.status: Rovnaký ako HTTP Status Code.detail: Podrobný, ľudsky čitateľný popis chyby.instance: URI, kde k chybe došlo. Napríklad, ak k chybe došlo priGET /user/info, potom/user/infobude jeho hodnota.extensions: Sekundárne prvky na popis chyby, štruktúrované ako JSON Object.- Napríklad, v prípade
BadRequestmôže obsahovať vstup používateľa. - Alebo v prípade
TooManyRequestmôže obsahovať čas poslednej požiadavky.
- Napríklad, v prípade
Na uľahčenie použitia sa vytvorí nový súbor v balíku httperror, na rovnakom mieste ako HttpError, a vytvorí sa štruktúra RFC7807Error, ktorú je možné generovať pomocou vzoru metódy reťazenia.
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" pre Type je predvolená hodnota. Označuje neexistujúcu stránku. Nižšie je príklad vytvorenia chyby pre nesprávnu požiadavku.
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")
Jednoduchým reťazením metód je možné generovať štruktúrované chybové správy pre používateľa. Okrem toho, na využitie centralizovaného routera, ktorý bol opísaný vyššie, je možné podporovať nasledujúcu metódu.
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}
Ak to použijeme priamo na úpravu vyššie uvedeného príkladu, bude to vyzerať takto.
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}
Záver
Spracovaním chýb prostredníctvom tohto centralizovaného routera je možné znížiť záťaž spojenú s neustálym overovaním chybových kódov a písaním vhodných chybových správ. Okrem toho, poskytnutím štruktúrovaných chybových správ pomocou RFC7807 je možné klientovi pomôcť pochopiť a spracovať chyby. Tieto metódy môžu zjednodušiť a zjednotiť spracovanie chýb v HTTP API napísaných v jazyku Go.
Kód k tomuto článku je k dispozícii v repozitári gosuda/httpwrap.