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

Created on 4 Mar 2019  路  2Comments  路  Source: apollographql/apollo-server

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.

All 2 comments

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

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!

Was this page helpful?
0 / 5 - 0 ratings