Betrachten Sie die Fehlerbehandlung auf der GraphQL Server-Seite mit gqlgen, einer von Golang erstellten GraphQL-Bibliothek, die "typsicheres GraphQL für Go" beansprucht.
--11th "Antwort auf N + 1-Problem mit Datenladern"
$ cat /etc/os-release
NAME="Ubuntu"
VERSION="18.04.5 LTS (Bionic Beaver)"
$ go version
go version go1.15.2 linux/amd64
v0.13.0
IDE - Goland
GoLand 2020.2.3
Build #GO-202.7319.61, built on September 16, 2020
https://github.com/sky0621/study-gqlgen/tree/v0.2
Probieren wir einige Methoden zum Umgang mit GraphQL-Fehlern auf der Serverseite mit gqlgen aus.
Listen Sie einige Muster auf.
server.go
package main
import (
"log"
"net/http"
"github.com/99designs/gqlgen/graphql/handler"
"github.com/99designs/gqlgen/graphql/playground"
"github.com/sky0621/study-gqlgen/errorhandling/graph"
"github.com/sky0621/study-gqlgen/errorhandling/graph/generated"
)
func main() {
srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{}}))
http.Handle("/", playground.Handler("GraphQL playground", "/query"))
http.Handle("/query", srv)
log.Fatal(http.ListenAndServe(":8080", nil))
}
schema.graphqls
type Query {
normalReturn: [Todo!]!
errorReturn: [Todo!]!
customErrorReturn: [Todo!]!
customErrorReturn2: [Todo!]!
customErrorReturn3: [Todo!]!
customErrorReturn4: [Todo!]!
panicReturn: [Todo!]!
}
type Todo {
id: ID!
text: String!
}
go:schema.resolvers.go
package graph
// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.
import (
"context"
"errors"
"fmt"
"time"
"github.com/99designs/gqlgen/graphql"
"github.com/sky0621/study-gqlgen/errorhandling/graph/generated"
"github.com/sky0621/study-gqlgen/errorhandling/graph/model"
"github.com/vektah/gqlparser/v2/gqlerror"
)
func (r *queryResolver) NormalReturn(ctx context.Context) ([]*model.Todo, error) {
return []*model.Todo{
{ID: "001", Text: "something1"},
{ID: "002", Text: "something2"},
}, nil
}
func (r *queryResolver) ErrorReturn(ctx context.Context) ([]*model.Todo, error) {
return nil, errors.New("error occurred")
}
func (r *queryResolver) CustomErrorReturn(ctx context.Context) ([]*model.Todo, error) {
return nil, gqlerror.Errorf("custom error")
}
func (r *queryResolver) CustomErrorReturn2(ctx context.Context) ([]*model.Todo, error) {
graphql.AddError(ctx, gqlerror.Errorf("add error"))
graphql.AddErrorf(ctx, "add error2: %s", time.Now().String())
return nil, nil
}
func (r *queryResolver) CustomErrorReturn3(ctx context.Context) ([]*model.Todo, error) {
return nil, &gqlerror.Error{
Extensions: map[string]interface{}{
"code": "A00001",
"field": "text",
"value": "Toilettenreinigung",
},
}
}
func (r *queryResolver) CustomErrorReturn4(ctx context.Context) ([]*model.Todo, error) {
return nil, &gqlerror.Error{
Extensions: map[string]interface{}{
"errors": []map[string]interface{}{
{
"code": "A00001",
"field": "text",
"value": "Toilettenreinigung",
},
{
"code": "A00002",
"field": "text",
"value": "Toilettenreinigung",
},
},
},
}
}
func (r *queryResolver) PanicReturn(ctx context.Context) ([]*model.Todo, error) {
panic(fmt.Errorf("panic occurred"))
}
// Query returns generated.QueryResolver implementation.
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }
type queryResolver struct{ *Resolver }
In Bezug auf die Antwort in GraphQL. Im Fall eines normalen Systems ist die Struktur wie folgt.
{
"data": {
〜〜〜〜
}
}
Wenn der Resolver einen Fehler zurückgibt, sieht die Struktur wie folgt aus.
{
"errors": [
{
"message": 〜〜〜〜,
"path": [〜〜〜〜]
}
],
"data": null
}
Darüber hinaus wurde nachstehend darauf hingewiesen. https://gqlgen.com/reference/errors/
func (r *queryResolver) NormalReturn(ctx context.Context) ([]*model.Todo, error) {
return []*model.Todo{
{ID: "001", Text: "something1"},
{ID: "002", Text: "something2"},
}, nil
}
Normales System. Die eingestellten Daten werden zurückgegeben.
func (r *queryResolver) ErrorReturn(ctx context.Context) ([]*model.Todo, error) {
return nil, errors.New("error occurred")
}
Die angegebene Fehlermeldung wird in message
geladen.
path
wird willkürlich angegeben.
func (r *queryResolver) CustomErrorReturn(ctx context.Context) ([]*model.Todo, error) {
return nil, gqlerror.Errorf("custom error")
}
Immerhin wird die angegebene Fehlermeldung in message
geladen.
Die Struktur entspricht dem Muster, das einen Go-Standardfehler zurückgibt.
func (r *queryResolver) CustomErrorReturn2(ctx context.Context) ([]*model.Todo, error) {
graphql.AddError(ctx, gqlerror.Errorf("add error"))
graphql.AddErrorf(ctx, "add error2: %s", time.Now().String())
return nil, nil
}
Die zwei angegebenen Fehlertypen werden in jede "Nachricht" geladen. Ich bin ein wenig besorgt, dass "data" ein leeres Slice anstelle von "null" zurückgibt, im Gegensatz zu dem Zeitpunkt, als bisher ein Fehler aufgetreten ist. (Wahrscheinlich, weil "return" keinen Fehler zurückgegeben hat.)
func (r *queryResolver) CustomErrorReturn3(ctx context.Context) ([]*model.Todo, error) {
return nil, &gqlerror.Error{
Extensions: map[string]interface{}{
"code": "A00001",
"field": "text",
"value": "Toilettenreinigung",
},
}
}
In message
wird nichts geladen, und der Fehlerinhalt wird in den vorbereiteten Erweiterungen
mit einem dienstspezifischen Ausdruck definiert.
Da es sich um "map [string] interface {}" handelt, kann jede Struktur verwendet werden.
Dies ermöglicht es dem Front-End, das die Antwort empfängt, eine Fehlermeldung gemäß dem "Code" zu generieren und diese dem Endbenutzer anzuzeigen.
func (r *queryResolver) CustomErrorReturn4(ctx context.Context) ([]*model.Todo, error) {
return nil, &gqlerror.Error{
Extensions: map[string]interface{}{
"errors": []map[string]interface{}{
{
"code": "A00001",
"field": "text",
"value": "Toilettenreinigung",
},
{
"code": "A00002",
"field": "text",
"value": "Toilettenreinigung",
},
},
},
}
}
Sie möchten nicht immer einen Fehler zurückgeben. Natürlich ist es möglich, mehrere Fehler zurückzugeben, wenn Sie es in Form eines solchen Ausschnitts der Karte behalten.
func (r *queryResolver) PanicReturn(ctx context.Context) ([]*model.Todo, error) {
panic(fmt.Errorf("panic occurred"))
}
Die Nachricht, die geladen wird, wenn "Panik" auftritt, wird ignoriert und der "interne Systemfehler" wird in "Nachricht" geladen.
Sofern es sich nicht um einen sehr kleinen Dienst handelt, ist meines Erachtens ein dienstspezifischer Ausdruck für die Fehlerbehandlung erforderlich. In gqlgen gibt es einen Mechanismus zum Hinzufügen einer Verarbeitung durch Verknüpfen von "wenn ein Fehler auftritt" und "wenn eine Panik auftritt", wenn ein Handler generiert wird. Mit diesem Mechanismus Der Resolver gibt die eindeutig definierte Fehlerstruktur an den Service zurück (wenn ein Fehler auftritt), bindet sie an den Handler, verarbeitet die Fehlerstruktur und implementiert sie als Antwort.
schema.graphqls
type Query {
errorPresenter: [Todo!]!
panicHandler: [Todo!]!
}
type Todo {
id: ID!
text: String!
}
schema.resolvers.go Erstellen Sie "AppError" als dienstspezifische Fehlerstruktur. Die Struktur wird vom Resolver zurückgegeben.
package graph
import (
"context"
"fmt"
"github.com/sky0621/study-gqlgen/errorhandling2/graph/generated"
"github.com/sky0621/study-gqlgen/errorhandling2/graph/model"
)
type ErrorCode string
const (
ErrorCodeRequired ErrorCode = "1001"
ErrorCodeUnexpectedSituation ErrorCode = "9999"
)
type AppError struct {
Code ErrorCode
Msg string
}
func (e AppError) Error() string {
return fmt.Sprintf("[%s]%s", e.Code, e.Msg)
}
func (r *queryResolver) ErrorPresenter(ctx context.Context) ([]*model.Todo, error) {
return nil, AppError{
Code: ErrorCodeRequired,
Msg: "text is none",
}
}
func (r *queryResolver) PanicHandler(ctx context.Context) ([]*model.Todo, error) {
panic("unexpected situation")
}
// Query returns generated.QueryResolver implementation.
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }
type queryResolver struct{ *Resolver }
server.go In der von "SetErrorPresenter ()" festgelegten Funktion wird der vom Resolver ausgelöste Fehler empfangen, und wenn es sich um "AppError" handelt, wird er erneut in die Struktur von "* gqlerror.Error {}" bearbeitet. Übrigens wird "SetRecoverFunc ()" auch vorbereitet und bearbeitet, so dass angenommen wird, dass der Fehlerausdruck auch im Falle einer Panik dienstspezifisch ist.
package main
import (
"context"
"errors"
"log"
"net/http"
"github.com/99designs/gqlgen/graphql"
"github.com/vektah/gqlparser/v2/gqlerror"
"github.com/99designs/gqlgen/graphql/handler"
"github.com/99designs/gqlgen/graphql/playground"
"github.com/sky0621/study-gqlgen/errorhandling2/graph"
"github.com/sky0621/study-gqlgen/errorhandling2/graph/generated"
)
func main() {
srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{}}))
srv.SetErrorPresenter(func(ctx context.Context, e error) *gqlerror.Error {
err := graphql.DefaultErrorPresenter(ctx, e)
var appErr graph.AppError
if errors.As(err, &appErr) {
return &gqlerror.Error{
Message: appErr.Msg,
Extensions: map[string]interface{}{
"code": appErr.Code,
},
}
}
return err
})
srv.SetRecoverFunc(func(ctx context.Context, err interface{}) error {
return &gqlerror.Error{
Extensions: map[string]interface{}{
"code": graph.ErrorCodeUnexpectedSituation,
"cause": err,
},
}
})
http.Handle("/", playground.Handler("GraphQL playground", "/query"))
http.Handle("/query", srv)
log.Fatal(http.ListenAndServe(":8080", nil))
}
Gut, dass der Fehlercode und die Fehlermeldung sortiert werden können.
Da der Fehler, wenn Panik auftritt, als "Ursache" hinzugefügt wird, ist der Fehlerinhalt der Quelle auch ordnungsgemäß in der Antwort enthalten.
Es gibt verschiedene Arten von Fehlern, wie z. B. Validierungsfehler, Authentifizierungsfehler, DB-Verbindungsfehler usw., und ich denke, dass sich die für die Fehlerstruktur erforderlichen Elemente ändern werden. In einigen Fällen reicht es aus, einen einzelnen Fehler zurückzugeben, in anderen Fällen muss jedes Fehlerelement (wie ein Validierungsfehler) zurückgegeben werden, und daher müssen mehrere Fehler zurückgegeben werden. Basierend auf dieser Situation werden wir versuchen, Fehler so universell wie möglich zu behandeln.
apperror.go
package graph
import (
"context"
"net/http"
"github.com/vektah/gqlparser/v2/gqlerror"
"github.com/99designs/gqlgen/graphql"
)
type AppError struct {
httpStatusCode int // http.Geben Sie StatusCodeXXXXXXX ein
appErrorCode AppErrorCode //Dienstspezifischer Fehlercode
/*
*Im Folgenden werden Elemente aufgeführt, die nicht für alle Fehlerausdrücke erforderlich sind (können optional festgelegt werden).
*/
field string
value string
}
func (e *AppError) AddGraphQLError(ctx context.Context) {
extensions := map[string]interface{}{
"status_code": e.httpStatusCode,
"error_code": e.appErrorCode,
}
if e.field != "" {
extensions["field"] = e.field
}
if e.value != "" {
extensions["value"] = e.value
}
graphql.AddError(ctx, &gqlerror.Error{
Message: "",
Extensions: extensions,
})
}
func NewAppError(httpStatusCode int, appErrorCode AppErrorCode, opts ...AppErrorOption) *AppError {
a := &AppError{
httpStatusCode: httpStatusCode,
appErrorCode: appErrorCode,
}
for _, o := range opts {
o(a)
}
return a
}
//Für Authentifizierungsfehler
func NewAuthenticationError(opts ...AppErrorOption) *AppError {
return NewAppError(http.StatusUnauthorized, AppErrorCodeAuthenticationFailure, opts...)
}
//Für Autorisierungsfehler
func NewAuthorizationError(opts ...AppErrorOption) *AppError {
return NewAppError(http.StatusForbidden, AppErrorCodeAuthorizationFailure, opts...)
}
//Für Validierungsfehler
func NewValidationError(field, value string, opts ...AppErrorOption) *AppError {
options := []AppErrorOption{WithField(field), WithValue(value)}
for _, opt := range opts {
options = append(options, opt)
}
return NewAppError(http.StatusBadRequest, AppErrorCodeValidationFailure, options...)
}
//Für andere Fehler
func NewInternalServerError(opts ...AppErrorOption) *AppError {
return NewAppError(http.StatusInternalServerError, AppErrorCodeUnexpectedFailure, opts...)
}
type AppErrorCode string
// MEMO:Abhängig von der Definition des Dienstes kann das Codesystem anstelle einer aussagekräftigen Zeichenfolge festgelegt werden.
const (
//Authentifizierungsfehler
AppErrorCodeAuthenticationFailure AppErrorCode = "AUTHENTICATION_FAILURE"
//Autorisierungsfehler
AppErrorCodeAuthorizationFailure AppErrorCode = "AUTHORIZATION_FAILURE"
//Validierungsfehler
AppErrorCodeValidationFailure AppErrorCode = "VALIDATION_FAILURE"
//Andere unerwartete Fehler
AppErrorCodeUnexpectedFailure AppErrorCode = "UNEXPECTED_FAILURE"
)
type AppErrorOption func(*AppError)
func WithField(v string) AppErrorOption {
return func(a *AppError) {
a.field = v
}
}
func WithValue(v string) AppErrorOption {
return func(a *AppError) {
a.value = v
}
}
Erstellen Sie zunächst "AppError" als dienstspezifische Fehlerstruktur. Ich denke, dass das, was Sie als Fehlerelement haben, vom Dienst abhängt, aber vorerst werden die folgenden beiden als wesentlich definiert, unabhängig vom Inhalt des Fehlers.
--HTTP-Statuscode
type AppError struct {
httpStatusCode int // http.Geben Sie StatusCodeXXXXXXX ein
appErrorCode AppErrorCode //Dienstspezifischer Fehlercode
〜〜
}
func NewAppError(httpStatusCode int, appErrorCode AppErrorCode, opts ...AppErrorOption) *AppError {
a := &AppError{
httpStatusCode: httpStatusCode,
appErrorCode: appErrorCode,
}
〜〜
}
In Fällen wie Validierungsfehlern, in denen Sie Informationen zu "Welcher Wert in welchem Feld" wünschen, sollte die Struktur die erforderlichen Elemente für jedes Muster enthalten (auch wenn es redundant ist).
type AppError struct {
〜〜
/*
*Im Folgenden werden Elemente aufgeführt, die nicht für alle Fehlerausdrücke erforderlich sind (können optional festgelegt werden).
*/
field string
value string
}
Ich möchte jedoch nicht jedes Mal die Funktion "Neu" ändern (dh alle Aufrufer ändern), wenn ich diese Elemente in Zukunft hinzufügen muss. Daher [Funktionsoptionsmuster](https: //commandcenter.blogspot] .com / 2014/01 / self-referential-functions-and-design.html) wird verwendet.
Definieren Sie eine Funktion zum Anwenden von Optionen und übergeben Sie sie als variables Argument in der Funktion "Neu" (dh Sie müssen dies nicht tun).
type AppErrorOption func(*AppError)
func NewAppError(httpStatusCode int, appErrorCode AppErrorCode, opts ...AppErrorOption) *AppError {
a := &AppError{
httpStatusCode: httpStatusCode,
appErrorCode: appErrorCode,
}
for _, o := range opts {
o(a)
}
return a
}
Die folgenden beiden werden als Anwendungsbeispiele für "AppErrorOption" erstellt.
func WithField(v string) AppErrorOption {
return func(a *AppError) {
a.field = v
}
}
func WithValue(v string) AppErrorOption {
return func(a *AppError) {
a.value = v
}
}
Auf diese Weise kann die Fehlerstruktur erweitert werden, ohne dass der vorhandene Aufrufer geändert wird, auch wenn in Zukunft weitere Elemente zur Fehlerstruktur hinzugefügt werden.
Ich denke, es ist ziemlich schwierig, diesen Mechanismus auf den ersten Blick zu verstehen (der Hauptgrund ist, dass die Erklärung schlampig ist), daher möchte ich, dass Sie mit "Functional Option Pattern" googeln und einen einfachen Erklärungsartikel lesen. .. ..
Definieren Sie anschließend den dienstspezifischen Fehlercode wie folgt:
type AppErrorCode string
// MEMO:Abhängig von der Definition des Dienstes kann das Codesystem anstelle einer aussagekräftigen Zeichenfolge festgelegt werden.
const (
//Authentifizierungsfehler
AppErrorCodeAuthenticationFailure AppErrorCode = "AUTHENTICATION_FAILURE"
//Autorisierungsfehler
AppErrorCodeAuthorizationFailure AppErrorCode = "AUTHORIZATION_FAILURE"
//Validierungsfehler
AppErrorCodeValidationFailure AppErrorCode = "VALIDATION_FAILURE"
//Andere unerwartete Fehler
AppErrorCodeUnexpectedFailure AppErrorCode = "UNEXPECTED_FAILURE"
)
Es ist in Ordnung, wenn Sie für jeden Fehlertyp eine dedizierte "Neu" -Funktion vorbereiten.
//Für Authentifizierungsfehler
func NewAuthenticationError(opts ...AppErrorOption) *AppError {
return NewAppError(http.StatusUnauthorized, AppErrorCodeAuthenticationFailure, opts...)
}
//Für Autorisierungsfehler
func NewAuthorizationError(opts ...AppErrorOption) *AppError {
return NewAppError(http.StatusForbidden, AppErrorCodeAuthorizationFailure, opts...)
}
//Für Validierungsfehler
func NewValidationError(field, value string, opts ...AppErrorOption) *AppError {
options := []AppErrorOption{WithField(field), WithValue(value)}
for _, opt := range opts {
options = append(options, opt)
}
return NewAppError(http.StatusBadRequest, AppErrorCodeValidationFailure, options...)
}
//Für andere Fehler
func NewInternalServerError(opts ...AppErrorOption) *AppError {
return NewAppError(http.StatusInternalServerError, AppErrorCodeUnexpectedFailure, opts...)
}
Wenn Sie als Test für jeden Typ einen Fehler generieren und ihn als GraphQL-Fehler hinzufügen, sieht er folgendermaßen aus. (Natürlich werden Authentifizierungsfehler mit Benutzer-IDs geladen, aber vorerst handelt es sich um ein Beispiel.)
go:schema.resolvers.go
package graph
import (
"context"
"github.com/sky0621/study-gqlgen/errorhandling3/graph/generated"
"github.com/sky0621/study-gqlgen/errorhandling3/graph/model"
)
func (r *queryResolver) CustomErrorReturn(ctx context.Context) ([]*model.Todo, error) {
//Authentifizierungsfehler hinzugefügt
NewAuthenticationError().AddGraphQLError(ctx)
//Autorisierungsfehler hinzugefügt
NewAuthorizationError().AddGraphQLError(ctx)
//Validierungsfehler hinzugefügt
NewValidationError("name", "taro").AddGraphQLError(ctx)
//Andere Fehler hinzugefügt
NewInternalServerError().AddGraphQLError(ctx)
return nil, nil
}
// Query returns generated.QueryResolver implementation.
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }
type queryResolver struct{ *Resolver }
schema.graphqls
type Query {
customErrorReturn: [Todo!]!
}
type Todo {
id: ID!
text: String!
}
server.go Diesmal gibt es keine Vorbereitung für den Handler.
package main
import (
"log"
"net/http"
"github.com/99designs/gqlgen/graphql/handler"
"github.com/99designs/gqlgen/graphql/playground"
"github.com/sky0621/study-gqlgen/errorhandling3/graph"
"github.com/sky0621/study-gqlgen/errorhandling3/graph/generated"
)
func main() {
srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{}}))
http.Handle("/", playground.Handler("GraphQL playground", "/query"))
http.Handle("/query", srv)
log.Fatal(http.ListenAndServe(":8080", nil))
}
Wie Sie sehen können, hat es ein einheitliches Format, daher sollte es auf der Empfangsseite einfach zu handhaben sein. .. ..
Ich habe mehrere Vorschläge zur Fehlerbehandlung vorgestellt, von einfachen, die vorerst nur einen Fehler zurückgeben müssen, bis zu einer Methode, die unter Berücksichtigung der Vielseitigkeit eine dienstspezifische Fehlerstruktur definiert. Natürlich kann es neben den hier gezeigten auch andere Muster geben, und die hier aufgeführten befinden sich nicht auf Produktionsebene. Wenn Sie es als einen Dienst betrachten, ist auch der Umgang mit dem Fehlerinhalt, der hier am Frontend zurückgegeben wird, ein wichtiger Faktor.
Recommended Posts