Hantera HTTP-fel på ett smidigare sätt + RFC7807
Översikt
När man skapar http API:er i Go-språket är det mest besvärliga felhantering. Typiskt sett finns kod som denna.
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}
Om det inte finns många API:er, kommer det inte att vara någon särskild olägenhet med att skriva på detta sätt. Men i takt med att antalet API:er ökar och den interna logiken blir mer komplex, blir tre saker störande.
- Returnerande av lämpliga felkoder
- Stort antal skrivna resultatloggar
- Sändning av tydliga felmeddelanden
Huvuddel
Returnerande av lämpliga felkoder
Naturligtvis är punkt 1, returnerande av lämpliga felkoder, en personlig klagomålspunkt för mig.
Erfarna utvecklare kommer att hitta den lämpliga koden och infoga den väl varje gång, men oerfarna utvecklare som jag själv kan ha svårigheter att regelbundet använda lämpliga felkoder i takt med att logiken blir komplex och antalet anrop ökar.
Det kommer att finnas flera metoder för detta, och den mest typiska är förmodligen att designa API-logikflödet i förväg och sedan skriva kod för att returnera lämpliga fel.Gör så
Detta verkar dock inte vara den optimala metoden för mänskliga utvecklare som får hjälp av en IDE (eller Language Server).
Eftersom REST API i sig utnyttjar den mening som finns i felkoder maximalt, kan en annan metod föreslås.
Skapa en ny implementation av error (error
)-gränssnittet kallad HttpError
för att lagra StatusCode
och Message
.
Och tillhandahåll hjälparfunktioner som följande.
1err := httperror.BadRequest("wrong format")
Hjälparfunktionen BadRequest
kommer att returnera en HttpError
med StatusCode
satt till 400 och Message
satt till det värde som mottagits som argument.
Utöver detta kommer du naturligtvis att kunna slå upp och lägga till hjälparfunktioner som NotImplement
, ServiceUnavailable
, Unauthorized
, PaymentRequired
, etc., med hjälp av auto-komplettering.
Detta kommer att vara snabbare än att kontrollera det förberedda designunderlaget varje gång, och mer stabilt än att ange felkoder som siffror varje gång.Du menar att det finns i http.StatusCode
-konstanterna? Tyst
Stort antal skrivna resultatloggar
När ett fel inträffar, lämnas naturligtvis loggar. När ett API anropas och loggar lämnas om huruvida begäran lyckades eller misslyckades, ökar mängden kod att skriva om man lämnar loggar vid alla förväntade avslutningspunkter från början. Genom att linda in själva handlaren en gång, kan den hanteras centralt.
Följande är ett exempel på hur man lindar in chi
-routern.
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-strukturen innehåller chi.Router
internt, så den är konfigurerad att använda chi.Router
s funktionalitet som den är.
Om man tittar på Get
-metoden, kontrollerar den om HttpError
-strukturen som returneras av den hjälparfunktion som föreslogs strax ovan har returnerats, returnerar den lämpligt, och om det är en error
, skickas den enhetligt till fel-callback-funktionen.
Denna callback tas emot via konstruktorn.
Följande är kod skriven med hjälp av detta paket.
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}
Hur är det?
Om du bara skapar och returnerar enbart HttpError
som en hjälparfunktion, kan du hantera det genom att registrera en callback i det högre skopet för att lämna lämpliga loggar för varje implementerad tjänst samtidigt som du returnerar ett svar med lämplig felkod och meddelande.
Om ytterligare behov finns, kan du utöka det för att möjliggöra detaljerad loggning med hjälp av RequestID
, etc.
Sändning av tydliga felmeddelanden
RFC7807 är ett dokument för detta. RFC7807 används huvudsakligen genom att definiera följande element.
type
: URI som identifierar feltypen. Det är huvudsakligen ett dokument som förklarar felet.title
: En enradig beskrivning av vad felet är.status
: Samma som HTTP Status Code.detail
: En detaljerad beskrivning av felet som är läsbar för människor.instance
: URI:n där felet inträffade. Till exempel, om ett fel inträffade vidGET /user/info
, skulle/user/info
vara det värdet.extensions
: Sekundära element för att beskriva felet, strukturerade som ett JSON Object.- Till exempel, i fallet med
BadRequest
, kan användarens input inkluderas. - Eller i fallet med
TooManyRequest
, kan tidpunkten för den senaste begäran inkluderas.
För att enkelt använda detta, skapa en ny fil i httperror
-paketet, som ligger på samma plats som HttpError
, skapa strukturen RFC7807Error
och möjliggör skapande med ett metodkedjningsmönster.
1func NewRFC7807Error(status int, title, detail string) *RFC7807Error {
2 return &RFC7807Error{
3 Type: "about:blank", // Standardtyp enligt 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"
för Type
är standardvärdet.
Det betyder en icke-existerande sida.
Nedan är ett exempel på felgenerering för en felaktig begäran.
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")
Du kan generera strukturerade felmeddelanden för användaren med enkel metodkedjning. Dessutom, för att använda den centraliserade routern som skrevs ovan, kan följande metod stödjas.
1func (p *RFC7807Error) ToHttpError() *HttpError {
2 jsonBytes, err := json.Marshal(p)
3 if err != nil {
4 // Om marshaling misslyckas, fall tillbaka till att bara använda detaljen
5 return New(p.Status, p.Detail)
6 }
7 return New(p.Status, string(jsonBytes))
8}
Om du modifierar exemplet ovan genom att använda detta som det är, blir det så här.
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}
Slutsats
Genom att hantera fel med hjälp av en centraliserad router på detta sätt, kan bördan att kontrollera felkoder och skriva lämpliga felmeddelanden varje gång reduceras. Dessutom, genom att tillhandahålla strukturerade felmeddelanden med hjälp av RFC7807, kan det hjälpa klienter att förstå och hantera fel. Genom dessa metoder kan felhantering för HTTP API:er skrivna i Go-språket göras mer bekväm och konsekvent.
Koden för denna artikel kan hittas i gosuda/httpwrap-repot.