Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How can I generate custom errors during resolve #281

Closed
pravinhabbu4u opened this issue Feb 21, 2017 · 20 comments
Closed

How can I generate custom errors during resolve #281

pravinhabbu4u opened this issue Feb 21, 2017 · 20 comments
Labels
question Developer asks to help him deal with some problem

Comments

@pravinhabbu4u
Copy link

Example: I want to throw exception based on certain business rules OR based on some generic validation. Simple example would be to have an incoming ID of type GraphQLNonNull, however, if someone passes it empty ( "" ) then I want to generate error as Parameter "xyz" must be provided. This error should appear under the errors section on graphql response.

@joemcbride
Copy link
Member

joemcbride commented Feb 21, 2017

The GraphQL spec has the concept of "Validation Rules". They are mentioned briefly in the new docs. If you want to do a custom check then I would suggest to use a Validation Rule. Here are all of the existing rules.

@joemcbride joemcbride added the question Developer asks to help him deal with some problem label Feb 21, 2017
@pravinhabbu4u
Copy link
Author

@joemcbride As always thanks for quick response. Do you recommend creating a new type NonNullNonEmptyGraphType (similar to NonNullGraphType) so that I can easily add this validation on arguments that I want to have value?

  1. Create NonNullNonEmptyGraphType
  2. Creat ValidationRule based on this type check

@joemcbride
Copy link
Member

I think you should be able to do that (inherit from NonNullGraphType<T>.

    public class NonNullNonEmptyGraphType<T> : NonNullGraphType<T>
        where T : GraphType
    {
        ...
    }

Then can do a check similar to ProvidedNonNullArguments.

@pravinhabbu4u
Copy link
Author

pravinhabbu4u commented Feb 21, 2017

I must be doing something wrong because of which I am getting an exception as - GraphQL.ExecutionError: Only add root types. at GraphQL.Types.GraphTypesLookup.AddType(IGraphType type, TypeCollectionContext context) at GraphQL.Types.GraphTypesLookup.<>c__DisplayClass15_0.<HandleField>b__0(QueryArgument arg) at GraphQL.EnumerableExtensions.Apply[T](IEnumerable1 items, Action1 action) at GraphQL.EnumerableExtensions.Apply[T](IEnumerable1 items, Action1 action) at GraphQL.Types.GraphTypesLookup.AddType(IGraphType type, TypeCollectionContext context) at GraphQL.EnumerableExtensions.Apply[T](IEnumerable1 items, Action1 action) at GraphQL.Types.GraphTypesLookup.Create(IEnumerable1 types, IEnumerable1 directives, Func2 resolveType)&#xD;&#xA; at System.Lazy1.CreateValue() --- End of stack trace from previous location where exception

My project is currently using graphql nuget package hence I was trying to do some quick checks using:

public class NonNullNonEmptyGraphType<T> : NonNullGraphType<T>
   where T : GraphType
{

}


public class NonNullNonEmptyGraphType : GraphType
{
    public NonNullNonEmptyGraphType(IGraphType type)
    {
        if (type is NonNullNonEmptyGraphType)
        {
            throw new ArgumentException("Cannot nest NonNull inside NonNull.", nameof(type));
        }

        ResolvedType = type;
    }

    protected NonNullNonEmptyGraphType(Type type)
    {
        if (type == typeof(NonNullNonEmptyGraphType))
        {
            throw new ArgumentException("Cannot nest NonNull inside NonNull.", nameof(type));
        }

        Type = type;
    }

    public Type Type { get; private set; }
    public IGraphType ResolvedType { get; set; }

    public override string CollectTypes(TypeCollectionContext context)
    {
        var innerType = context.ResolveType(Type);
        ResolvedType = innerType;
        var name = innerType.CollectTypes(context);
        context.AddType(name, innerType, context);
        return "{0}!".ToFormat(name);
    }
}

public class ProvidedNonNullNonEmptyArguments : IValidationRule
{
    public string MissingFieldArgMessage(string fieldName, string argName, string type)
    {
        return $"Field \"{fieldName}\" argument \"{argName}\" of type \"{type}\" is required but not provided.";
    }

    public string MissingDirectiveArgMessage(string directiveName, string argName, string type)
    {
        return $"Directive \"{directiveName}\" argument \"{argName}\" of type \"{type}\" is required but not provided.";
    }

    public INodeVisitor Validate(ValidationContext context)
    {
        return new EnterLeaveListener(_ =>
        {
            _.Match<Field>(leave: node =>
            {
                var fieldDef = context.TypeInfo.GetFieldDef();

                if (fieldDef == null)
                {
                    return;
                }

                fieldDef.Arguments?.Apply(arg =>
                {
                    var argAst = node.Arguments?.ValueFor(arg.Name);
                    var type = arg.ResolvedType;

                    if (argAst == null && type is NonNullGraphType)
                    {
                        context.ReportError(
                            new ValidationError(
                                context.OriginalQuery,
                                "5.3.3.2",
                                MissingFieldArgMessage(node.Name, arg.Name, context.Print(type)),
                                node));
                    }
                    if ((argAst == null || argAst.ToString() == String.Empty) && type is NonNullNonEmptyGraphType)
                    {
                        context.ReportError(
                            new ValidationError(
                                context.OriginalQuery,
                                "5.3.3.2",
                                MissingFieldArgMessage(node.Name, arg.Name, context.Print(type)),
                                node));
                    }
                });
            });

            _.Match<Directive>(leave: node =>
            {
                var directive = context.TypeInfo.GetDirective();

                if (directive == null)
                {
                    return;
                }

                directive.Arguments?.Apply(arg =>
                {
                    var argAst = node.Arguments?.ValueFor(arg.Name);
                    var type = arg.ResolvedType;

                    if (argAst == null && type is NonNullGraphType)
                    {
                        context.ReportError(
                            new ValidationError(
                                context.OriginalQuery,
                                "5.3.3.2",
                                MissingDirectiveArgMessage(node.Name, arg.Name, context.Print(type)),
                                node));
                    }
                    if ((argAst == null || argAst.ToString() == String.Empty) && type is NonNullNonEmptyGraphType)
                    {
                        context.ReportError(
                            new ValidationError(
                                context.OriginalQuery,
                                "5.3.3.2",
                                MissingDirectiveArgMessage(node.Name, arg.Name, context.Print(type)),
                                node));
                    }
                });
            });
        });
    }
}

@joemcbride
Copy link
Member

Which one are you using and how are you using it? That error means that a NonNullGraphType is not getting properly unwrapped. The core engine only wants to register "root" types (not ones wrapped in non-null or list), which is what that error means.

@pravinhabbu4u
Copy link
Author

My use was as simple as:

Field( "MyName",
arguments: new QueryArguments(new QueryArgument<NonNullNonEmptyGraphType> { Name = "MyParmName"}),

@joemcbride
Copy link
Member

It looks like GetNamedType will have to be updated to be more robust. Right now it seems like it doesn't work with inherited classes. It appears that it also won't work with the non-generic version.

@joemcbride
Copy link
Member

joemcbride commented Feb 21, 2017

The more I think on this though I think you could continue to hit issues. Since I know there are other specific checks + type building with list/non-null.

Perhaps a better way would be to add meta-data and use that for the validation check? I have an example of using metadata to provide authorization.

Object/Field Metadata
Authorization Metadata / Validation Rule

@joemcbride
Copy link
Member

And here's a discussion on this issue with GraphQL in general - graphql/graphql-js#361

@pravinhabbu4u
Copy link
Author

Thanks for the information. I am trying to integrate the information provided above. I would have simply loved to have something as throwGraphQLError / addGraphQLError (something like ModelType.AddModelError) from resolver. This can allow more control that just restricting logic to validation errors.

@joemcbride
Copy link
Member

In general, you can throw an error. I'm not sure that it comes through very well currently but that is possible.

As per providing a better way to handle fine-grained validation, I agree with you that coming up with something more concrete would be nice. As everyone has their own favorite validation framework I kind of don't want to invent something new myself. Since the core GraphQL does not provide anything in the spec yet, I see this as an "add-on" currently.

Another option would be to create a custom Scalar that does validation on the value.

A reason to not do validation in the resolver is because if your input values do not pass validation there is really no reason to execute the request. So you should be able to validate those inputs before execution happens so you're not fetching data you don't need to. That is how the validation rules in the GraphQL spec are currently setup.

@pravinhabbu4u
Copy link
Author

Throwing error generally results in error resolving .... error. This really doesnt work well when I allow requestor to access data from related microservices as part of single query. Failure on any API call in this case results in entire resolve exception. If an individual nested call fails then currently I report error at specific level/ nested field using error block available on each microservice.

@oldnavy1989
Copy link

I've got similar behavior. There's a service inside resolving function. It can throw an exception. I need to log the exception. Haw can I reach that globally without try/catch blocks in particular resolving function?

@joemcbride
Copy link
Member

@oldnavy1989 All errors in resolvers should be caught by the framework and show up in ExecutionResult.Errors. If you want to handle it "closer to the source", one option would be to use field middleware.

https://github.com/graphql-dotnet/graphql-dotnet/blob/master/docs/learn.md#error-handling
https://github.com/graphql-dotnet/graphql-dotnet/blob/master/docs/learn.md#field-middleware

@oldnavy1989
Copy link

@joemcbride Thank you for answer. I tried to use middleware to catch exceptions but didn't figure out how to implement it. While debuging if exception raised in resolver there is no continuation in resolve method in middleware class, so it just returns executionresult. Can you provide a sample with handling errors in middleware please?

@rami-hatoum
Copy link

I have tried to add meta-data using the field middleware to use in a validation rule and generate custom errors. The problem is validation rules run before the field middleware. Any ideas? Also it would be great if we can pass in a custom JSON convertor.

@joemcbride
Copy link
Member

Closing this as #342 was merged.

@denisedelbando
Copy link

denisedelbando commented Apr 21, 2020

In case anyone comes in here and still can't figure it out, I got it working:

public class CustomFieldMiddleWare
    {
        public async Task<object> Resolve(
            ResolveFieldContext context,
            FieldMiddlewareDelegate next)
        {
            object result = null;
            try
            {
                result = await next(context);
                return result;
            }
            catch (ServiceException serviceException)
            {
                var metadata = new Dictionary<string, object>
                {
                    
                    {"typeName", context.ParentType.Name},
                    {"source", context.FieldName},
                    {"innerException", serviceException.InnerException != null ? serviceException.InnerException.Message : "" },
                    {"stackTrace", serviceException.StackTrace },
                    {"message", serviceException.Message },
                    {"httpCode", serviceException.StatusCode }

                };
                var exception = new ExecutionError(serviceException.Message, metadata);
                exception.Code = serviceException.Code.ToString();
                exception.Path = new[] {context.FieldName};
                context.Errors.Add(exception);
                return result;
            }
        }

        
    }

sample result.
sc-20200421000859-chrome

this was the query:

{
  error
  simpleHi
}

the error field just threw an error, while simpleHi was just a static string (just to test it).
unfortunately, I had to use an older version of this issue #1620

@mcpine
Copy link

mcpine commented May 22, 2024

@denisedelbando How did you configure your app to use the CustomFieldMiddleware at runtime?

@Shane32
Copy link
Member

Shane32 commented May 22, 2024

@mcpine If you just want more detailed error messages reported through the GraphQL interface, you can use:

services.AddGraphQL(b => b
    // .AddSchema() and other stuff here
    .AddErrorInfoProvider(o => o.ExposeExceptionDetails = true));

For my application, I only want authenticated users to be able to see stack traces and stuff, so I did this:

services.AddGraphQL(b => b
    .AddErrorInfoProvider<MyErrorInfoProvider>());

public class MyErrorInfoProvider : IErrorInfoProvider
{
    private readonly ErrorInfoProvider _defaultErrorInfoProvider = new();
    private readonly ErrorInfoProvider _authenticatedErrorInfoProvider = new(o => o.ExposeExceptionDetails = true);
    private readonly IHttpContextAccessor _httpContextAccessor;

    public MyErrorInfoProvider(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public ErrorInfo GetInfo(ExecutionError executionError)
    {
        var user = _httpContextAccessor.HttpContext!.User;
        if (!user.Identity!.IsAuthenticated) {
            return _defaultErrorInfoProvider.GetInfo(executionError);
        }
        return _authenticatedErrorInfoProvider.GetInfo(executionError);
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Developer asks to help him deal with some problem
Projects
None yet
Development

No branches or pull requests

7 participants