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