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

[apollo-server-koa] Error is missing message and properties when thrown inside the parse of a custom scalar type #2399

Closed
michaeldzjap opened this issue Mar 4, 2019 · 2 comments

Comments

@michaeldzjap
Copy link

michaeldzjap commented Mar 4, 2019

Probably similar to #2155, but since that issue hasn't got a response yet and I am experiencing slightly different behaviour I think it justifies opening a separate issue for it.

Any errors thrown in the parseValue / parseLiteral of a custom scalar type swallows part of the original error either when simply throwing an Error or ApolloError instance.

I am using apollo-server-koa v2.4.8 as a middleware:

import * as Router from 'koa-router';
import {graphqlKoa} from 'apollo-server-koa/dist/koaApollo';
import {GraphQLError} from 'graphql';

import config from '../config/app';
import graphiqlKoa from '../graphql/graphiqlKoa';
import schema from '../graphql/index';
import {environment} from '../support/helpers';

const router = new Router;
const options = graphqlKoa({
    schema,
    debug: config.DEBUG,
    formatError: (error: GraphQLError): GraphQLError => {
        console.log('ERROR:', error.originalError);
        return error;
    }
});

router.get('/graphql', options);
router.post('/graphql', options);

if (environment('development')) {
    router.get('/graphiql', graphiqlKoa({endpointURL: '/graphql'}));
}

export default router;

My schema is pretty basic still, but I am using a custom schema directive:

const schema = makeExecutableSchema({
    typeDefs,
    resolvers,
    schemaDirectives: {
        uuid: UUIDDirective,
    },
});

where the UUIDDirective is of the form

import {SchemaDirectiveVisitor} from 'apollo-server-koa';
import {
    GraphQLNonNull, GraphQLScalarType, GraphQLArgument, GraphQLField,
    GraphQLInputField,
} from 'graphql';

import UUIDType from './UUIDType';

class UUIDDirective extends SchemaDirectiveVisitor {

    /**
     * Express interest in handling an argument definition schema type.
     *
     * @param {GraphQLArgument} argument
     * @returns {void}
     */
    public visitArgumentDefinition(argument: GraphQLArgument): void {
        this._wrapType(argument);
    }

    /**
     * Replace "field.type" with a custom "GraphQLScalarType" that enforces the
     * field to be in UUID format.
     *
     * @param {(GraphQLField|GraphQLInputField)} field
     * @returns {void}
     */
    private _wrapType(field: GraphQLArgument | GraphQLField<unknown, unknown> | GraphQLInputField): void {
        if (field.type instanceof GraphQLNonNull
            && field.type.ofType instanceof GraphQLScalarType) {
            field.type = new GraphQLNonNull(
                new UUIDType(field.name, field.type.ofType)
            );
        } else if (field.type instanceof GraphQLScalarType) {
            field.type = new UUIDType(field.name, field.type);
        } else {
            throw new Error(`Not a scalar type: ${field.type}`);
        }
    }

}

export default UUIDDirective;

In the parse methods of UUIDType I do some validation on the field value. If it is not in UUID format I throw an error like so:

import {ApolloError} from 'apollo-server-koa';
import {
    GraphQLError, GraphQLScalarType, GraphQLEnumType, ValueNode
} from 'graphql';

import Validator from '../../validation/Validator';

class UUIDType extends GraphQLScalarType {

    /**
     * Create a new validator string type instance.
     *
     * @constructor
     * @param {string} name
     * @param {(GraphQLScalarType|GraphQLEnumType)} type
     * @param {Array} rules
     */
    public constructor(name: string, type: GraphQLScalarType | GraphQLEnumType) {
        super({
            name: 'UUID',
            serialize(value) {
                return type.serialize(value);
            },
            parseValue(value) {
                UUIDType._validate(name, value);

                return type.parseValue(value);
            },
            parseLiteral(ast: ValueNode, variables) {
                const value = type.parseLiteral(ast, variables);

                UUIDType._validate(name, value);

                return value;
            },
        });
    }

    /**
     * Do the desired validation and re-throw any "ValidationError" instances as
     * a "GraphQLError" instance.
     *
     * @param {string} name
     * @param {*} value
     *
     * @throws {GraphQLError}
     * @returns {void}
     */
    private static _validate(name: string, value: any): void {
        // Three different attempts at throwing an error:

        // 1. "ApolloError": Doesn't do what I want, the message and additional
        // properties are lost, only the error code is visible in the response
        throw new ApolloError('Hey now!', 'VALIDATION_ERROR', {a: 'b'});

        // 2. "ValidationError" (custom error that inherits from "Error"): Doesn't
        // do what I wan't.
        Validator.validate(name, value, ['uuid']);

        // 3. "GraphQLError": Finally does what I want, albeit using a bit of a
        // workaround
        try {
            Validator.validate(name, value, ['uuid']);
        } catch (error) {
            throw new GraphQLError(
                error.message,
                value,
                null,
                null,
                null,
                null,
                {code: error.code, message: error.message}
            );
        }
    }

}

export default UUIDType;

Here Validator.validate(name, value, ['uuid']); simply throws a ValidationError when the field value is not in UUID format. ValidationError is a simple error that extends Error:

import {UNPROCESSABLE_ENTITY} from '../constants/errorCodes';

class ValidationError extends Error {

    /**
     * The status code for a validation error.
     *
     * @var {number}
     */
    public code: number = UNPROCESSABLE_ENTITY;

}

export default ValidationError;

The @uuid directive is then applied to a query type like so:

const Query = `
    directive @uuid on ARGUMENT_DEFINITION

    type Query {
        order(id: ID! @uuid): Order
    }
`;

Everything seems to work fine, but I get inconsistent results using the 3 different ways of throwing an error. Only the 3rd option give me a satisfactory response.

For ApolloError I receive the following response:

{
    "errors": [
        {
            "extensions": {
                "code": "VALIDATION_ERROR"
            },
            "locations": [
                {
                    "line": 2,
                    "column": 13
                }
            ]
        }
    ]
}

For ValidationError I receive the following response:

{
    "errors": [
        {
            "extensions": {
                "code": "GRAPHQL_VALIDATION_FAILED"
            },
            "locations": [
                {
                    "line": 2,
                    "column": 13
                }
            ]
        }
    ]
}

For GraphQLError I receive the (desired) response:

{
    "errors": [
        {
            "extensions": {
                "code": 422,
                "message": "The id is not a valid UUID."
            },
            "locations": [
                {
                    "line": 2,
                    "column": 13
                }
            ]
        }
    ]
}

So to make a long story short: why are the error message (and any additional properties in the case of ApolloError) lost when throwing an instance of ApolloError or Error? I at least would like to have access to the error message, that is pretty essential.

On a related note; I found that formatError is not called when specifying it on the graphqlKoa middleware. There is already an issue open for this #1439 and a PR has been submitted #1662, but it doesn't seem the problem has been solved so far.

@michaeldzjap michaeldzjap changed the title Error is missing message and properties when thrown inside the parse of a custom scalar type [apollo-server-koa] Error is missing message and properties when thrown inside the parse of a custom scalar type Mar 4, 2019
@vale981
Copy link

vale981 commented Mar 7, 2019

Try upgrading graphql-tools to the latest version. That worked for me!

@martijnwalraven
Copy link
Contributor

Thanks for reporting this issue originally!

I'm going to close this issue since it hasn't received a lot of traction and could have been resolved already.

If this is still a problem, would someone who's experiencing the problem (or anyone who comes across this issue and is able to assist) mind building a reproduction of the problem in to a runnable CodeSandbox reproduction using the latest version of Apollo Server and sharing the link to that CodeSandbox in this issue?

I'm happy to re-open if this is still occurring and someone can provide a reproduction. Thanks again!

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Apr 21, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants