GoSuda

Hantera HTTP-fel på ett smidigare sätt + RFC7807

By snowmerak
views ...

Ö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.

  1. Returnerande av lämpliga felkoder
  2. Stort antal skrivna resultatloggar
  3. 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.Routers 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 vid GET /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.