Search code examples
c#asp.net-coregraphqlhotchocolate

How can I handle input errors in HotChocolate?


I have Asp.Net Core Api with Graphql integrated by HotChocolate. I have mutation CreateRoadmap with input type CreateRoadmapInput. My Mutation. My input type.


I need to properly validate these input's fields. I tried to use IErrorFilter.My Error Filter I managed to customize error messages, but it always returns 500 status code and doesn't specify the field that has invalid input value. For example: client may send one field of GraphQl variable as float(with dot and numbers after it). In this case, i got Internal Server Error with status code 500 and the error message. Response with invalid input Response Body. Translation from Russian: "Invalid input data: Invalid field value: please enter a integer number"


What i want:

  1. To give 400(Bad Request) status code in case of invalid input.
  2. To specify which field(s) has invalid value

Sorry for my bad English. Hope you understood the question. Thanks for help!


My Mutation

public ObjectPayload<Roadmap> CreateRoadmap(
        CreateRoadmapInput obj,
        [Service] IServiceFactory<Roadmap> serviceFactory)
    {
        try
        {
            var roadmapService = serviceFactory.CreateServiceInstance(obj.WorkerType);
            var roadmap = roadmapService.Create(obj);

            return new ObjectPayload<Roadmap>(roadmap);
        }
        catch (Exception e)
        {
            return new ObjectPayload<Roadmap>(
                new UserError(e.Message, "EXECUTION_ERROR"));
        }
    }

Mu input type

public class CreateRoadmapInput : InputBase
{
    
    public int WorkerId { get; set; }
    public RoadmapType Type { get; set; }
    public int Period { get; set; }
    public int Year { get; set; }
    public int ProcessPercent { get; set; }
    public int ProjectPercent { get; set; }
    public WorkerType WorkerType { get; set; }
}

My error filter

public class ErrorHandling : IErrorFilter
{
    public IError OnError(IError error)
    {
        error = error.RemoveExtensions().RemoveLocations().RemoveCode();
        if (error.Message.Contains("got an invalid value"))
        {
            if (error.Exception.Message.Contains("Int cannot parse the given literal of type"))
                return error.WithMessage($"Некорректные вводимые данные: Неверное значение поля. Введите целое число");
            else
                return error.WithMessage($"Некорректные вводимые данные: {error.Exception.Message}");
        }
        return error.WithMessage(error.Exception.Message);
    }
}


Solution

  • The graphql processing pipeline is split into the validation and the execution. As far as I understand, the custom libraries for input validation proposed by Sergey Shaykhullin start taking part in the process only on the execution stage. But the case with variables you have mentioned is a part of the validation stage. Actually, the HotChocolate of version 11.2.2 validation engine provides you with all the data you need (i.e. specific error code and variable name) in the "extensions" field:

    {
        "errors": [
            {
                "message": "Variable `someInt` got an invalid value.",
                "locations": [
                    {
                        "line": 1,
                        "column": 21
                    }
                ],
                "extensions": {
                    "code": "HC0016",
                    "variable": "someInt",
                    "underlyingError": "Int cannot parse the given literal of type `StringValueNode`."
                }
            }
        ]
    }
    

    Regarding the second issue with status code 400 then, yes, error 500 is really strange here. I believe error 500 for validation error doesn't conform to the graphql over http spec. The spec explicitly states that

    "The server SHOULD deny execution with a status code of 400"

    To overcome that issue, you can write your own result serializer. In the simplest form, it might be something like the below one:

    public class HttpResultSerializerWithCustomStatusCodes : DefaultHttpResultSerializer
        {
            public override HttpStatusCode GetStatusCode(IExecutionResult result)
            {
                var baseStatusCode = base.GetStatusCode(result);
    
                if (result is IQueryResult && baseStatusCode == HttpStatusCode.InternalServerError && result.Errors?.Count > 0)
                {
                    if (result.Errors.Any(e => e.Code == ErrorCodes.Authentication.NotAuthorized || e.Code == ErrorCodes.Authentication.NotAuthenticated))
                        return HttpStatusCode.Forbidden;    
    
                    return HttpStatusCode.BadRequest;    
    
                }
    
                return baseStatusCode;
            }
        }