Efektívnejšie spracovanie HTTP Errors + RFC7807
Prehľad
Pri vytváraní http API v jazyku Go je najotravnejšou vecou spracovanie chýb. Typickým príkladom je tento druh kódu.
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 nie je veľa API, písanie týmto spôsobom nebude obzvlášť nepohodlné. Avšak, s narastajúcim počtom API a komplexnosťou vnútornej logiky sa stávajú rušivými tri veci.
- Vracanie vhodných error codes
- Vysoký počet zapísaných result logs
- Odosielanie jasných error messages
Hlavná časť
Vracanie vhodných error codes
Samozrejme, bod 1, vracanie vhodných error codes, je mojou osobnou sťažnosťou. Skúsení vývojári si zakaždým nájdu vhodný kód a dobre ho vložia, avšak neskúsení vývojári ako ja, s rastúcou komplexnosťou logiky a frekvenciou, môžu mať ťažkosti s pravidelným používaním vhodných error codes. Existuje na to viacero spôsobov a najtypickejším je vopred navrhnúť tok API logiky a následne napísať kód, ktorý vracia vhodné errors. Urobte to takto
Avšak, toto sa nezdá byť optimálnou metódou pre ľudských vývojárov s asistenciou IDE (alebo Language Server). Navyše, keďže samotné REST API maximálne využíva význam obsiahnutý v error codes, možno navrhnúť iný prístup. Vytvorte novú implementáciu error (error
) interface nazvanú HttpError
, ktorá bude ukladať StatusCode
a Message
. A poskytnite nasledujúce helper functions.
1err := httperror.BadRequest("wrong format")
Helper function BadRequest
vráti HttpError
s StatusCode
nastaveným na 400 a Message
nastaveným na hodnotu prijatú ako argument. Okrem toho, samozrejme, budete môcť vyhľadávať a pridávať helper functions ako NotImplement
, ServiceUnavailable
, Unauthorized
, PaymentRequired
atď. pomocou funkcie automatického doplňovania. Je to rýchlejšie ako zakaždým kontrolovať pripravený design document a bude to stabilnejšie ako zakaždým zadávať error codes číslami. Myslíte, že všetky sú v konštante http.StatusCode
? Ticho
Vysoký počet zapísaných result logs
Pri výskyte error sa prirodzene zanechávajú logs. Keď sa volá API a zanechávate logs o tom, či požiadavka uspela alebo zlyhala, zanechávanie logs od začiatku po všetky očakávané koncové body zvyšuje počet riadkov kódu na napísanie. Obálaním samotného handler jedenkrát, ho možno spravovať centrálne.
Nasleduje príklad obálania chi
router.
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}
Router štruktúra obsahuje vnútorne chi.Router
, takže je nakonfigurovaná tak, aby používala funkcionalitu chi.Router
tak, ako je. Ak sa pozriete na metódu Get
, skontroluje, či bola vrátená štruktúra HttpError
vrátená helper function, ktorú som práve navrhol vyššie, vráti ju vhodne, a ak ide o error
, hromadne ju odovzdá error callback function. Tento callback sa prijíma prostredníctvom constructor.
Nasleduje kód napísaný s využitím tohto package.
1package main
2
3import (
4 "bytes"
5 "context"
6 "errors"
7 "io"
8 "log"
9 "net/http"
10 "os" // Added required import
11 "os/signal" // Added required import
12 "syscall" // Added required import
13
14 "github.com/gosuda/httpwrap/httperror"
15 "github.com/gosuda/httpwrap/wrapper/chiwrap"
16)
17
18func main() {
19 // Handle graceful shutdown
20 ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
21 defer cancel()
22
23 r := chiwrap.NewRouter(func(err error) {
24 log.Printf("Router log test: Error occured: %v", err)
25 })
26 r.Get("/echo", func(writer http.ResponseWriter, request *http.Request) error {
27 name := request.URL.Query().Get("name")
28 if name == "" {
29 return httperror.BadRequest("name is required")
30 }
31
32 writer.Write([]byte("Hello " + name))
33 return nil
34 })
35
36 svr := http.Server{
37 Addr: ":8080",
38 Handler: r,
39 }
40 go func() {
41 // Start the server and log error if it fails
42 if err := svr.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
43 log.Fatalf("Failed to start server: %v", err)
44 }
45 }()
46
47 // Wait for shutdown signal
48 <-ctx.Done()
49 // Shutdown the server gracefully
50 svr.Shutdown(context.Background())
51}
Aký je? Jednoduchým vytvorením HttpError
ako helper function a jej vrátením, to môžete spracovať registráciou callback v nadradenom scope na vrátenie vhodného error code a message ako response a zanechávanie vhodných logs pre každú implementovanú service. Ak je to potrebné, môžete to ďalej rozšíriť, aby ste umožnili detailné logging pomocou RequestID
atď.
Odosielanie jasných error messages
Existuje na to document, RFC7807. RFC7807 primárne definuje a používa nasledujúce elements.
type
: URI, ktorá identifikuje error type. Je to hlavne document vysvetľujúci error.title
: Jednoriadkový description toho, o aký error ide.status
: Rovnaké ako HTTP Status Code.detail
: Detailný description error, ktorý je čitateľný pre človeka.instance
: URI, kde došlo k error. Napríklad, ak došlo k error vGET /user/info
, potom/user/info
bude táto hodnota.extensions
: Sekundárne elements zložené vo forme JSON Object na description error.- Napríklad, v prípade
BadRequest
môže byť zahrnutý user's input. - Alebo v prípade
TooManyRequest
môže zahŕňať čas najnovšej request.
- Napríklad, v prípade
Na jednoduché používanie tohto vytvorte nový file v httperror
package, ktorý je na rovnakom mieste ako HttpError
, vytvorte štruktúru RFC7807Error
a umožnite jej vytvorenie pomocou method chaining pattern.
1import "net/http" // Added required import
2
3func NewRFC7807Error(status int, title, detail string) *RFC7807Error {
4 return &RFC7807Error{
5 Type: "about:blank", // Default type as per RFC7807
6 Title: title,
7 Status: status,
8 Detail: detail,
9 }
10}
11
12func BadRequestProblem(detail string, title ...string) *RFC7807Error {
13 t := "Bad Request"
14 if len(title) > 0 && title[0] != "" {
15 t = title[0]
16 }
17 return NewRFC7807Error(http.StatusBadRequest, t, detail)
18}
19
20func (p *RFC7807Error) WithType(typeURI string) *RFC7807Error { // Placeholder for method implementation
21 // Implementation would modify p.Type and return p
22 return p
23}
24func (p *RFC7807Error) WithInstance(instance string) *RFC7807Error { // Placeholder for method implementation
25 // Implementation would modify p.Instance and return p
26 return p
27}
28func (p *RFC7807Error) WithExtension(key string, value interface{}) *RFC7807Error { // Placeholder for method implementation
29 // Implementation would add key-value to p.Extensions map and return p
30 return p
31}
"about:blank"
pre Type
je default value. Znamená to neexistujúcu page. Nižšie je príklad vytvorenia error pre bad request.
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")
Štruktúrované error messages pre user môžete generovať jednoduchým method chaining. Navyše, na použitie centralizovaného router napísaného vyššie, môže byť podporovaná nasledujúca method.
1import "encoding/json" // Added required import
2
3// ToHttpError converts RFC7807Error to HttpError
4func (p *RFC7807Error) ToHttpError() *HttpError {
5 jsonBytes, err := json.Marshal(p)
6 if err != nil {
7 // If marshaling fails, fall back to just using the detail
8 return New(p.Status, p.Detail) // Assuming New HttpError constructor exists
9 }
10 return New(p.Status, string(jsonBytes)) // Assuming New HttpError constructor exists
11}
Ak to použijete tak, ako je, 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 "os" // Added required import
11 "os/signal" // Added required import
12 "syscall" // Added required import
13
14 "github.com/gosuda/httpwrap/httperror"
15 "github.com/gosuda/httpwrap/wrapper/chiwrap"
16)
17
18func main() {
19 // Handle graceful shutdown
20 ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
21 defer cancel()
22
23 r := chiwrap.NewRouter(func(err error) {
24 log.Printf("Router log test: Error occured: %v", err)
25 })
26 r.Get("/echo", func(writer http.ResponseWriter, request *http.Request) error {
27 name := request.URL.Query().Get("name")
28 if name == "" {
29 return httperror.BadRequestProblem("name is required", "Bad User Input").
30 WithType("https://example.com/errors/validation").
31 WithInstance("/api/echo").
32 WithExtension("invalid_field", "name").
33 WithExtension("expected_format", "string").
34 WithExtension("actual_value", name).
35 ToHttpError()
36 }
37
38 writer.Write([]byte("Hello " + name))
39 return nil
40 })
41
42 svr := http.Server{
43 Addr: ":8080",
44 Handler: r,
45 }
46 go func() {
47 // Start the server and log error if it fails
48 if err := svr.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
49 log.Fatalf("Failed to start server: %v", err)
50 }
51 }()
52
53 // Wait for shutdown signal
54 <-ctx.Done()
55 // Shutdown the server gracefully
56 svr.Shutdown(context.Background())
57}
Záver
Spracovaním errors pomocou centralizovaného router týmto spôsobom môžete znížiť záťaž spojenú s každým kontrolovaním error codes a písaním vhodných error messages. Navyše, poskytovaním štruktúrovaných error messages s využitím RFC7807, môžete pomôcť clients pochopiť a spracovať errors. Týmito methods možno spracovanie errors pre HTTP API napísané v jazyku Go urobiť pohodlnejším a konzistentnejším.
Code pre tento article nájdete v gosuda/httpwrap repository.