Considérez la gestion des erreurs du côté du serveur GraphQL en utilisant gqlgen, qui est une bibliothèque GraphQL créée par Golang qui revendique «Type-safe GraphQL for Go».
--11e "Réponse au problème N + 1 à l'aide de chargeurs de données"
$ 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
Essayons quelques méthodes pour gérer les erreurs GraphQL côté serveur à l'aide de gqlgen.
Énumérez quelques modèles.
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": "Nettoyage des toilettes",
},
}
}
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": "Nettoyage des toilettes",
},
{
"code": "A00002",
"field": "text",
"value": "Nettoyage des toilettes",
},
},
},
}
}
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 }
Concernant la réponse dans GraphQL. Dans le cas d'un système normal, la structure est la suivante.
{
"data": {
〜〜〜〜
}
}
Si le résolveur renvoie une erreur, la structure sera la suivante.
{
"errors": [
{
"message": 〜〜〜〜,
"path": [〜〜〜〜]
}
],
"data": null
}
En outre, il a été mentionné ci-dessous. 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
}
Système normal. Les données d'ensemble sont renvoyées.
func (r *queryResolver) ErrorReturn(ctx context.Context) ([]*model.Todo, error) {
return nil, errors.New("error occurred")
}
Le message d'erreur spécifié est chargé dans message
.
path
est donné arbitrairement.
func (r *queryResolver) CustomErrorReturn(ctx context.Context) ([]*model.Todo, error) {
return nil, gqlerror.Errorf("custom error")
}
Après tout, le message d'erreur spécifié est chargé dans message
.
La structure est la même que le modèle qui renvoie une erreur standard go.
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
}
Les deux types d'erreurs spécifiés sont chargés dans chaque message
.
Je suis un peu inquiet que "data" renvoie une tranche vide au lieu de "null", contrairement à ce qu'une erreur s'est produite jusqu'à présent.
(Probablement parce que return
n'a pas renvoyé d'erreur.)
func (r *queryResolver) CustomErrorReturn3(ctx context.Context) ([]*model.Todo, error) {
return nil, &gqlerror.Error{
Extensions: map[string]interface{}{
"code": "A00001",
"field": "text",
"value": "Nettoyage des toilettes",
},
}
}
Rien n'est chargé dans «message», et le contenu de l'erreur est défini dans les «extensions» préparées avec une expression spécifique au service.
Puisqu'il s'agit de map [string] interface {}
, n'importe quelle structure peut être utilisée.
Cela permet au frontal qui reçoit la réponse de générer un message d'erreur en fonction du «code» et de l'afficher à l'utilisateur final.
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": "Nettoyage des toilettes",
},
{
"code": "A00002",
"field": "text",
"value": "Nettoyage des toilettes",
},
},
},
}
}
Vous ne voulez pas toujours renvoyer une erreur. Bien sûr, il est possible de renvoyer plusieurs erreurs si vous le conservez sous la forme d'une tranche de la carte comme celle-ci.
func (r *queryResolver) PanicReturn(ctx context.Context) ([]*model.Todo, error) {
panic(fmt.Errorf("panic occurred"))
}
Ignorez le message chargé lorsque "panic" se produit et qu'une "erreur système interne" est chargée dans "message".
À moins qu'il ne s'agisse d'un très petit service, je pense qu'une expression de gestion des erreurs spécifique au service sera nécessaire. Dans gqlgen, il existe un mécanisme pour ajouter un traitement en accrochant «lorsqu'une erreur se produit» et «lorsqu'une panique se produit» lorsqu'un gestionnaire est généré. En utilisant ce mécanisme, Le résolveur renvoie la structure d'erreur définie de manière unique au service (lorsqu'une erreur se produit), la relie au gestionnaire, traite la structure d'erreur et l'implémente en tant que réponse.
schema.graphqls
type Query {
errorPresenter: [Todo!]!
panicHandler: [Todo!]!
}
type Todo {
id: ID!
text: String!
}
schema.resolvers.go Créez ʻAppError` comme structure d'erreur spécifique au service. La structure est renvoyée par le résolveur.
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
Dans la fonction définie par SetErrorPresenter ()
, l'erreur lancée par le résolveur est reçue, et si elle est ʻAppError, elle est rééditée dans la structure de
* gqlerror.Error {} . À propos,
SetRecoverFunc ()` est également préparé et édité afin que l'expression d'erreur soit supposée être spécifique au service même en cas de panique.
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))
}
Heureusement, le code d'erreur et le message d'erreur peuvent être triés.
Puisque l'erreur en cas de panique est ajoutée comme «cause», le contenu d'erreur de la source est également inclus correctement dans la réponse.
Il existe différents types d'erreurs, telles que les erreurs de validation, les erreurs d'authentification, les erreurs de connexion à la base de données, etc., et je pense que les éléments requis pour la structure des erreurs vont changer. Dans certains cas, il suffit de renvoyer une seule erreur, dans d'autres cas, il est nécessaire de renvoyer chaque élément d'erreur (comme une erreur de validation) et, par conséquent, il est nécessaire de renvoyer plusieurs erreurs. Sur la base de cette situation, nous essaierons de gérer les erreurs aussi universellement que possible.
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.Entrez StatusCodeXXXXXXX
appErrorCode AppErrorCode //Code d'erreur spécifique au service
/*
*Ci-dessous, les éléments qui ne sont pas essentiels pour toutes les expressions d'erreur (peuvent être définis en option)
*/
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
}
//Pour les erreurs d'authentification
func NewAuthenticationError(opts ...AppErrorOption) *AppError {
return NewAppError(http.StatusUnauthorized, AppErrorCodeAuthenticationFailure, opts...)
}
//Pour erreur d'autorisation
func NewAuthorizationError(opts ...AppErrorOption) *AppError {
return NewAppError(http.StatusForbidden, AppErrorCodeAuthorizationFailure, opts...)
}
//Pour erreur de validation
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...)
}
//Pour d'autres erreurs
func NewInternalServerError(opts ...AppErrorOption) *AppError {
return NewAppError(http.StatusInternalServerError, AppErrorCodeUnexpectedFailure, opts...)
}
type AppErrorCode string
// MEMO:Selon la définition du service, le système de code peut être choisi plutôt qu'une chaîne de caractères significative.
const (
//Erreur d'authentification
AppErrorCodeAuthenticationFailure AppErrorCode = "AUTHENTICATION_FAILURE"
//Erreur d'autorisation
AppErrorCodeAuthorizationFailure AppErrorCode = "AUTHORIZATION_FAILURE"
//Erreur de validation
AppErrorCodeValidationFailure AppErrorCode = "VALIDATION_FAILURE"
//Autres erreurs inattendues
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
}
}
Commencez par créer ʻAppError` comme structure d'erreur spécifique au service. Je pense que ce que vous avez comme élément d'erreur dépend du service, mais pour le moment, les deux suivants sont définis comme essentiels quel que soit le contenu de l'erreur.
type AppError struct {
httpStatusCode int // http.Entrez StatusCodeXXXXXXX
appErrorCode AppErrorCode //Code d'erreur spécifique au service
〜〜
}
func NewAppError(httpStatusCode int, appErrorCode AppErrorCode, opts ...AppErrorOption) *AppError {
a := &AppError{
httpStatusCode: httpStatusCode,
appErrorCode: appErrorCode,
}
〜〜
}
Ensuite, pour les cas tels que les erreurs de validation où vous voulez des informations sur "quelle valeur dans quel champ", la structure doit avoir les éléments nécessaires pour chaque modèle (même s'il est redondant).
type AppError struct {
〜〜
/*
*Ci-dessous, les éléments qui ne sont pas essentiels pour toutes les expressions d'erreur (peuvent être définis en option)
*/
field string
value string
}
Cependant, je ne veux pas modifier la fonction New
(c'est-à-dire modifier tous les appelants) chaque fois que je dois ajouter ces éléments à l'avenir, donc [Functional Option Pattern](https: //commandcenter.blogspot] .com / 2014/01 / self-referential-functions-and-design.html) sera utilisé.
Définissez une fonction pour appliquer les options et passez-la comme argument variable dans la fonction New
(c'est-à-dire que vous n'avez pas à le faire).
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
}
Ainsi, les deux suivants sont préparés comme exemples d'application de ʻAppErrorOption`.
func WithField(v string) AppErrorOption {
return func(a *AppError) {
a.field = v
}
}
func WithValue(v string) AppErrorOption {
return func(a *AppError) {
a.value = v
}
}
En faisant cela, même si plus d'éléments sont ajoutés à la structure d'erreur dans le futur, elle peut être étendue sans modifier l'appelant existant.
Je pense qu'il est assez difficile de comprendre ce mécanisme à première vue (la raison principale est que l'explication est bâclée), donc j'aimerais que vous cherchiez sur Google avec "Functional Option Pattern" et lisez un article d'explication facile. .. ..
Après cela, définissez le code d'erreur spécifique au service comme suit,
type AppErrorCode string
// MEMO:Selon la définition du service, le système de code peut être choisi plutôt qu'une chaîne de caractères significative.
const (
//Erreur d'authentification
AppErrorCodeAuthenticationFailure AppErrorCode = "AUTHENTICATION_FAILURE"
//Erreur d'autorisation
AppErrorCodeAuthorizationFailure AppErrorCode = "AUTHORIZATION_FAILURE"
//Erreur de validation
AppErrorCodeValidationFailure AppErrorCode = "VALIDATION_FAILURE"
//Autres erreurs inattendues
AppErrorCodeUnexpectedFailure AppErrorCode = "UNEXPECTED_FAILURE"
)
Ce n'est pas grave si vous préparez une fonction Nouveau
dédiée pour chaque type d'erreur.
//Pour les erreurs d'authentification
func NewAuthenticationError(opts ...AppErrorOption) *AppError {
return NewAppError(http.StatusUnauthorized, AppErrorCodeAuthenticationFailure, opts...)
}
//Pour erreur d'autorisation
func NewAuthorizationError(opts ...AppErrorOption) *AppError {
return NewAppError(http.StatusForbidden, AppErrorCodeAuthorizationFailure, opts...)
}
//Pour erreur de validation
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...)
}
//Pour d'autres erreurs
func NewInternalServerError(opts ...AppErrorOption) *AppError {
return NewAppError(http.StatusInternalServerError, AppErrorCodeUnexpectedFailure, opts...)
}
À titre de test, si vous générez une erreur pour chaque type et l'ajoutez en tant qu'erreur GraphQL, cela ressemble à ceci. (Bien sûr, les erreurs d'authentification seront chargées avec les ID utilisateur, mais pour le moment, c'est un exemple.)
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) {
//Erreur d'authentification ajoutée
NewAuthenticationError().AddGraphQLError(ctx)
//Erreur d'autorisation ajoutée
NewAuthorizationError().AddGraphQLError(ctx)
//Erreur de validation ajoutée
NewValidationError("name", "taro").AddGraphQLError(ctx)
//Ajout d'autres erreurs
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 Il n'y a pas de préparation pour le gestionnaire cette fois.
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))
}
Comme vous pouvez le voir, il a un format unifié, il devrait donc être facile à manipuler du côté qui reçoit la réponse. .. ..
J'ai présenté plusieurs propositions de gestion des erreurs, des plus simples qui doivent simplement renvoyer une erreur pour le moment, à une méthode qui définit une structure d'erreur spécifique au service en tenant compte de la polyvalence. Bien sûr, il peut y avoir d'autres modèles en plus de ceux ici, et ceux énumérés ici ne sont pas au niveau de la production. Si vous le considérez comme un service unique, la manière de gérer le contenu d'erreur renvoyé ici au front-end est également un facteur important.
Recommended Posts