Snadnější zpracování HTTP chyb + RFC7807
Přehled
Při vytváření http api v jazyce Go je nejnepříjemnější zpracování chyb. Typickým příkladem je tento 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}
Pokud by API nebylo mnoho, takový způsob zápisu by pravděpodobně nepůsobil žádné zvláštní nepříjemnosti. Avšak s rostoucím počtem API a komplexností vnitřní logiky se stanou rušivými tři věci.
- Vracení vhodného error kódu
- Velké množství zápisů logů výsledků
- Odesílání jasných error zpráv
Hlavní část
Vracení vhodného error kódu
Samozřejmě, bod 1, vracení vhodného error kódu, je spíše mou osobní stížností. Zkušený vývojář by si vhodný kód pokaždé našel a správně jej vložil, avšak jak já, tak i méně zkušení vývojáři mohou mít potíže s pravidelným používáním vhodných error kódů, zejména s rostoucí komplexností logiky a počtem volání. Existuje několik způsobů, jak se s tím vypořádat, a nejtypičtější by pravděpodobně bylo navrhnout předem tok logiky API a poté napsat kód tak, aby vracel vhodné error. Udělejte to tak
Avšak toto se nezdá být optimální metodou pro lidského vývojáře, který využívá pomoc IDE (nebo Language Server). Navíc, jelikož REST API samotné maximálně využívá význam obsažený v error kódech, lze navrhnout jiný způsob. Vytvoříme novou implementaci error (error
) interface nazvanou HttpError
, která bude ukládat StatusCode
a Message
. A poskytneme následující helper funkci.
1err := httperror.BadRequest("wrong format")
Helper funkce BadRequest
vrátí HttpError
s StatusCode
nastaveným na 400 a Message
nastaveným na hodnotu přijatou jako argument. Kromě toho bude samozřejmě možné vyhledávat a přidávat další helper funkce, jako jsou NotImplement
, ServiceUnavailable
, Unauthorized
, PaymentRequired
atd., pomocí funkce automatického doplňování. Toto bude rychlejší než pokaždé kontrolovat připravený návrhový dokument a stabilnější než pokaždé zadávat error kód číslem. Všechno je v konstantách http.StatusCode
? Pšššt
Velké množství zápisů logů výsledků
Při výskytu error se samozřejmě zaznamenává log. Při logování, zda bylo API voláno a zda požadavek uspěl či selhal, zaznamenávání logů od začátku až po všechny očekávané koncové body zvyšuje množství kódu k napsání. Toto lze centrálně spravovat obalením samotného handler jednou.
Následuje příklad obalení chi
라우teru.
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}
라우터 struktura má uvnitř chi.Router
, takže je konfigurována tak, aby přímo používala funkce chi.Router
. Pokud se podíváte na Get
메서du, po kontrole, zda byla vrácena HttpError
struktura, kterou vrací helper funkce navržená výše, ji vhodně vrátí, a v případě error
ji hromadně předá error callback funkci. Tento callback je přijímán prostřednictvím 생성자.
Následuje kód napsaný s využitím tohoto balíčku.
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}
Jak se vám to líbí? Pokud jednoduše vytvoříte a vrátíte pouze HttpError
jako helper funkci, můžete ji zpracovat registrací callbacku, aby bylo možné zanechat vhodné logy pro každou implementovanou službu a zároveň vrátit odpověď s vhodným error kódem a zprávou z vyššího scope. Navíc, pokud by to bylo nutné, lze to rozšířit a umožnit detailní logování pomocí RequestID
apod.
Odesílání jasných error zpráv
Dokumentem pro tento účel je RFC7807. RFC7807 se používá především k definování a používání následujících prvků.
type
: URI identifikující typ error. Obvykle jde o dokument popisující error.title
: Jednořádkový popis toho, o jaký error se jedná.status
: Stejné jako HTTP Status Code.detail
: Detailní popis daného erroru, který je čitelný pro člověka.instance
: URI, kde došlo k erroru. Například, pokud došlo k erroru přiGET /user/info
, hodnota bude/user/info
.extensions
: Sekundární prvky pro popis erroru, strukturované ve formě JSON Object.- Například v případě
BadRequest
může být zahrnut uživatelský vstup. - Nebo v případě
TooManyRequest
může být zahrnut čas posledního požadavku.
- Například v případě
Aby bylo snadné toto používat, vytvoříme nový soubor ve stejném umístění jako HttpError
, v balíčku httperror
, a vytvoříme RFC7807Error
strukturu, kterou bude možné vytvářet pomocí 메서드 체이닝 patternu.
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 { ... }
Hodnota "about:blank"
pro Type
je výchozí hodnota. Znamená to neexistující stránku. Níže je příklad vytvoření erroru pro chybný požadavek.
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")
Pomocí jednoduchého 메서드 체이닝u lze generovat strukturované error zprávy pro uživatele. Dále, pro využití centralizovaného 라우teru napsaného výše, lze podporovat následující 메서du.
1func (p *RFC7807Error) ToHttpError() *HttpError {
2 jsonBytes, err := json.Marshal(p)
3 if err != nil {
4 // Pokud se marshaling nezdaří, použije se pouze detail
5 return New(p.Status, p.Detail)
6 }
7 return New(p.Status, string(jsonBytes))
8}
Pokud se toto použije beze změny k úpravě výše uvedeného příkladu, bude to vypadat 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ávěr
Tímto způsobem, použitím centralizovaného 라우teru pro zpracování errorů, lze snížit zátěž spojenou s pokaždým ověřováním error kódů a psaním vhodných error zpráv. Dále, poskytnutím strukturovaných error zpráv s využitím RFC7807 lze klientovi pomoci porozumět erroru a zpracovat jej. Touto metodou lze zpracování errorů v HTTP API napsaných v jazyce Go učinit jednodušším a konzistentnějším.
Kód z tohoto článku si můžete prohlédnout v 레포지토리 https://github.com/gosuda/httpwrap.