Gestion des erreurs GraphQL (gqlgen)

thème

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

Lecteur supposé

Index des articles associés

--11e "Réponse au problème N + 1 à l'aide de chargeurs de données"

Environnement de développement

OS - Linux(Ubuntu)

$ cat /etc/os-release 
NAME="Ubuntu"
VERSION="18.04.5 LTS (Bionic Beaver)"

#Backend

#Langue --Golang

$ go version
go version go1.15.2 linux/amd64

gqlgen

v0.13.0

IDE - Goland

GoLand 2020.2.3
Build #GO-202.7319.61, built on September 16, 2020

Toutes les sources cette fois

https://github.com/sky0621/study-gqlgen/tree/v0.2

Entraine toi

Essayons quelques méthodes pour gérer les erreurs GraphQL côté serveur à l'aide de gqlgen.

1. 1. Manipuler de manière basique

É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!
}

Résolveur

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 }

Explication par motif

référence

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/

■ Système normal

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. screenshot-localhost_8080-2020.10.17-23_48_21.png

■ modèle qui renvoie l'erreur standard go

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. screenshot-localhost_8080-2020.10.17-23_51_16.png

■ Motif qui renvoie une erreur via la méthode fournie par gqlgen

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. screenshot-localhost_8080-2020.10.18-00_08_41.png

■ Modèle qui renvoie plusieurs erreurs

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.) screenshot-localhost_8080-2020.10.18-00_19_32.png

■ Modèles utilisant des zones d'extension individuelles

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. screenshot-localhost_8080-2020.10.18-00_20_47.png

■ Modèle 2 qui utilise des zones d'extension individuelles

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. screenshot-localhost_8080-2020.10.18-00_24_29.png

■ Modèle en cas de panique

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". screenshot-localhost_8080-2020.10.18-00_28_03.png

2. Gestion des erreurs personnalisée

À 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))
}

Contrôle de fonctionnement

La gestion des erreurs

Heureusement, le code d'erreur et le message d'erreur peuvent être triés. screenshot-localhost_8080-2020.10.18-00_43_38.png

gestion de la panique

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. screenshot-localhost_8080-2020.10.18-00_45_38.png

3. 3. Gestion des erreurs personnalisée pour plus de polyvalence

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.

Structure d'erreur spécifique au service

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
	}
}

Commentaire

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...)
}

Résolveur

À 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))
}

Contrôle de fonctionnement

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. .. .. screenshot-localhost_8080-2020.10.18-01_22_59.png

Sommaire

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

Gestion des erreurs GraphQL (gqlgen)
Gestion des erreurs de trame principale
Gestion des erreurs SikuliX
django.db.migrations.exceptions.InconsistentMigrationHistory Gestion des erreurs
À propos de la gestion des erreurs Tweepy
Gestion des erreurs dans PythonBox
Autour de la gestion des erreurs de feedparser
[Contre-mesures d'erreur] Gestion des erreurs d'installation de django-heroku
Réponse aux erreurs lors de l'installation de mecab-python
À propos de FastAPI ~ Gestion des erreurs de point de terminaison ~
Mémorandum de gestion des erreurs de construction PyCUDA
Gestion des erreurs lors de la mise à jour de Fish shell
Gestion des erreurs lors de la migration de Django'DIRS ': [BASE_DIR /' templates ']