Minder omslachtige afhandeling van HTTP errors + RFC7807
Overzicht
Bij het genereren van een http api in de Go taal is het meest omslachtige de error handling. Kenmerkend is de volgende 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}
Als er niet veel API's zijn, zullen er geen noemenswaardige ongemakken zijn bij het schrijven op deze manier. Naarmate het aantal API's echter toeneemt en de interne logic complexer wordt, worden drie aspecten hinderlijk.
- Retourneren van de juiste error code
- Aanzienlijk aantal geschreven resultaat logs
- Transmissie van duidelijke error messages
Hoofdtekst
Retourneren van de juiste error code
Uiteraard is punt 1, het retourneren van de juiste error code, een persoonlijke klacht van mij.
Een ervaren ontwikkelaar zal de juiste code vinden en deze elke keer zonder problemen invoegen, maar zowel ik als nog onervaren ontwikkelaars kunnen moeite hebben met het consistent gebruiken van de geschikte error code naarmate de logic complexer wordt en het aantal gevallen toeneemt.
Hier zijn verschillende benaderingen voor, en de meest representatieve methode zal zijn om eerst de API-logicastroom te ontwerpen en vervolgens code te schrijven die de juiste error retourneert.Doe dat
Dit lijkt echter niet de optimale methode voor een menselijke ontwikkelaar die ondersteuning krijgt van een IDE (of Language Server).
Bovendien, aangezien de REST API zelf de betekenis die in de error code besloten ligt maximaal benut, kan een andere methode worden voorgesteld.
Creëer een nieuwe implementatie van de error
interface genaamd HttpError
, die StatusCode
en Message
opslaat.
En biedt de volgende helper function.
1err := httperror.BadRequest("wrong format")
De BadRequest
helper function zal een HttpError
retourneren met StatusCode
ingesteld op 400 en Message
ingesteld als de waarde ontvangen als argument.
Daarnaast is het uiteraard ook mogelijk om helper functions zoals NotImplement
, ServiceUnavailable
, Unauthorized
, PaymentRequired
enzovoorts te raadplegen en toe te voegen met de automatische voltooiingsfunctie.
Dit is sneller dan elke keer het voorbereide ontwerpdocument te controleren en stabieler dan elke keer de error code als een getal in te voeren.Het zit allemaal in de http.StatusCode
constanten, zegt u? Ssst
Aanzienlijk aantal geschreven resultaat logs
Bij het optreden van een error worden uiteraard logs vastgelegd. Bij het vastleggen van logs over of een API is aangeroepen en of een verzoek succesvol was of mislukte, resulteert het vastleggen van logs van het begin tot alle verwachte eindpunten in een toename van het aantal te schrijven code. Dit kan centraal beheerd worden door de handler zelf eenmaal te omwikkelen.
Het volgende is een voorbeeld van het omwikkelen van de 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}
De router struct bevat intern de chi.Router
, zodat het de functionaliteit van chi.Router
ongewijzigd kan gebruiken.
Als u naar de Get
methode kijkt, wordt gecontroleerd of de HttpError
structuur die door de zojuist hierboven voorgestelde helper function wordt geretourneerd, is geretourneerd, waarna deze correct wordt geretourneerd, en in geval van een error
wordt deze uniform doorgegeven aan de error callback function.
Deze callback wordt via de constructor ingevoerd.
De volgende code is geschreven gebruikmakend van dit package.
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) // Router log test: Fout opgetreden
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") // naam is vereist
26 }
27
28 writer.Write([]byte("Hello " + name)) // Hallo + naam
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) // Starten van server mislukt
39 }
40 }()
41
42 <-ctx.Done()
43 svr.Shutdown(context.Background())
44}
Hoe is het?
Door simpelweg HttpError
als een helper function te maken en te retourneren, kan vanuit een hoger scope een passende response met de juiste error code en message worden teruggestuurd, en kan de afhandeling plaatsvinden door een callback te registreren zodat passende logs kunnen worden vastgelegd voor elke implementatieservice.
Indien aanvullend nodig, kan dit worden uitgebreid zodat gedetailleerde logging mogelijk is met behulp van RequestID
en dergelijke.
Transmissie van duidelijke error messages
Hiervoor is RFC7807 een document. RFC7807 definieert en gebruikt voornamelijk de volgende elementen.
type
: Een URI die het type error identificeert. Dit is meestal een document dat de error beschrijft.title
: Een éénregelige beschrijving van welke error het is.status
: Gelijk aan de HTTP Status Code.detail
: Een gedetailleerde beschrijving van de betreffende error, leesbaar voor mensen.instance
: De URI waar de error is opgetreden. Bijvoorbeeld, als een error is opgetreden bijGET /user/info
, zal/user/info
de waarde daarvan zijn.extensions
: Aanvullende elementen om de error te beschrijven, samengesteld in de vorm van een JSON Object.- Bijvoorbeeld, in geval van
BadRequest
, kan de invoer van de gebruiker worden opgenomen. - Of in geval van
TooManyRequest
, kan het tijdstip van het meest recente verzoek worden opgenomen.
- Bijvoorbeeld, in geval van
Om dit gemakkelijk te gebruiken, wordt een nieuw bestand aangemaakt in hetzelfde httperror
package als HttpError
, de RFC7807Error
structuur wordt aangemaakt, en het is mogelijk om deze aan te maken met het method chaining patroon.
1func NewRFC7807Error(status int, title, detail string) *RFC7807Error {
2 return &RFC7807Error{
3 Type: "about:blank", // Default type as per RFC7807 // Standaard type volgens 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"
voor Type
is de standaardwaarde.
Het betekent een niet-bestaande pagina.
Hieronder volgt een voorbeeld van het creëren van een error voor een ongeldig verzoek.
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")
Met eenvoudige method chaining kan een gestructureerde error message voor de gebruiker worden gegenereerd. Bovendien, om gebruik te maken van de hierboven eerder geschreven gecentraliseerde router, kan de volgende methode worden ondersteund.
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 // Als marshalling mislukt, val terug op alleen het gebruik van het detail
5 return New(p.Status, p.Detail)
6 }
7 return New(p.Status, string(jsonBytes))
8}
Door dit direct te gebruiken en het bovenstaande voorbeeld aan te passen, wordt het als volgt.
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) // Router log test: Fout opgetreden
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"). // naam is vereist, Ongeldige Gebruikersinvoer
26 WithType("https://example.com/errors/validation").
27 WithInstance("/api/echo").
28 WithExtension("invalid_field", "name"). // ongeldig_veld, naam
29 WithExtension("expected_format", "string"). // verwachte_indeling, string
30 WithExtension("actual_value", name). // werkelijke_waarde, naam
31 ToHttpError()
32 }
33
34 writer.Write([]byte("Hello " + name)) // Hallo + naam
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) // Starten van server mislukt
45 }
46 }()
47
48 <-ctx.Done()
49 svr.Shutdown(context.Background())
50}
Conclusie
Door zo'n gecentraliseerde router te gebruiken bij het afhandelen van errors, kan de last van het elke keer controleren van de error code en het schrijven van passende error messages worden verminderd. Bovendien kan door RFC7807 te gebruiken en een gestructureerde error message te bieden, de client worden geholpen bij het begrijpen en verwerken van de error. Door middel van dergelijke methoden kan de error handling van een HTTP API geschreven in de Go taal eenvoudiger en consistenter worden gemaakt.
De code van dit artikel kunt u controleren op de gosuda/httpwrap repository.