If you want to format your errors when using stitching, you can't tell the error type from the error.
The issue seems to be located here:
https://github.com/apollographql/graphql-tools/blob/master/src/stitching/errors.ts#L80
So packages like apollo-errors don't work because you can't get the type from it.
One solution I think I see that's somewhat possible is to check if it's a schema created for a remote server and if so, run checkResultAndHandleErrors, otherwise, just throw the error as is since checkResultAndHandleErrors looks to be made for remote responses, and if it's local, the errors should just be thrown normally
Thoughts?
@j ran into this issue yesterday I think we can get around it easily by maintaining the original error in a merged error. Created a quick PR at #485 as we needed a workaround. But happy to help if there's a more in depth solution required.
I ran into this today. Needs a fix. Very hard to debug.
I had a subscription field returning a list which was returning this in graphql: [null, null, null].
The issue was one of the fields in the list items missing. There is no way to see the error without turning off stitching.
This should be labelled as bug, not enhancement.
i agree with @vjpr on this one. this issue should be marked as a bug and not an enhancement (@freiksenet )
im surprised there aren't more people complaining about this issue. is there a workaround that we're not aware of? how are other people debugging their local development with schema stitching when the stack traces are useless?
This is a bug. It's impacting us.
This is also impacting us.
We were unable to pass custom error codes because the merged schema swallows the thrown errors.
+1
We've also run into this, and had to turn off stitching to debug.
The issue can be closed given #637 was merged an released. I had to add some ugly error drill-down logic, at least, it's working.
I am still having issues viewing any extra properties associated with an error inside formatError:
const e = new Error('An Error...');
e.data = 'Error data...';
throw e;
Inside formatError I would expect to find the data property but I don't:
export const formatError = error => {
const {originalError} = error;
console.log(originalError.errors[0]);
return error;
};
output:
{ Error: An Error...
at _callee$ (/Users/davidstummer/code/DeliveriesOnDemand/ServiceLayer/app/admin/creditor/resolvers.js:12:15)
...
message: 'An Error...',
locations: [],
path: [ 'pageCreditors' ] }
This only occurs when using mergeSchemas
Update: I managed to finally find my error object here:
error.originalError.errors[0].originalError
Why wouldn't stitched errors be appended, unaltered, to the errors array? Let the stitched schema dictate its errors' format.
I'm having an issue with this also.
I've created remote executable schemas and merged them with mergeSchemas. I'm trying to use formatError function when executing against the merged schema, but the error passed as an argument only has the message, locations, and path properties, not other attributes of the error returned from the remote schema.
Is there a way to make properties of errors on remote schemas available to the merged schema? Or does the logic in mergeSchemas swallow them?
Facing the same issue as @brycesteinhoff, would be okay with using a workaround too for now if anyone know of any.
@lakshyaranganath, it seems this issue is actually with makeRemoteExecutableSchema, not mergeSchemas only. The workaround I've come up with for now is to intercept GraphQL errors on the remote schema, serialize the properties all into the error message as JSON, then format the errors by deserializing back to properties.
Thanks @brycesteinhoff, I've got something working based on @DavidStummer's solution.
Posting it here for the next person who faces this issue :)
// this is the formatError function for server which is stitching the schemas together
formatError: (error) => error.originalError.errors[0].originalError
Thanks @brycesteinhoff and @lakshyaranganath, this works for me =>
export const opts = {
...
formatError: (err:any) => (
err.originalError ? formatError(err.originalError.errors[0].originalError) : formatError(err)
),
...
}
Going to work on fixing errors as part of #890 - the state of error handling with schema stitching isn't great, so I think we need a new system here. Hope to get to this next week.
I was also just bit by this. It took ~2 days to track down the issue. Locally generated errors should maintain their types and custom fields when passed into formatError. In my mind, this is a bug, not a missing feature.
In my case, I have micro-services connected to an api gateway that makes the stitching.
The orignal error was deep buried: error.orignalError.errors[0].originalError.errors[0]
Here is how I solved the issue:
In User micro-service directrives/auth.js
const {
SchemaDirectiveVisitor,
AuthenticationError,
ForbiddenError
} = require('apollo-server-express');
// ...
if (!context.token) {
throw new AuthenticationError('User not authenticated');
}
// ...
In Api Gateway helpers/searchOriginalError.js
function searchOriginalError (error) {
if (error.originalError) {
return searchOriginalError(error.originalError);
}
if (error.errors) {
return error.errors.map(searchOriginalError)[0];
}
return error;
}
module.exports = { searchOriginalError };
In Api Gateway index.js
const server = new ApolloServer({
schema,
context: ({ req }) => {
// ...
},
formatError: (error) => {
const originalError = searchOriginalError(error);
logger.error(originalError);
return originalError;
}
});
I hope, this helps.
For us, using formatError and simply returning the error with NO changes impacts behavior. That's silly right? E.g.
formatError: (error) => error
This ^ behaves differently than no formatError
We are using schema stitching... it seems like data is stripped or removed from the errors only if we have a formatError function defined.
My team has been playing with stitching and attempted many of the suggestions above (and related issues). We appear to be hitting the issue of exceptions being swallowed versus having to unwrap. Has anyone actually figured out how to override the internals of graphql-tools to monkeypatch the code to work around this?
If we add debug console output for the unwrap,
function searchOriginalError(error) {
console.log('error', error);
if (error.originalError) {
return searchOriginalError(error.originalError);
}
if (error.errors) {
return error.errors.map(searchOriginalError)[0];
}
return error;
}
we see the following..
error { [GraphQLError: [object Object]]
message: '[object Object]',
locations: [ { line: 2, column: 3 } ],
path: [ 'createUser' ],
extensions:
{ code: 'INTERNAL_SERVER_ERROR',
exception: { errors: [Array], stacktrace: [Array] } } }
error { Error: [object Object]
at new CombinedError (/Users/todd/Documents/workspace/token-factory-graphql/node_modules/graphql-tools/dist/stitching/errors.js:82:28)
at Object.checkResultAndHandleErrors (/Users/todd/Documents/workspace/token-factory-graphql/node_modules/graphql-tools/dist/stitching/errors.js:98:15)
at CheckResultAndHandleErrors.transformResult (/Users/todd/Documents/workspace/token-factory-graphql/node_modules/graphql-tools/dist/transforms/CheckResultAndHandleErrors.js:9:25)
at /Users/todd/Documents/workspace/token-factory-graphql/node_modules/graphql-tools/dist/transforms/transforms.js:18:54
at Array.reduce (<anonymous>)
at applyResultTransforms (/Users/todd/Documents/workspace/token-factory-graphql/node_modules/graphql-tools/dist/transforms/transforms.js:17:23)
at /Users/todd/Documents/workspace/token-factory-graphql/node_modules/graphql-tools/dist/stitching/delegateToSchema.js:93:50
at step (/Users/todd/Documents/workspace/token-factory-graphql/node_modules/graphql-tools/dist/stitching/delegateToSchema.js:31:23)
at Object.next (/Users/todd/Documents/workspace/token-factory-graphql/node_modules/graphql-tools/dist/stitching/delegateToSchema.js:12:53)
at fulfilled (/Users/todd/Documents/workspace/token-factory-graphql/node_modules/graphql-tools/dist/stitching/delegateToSchema.js:3:58)
errors:
[ { Error: [object Object]
at asErrorInstance (/Users/todd/Documents/workspace/token-factory-graphql/node_modules/graphql/execution/execute.js:541:43)
at process._tickCallback (internal/process/next_tick.js:68:7) message: '[object Object]', locations: [], path: [Array] } ] }
error { Error: [object Object]
at asErrorInstance (/Users/todd/Documents/workspace/token-factory-graphql/node_modules/graphql/execution/execute.js:541:43)
at process._tickCallback (internal/process/next_tick.js:68:7)
message: '[object Object]',
locations: [],
path: [ 'createUser' ] }
error Error: [object Object]
at asErrorInstance (/Users/todd/Documents/workspace/token-factory-graphql/node_modules/graphql/execution/execute.js:541:43)
at process._tickCallback (internal/process/next_tick.js:68:7)
[2018-10-18T08:32:03.600] [ERROR] server.js - Error: [object Object]
at asErrorInstance (/Users/todd/Documents/workspace/token-factory-graphql/node_modules/graphql/execution/execute.js:541:43)
at process._tickCallback (internal/process/next_tick.js:68:7)
The result to the client request becomes
{
"data": {
"createUser": null
},
"errors": [
{}
]
}
Unfortunately @OlivierCuyp's solution didn't work in our case either. The upstream service errors weren't being exposed to formatError.
This flow, (_flow 1_) makeRemoteExecutableSchema => createResolver => checkResultsAndHandleErrors raised the upstream error.
However the subsequent (_flow 2_) MergeSchemas => delegateToSchema => applies CheckResultsAndHandleErrors as transform raised an error that had lost the upstream error context.
Narrowed it down to this line in checkResultAndHandleErrors. When this error is thrown, it's received by the mergeSchemas flow but has lost the real error context.
Workaround was to add an error handler to the httpLink that appended an additional error so that a CombinedError got thrown from checkResultAndHandleErrors like so
const errorLink = onError(({ graphQLErrors, response }) => {
if (graphQLErrors && graphQLErrors.length === 1) {
response.errors = graphQLErrors.concat(new ApolloError("hax0r"));
}
});
Then flow 2 received an error with the right context that was then conveyed to formatError.
@smber1 thanks for sharing it, it works for me
Hmmm... I've tried almost all of the approaches outlined in this thread and I just can't get any of them to work. In my system we have a main API gateway which links to other schemas. The flow we're following is pretty close to the docs
let remoteLink = new HttpLink({ uri : link.path, fetch });
let remoteSchema = await introspectSchema(remoteLink);
validateSchema({ schema : remoteSchema, prefix : link.name });
let remoteExecutableSchema = makeRemoteExecutableSchema({
schema : remoteSchema,
link : remoteLink
});
// later
var schema = mergeSchemas({ schemas });
I am able to access the errors with the proper stack trace if I utilize the apollo-link-error. The problem is that I'm only able to console.log them, and can never return them to the screen. I tried appending to the reponse.errors as outlined by @smber1 but they would not appear in my formatError method. I was able to access an error.originalError but the stacktrace would be overwritten at that point and there was never an error.originalError.errors as some other comments have alluded to.
Right now apollo has settings so that it will display stacktraces based on debug or NODE_ENV. It feels like if stacktraces are enabled, then by default they should display in such a way that we can determine exactly which link they originated from and preserving their stack trace. That seems like what most people above are looking for, and it cases where people want to override that default behavior there is the formatError method. If the apollo-link-error package is meant to solve this problem then I think the docs need to be extrapolated a lot more. Right now they don't even show on how to integrate it with an existing schema stitched system. Now do they show things like utilizing response.errors so it's not clear if that's even an official pathway.
Not saying this is the best solution but we have used an approach where we create a custom formatError for each apollo server instance unpacks the error and throws it again as an ApolloError
const stringify = require('json-stringify-safe');
const defaultFormatErrorHandler = (error) => {
const err = JSON.parse(stringify(error));
console.log(err);
delete err.extensions.exception.config;
delete err.extensions.exception.request;
delete err.extensions.exception.response;
return new ApolloError('ERROR', err.extensions.code, { exception: { ...err } });
};
new Apollo({
formatError: defaultFormatErrorHandler,
...
})
It is very hacky and the actual error gets buried one level deeper than necessary but it does the job of preserving the error code and the message in the first error layer which can then be inspected by the client.
Example error from our implementation
{
"errors": [
{
"locations": [],
"path": [
"updateSektion"
],
"extensions": {
"code": "SECTOR9_NOT_LOGGED_IN_ERROR",
"exception": {
"exception": {
"message": "Principal is not logged in",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"updateSektion"
],
"extensions": {
"code": "SECTOR9_NOT_LOGGED_IN_ERROR",
"exception": {
"invalidJWT": true,
"stacktrace": [
"Error: Principal is not logged in",
" at Principal.requireLogIn (/home/node/app/node_modules/sector9/build/Principal.js:73:19)",
" at exports.updateSektionMutation (/home/node/app/build/graphql/schemas/sektion/mutation.js:5:10)",
" at field.resolve (/home/node/app/node_modules/apollo-server-core/node_modules/graphql-extensions/dist/index.js:128:26)",
" at resolveFieldValueOrError (/home/node/app/node_modules/graphql/execution/execute.js:479:18)",
" at resolveField (/home/node/app/node_modules/graphql/execution/execute.js:446:16)",
" at /home/node/app/node_modules/graphql/execution/execute.js:262:18",
" at /home/node/app/node_modules/graphql/jsutils/promiseReduce.js:32:10",
" at Array.reduce (<anonymous>)",
" at promiseReduce (/home/node/app/node_modules/graphql/jsutils/promiseReduce.js:29:17)",
" at executeFieldsSerially (/home/node/app/node_modules/graphql/execution/execute.js:259:37)"
]
}
}
},
"stacktrace": [
"GraphQLError",
" at Object.locatedError (/home/node/app/node_modules/graphql/error/locatedError.js:31:10)",
" at Object.checkResultAndHandleErrors (/home/node/app/node_modules/graphql-tools/dist/stitching/errors.js:99:23)",
" at /home/node/app/node_modules/graphql-tools/dist/stitching/makeRemoteExecutableSchema.js:159:52",
" at step (/home/node/app/node_modules/graphql-tools/dist/stitching/makeRemoteExecutableSchema.js:31:23)",
" at Object.next (/home/node/app/node_modules/graphql-tools/dist/stitching/makeRemoteExecutableSchema.js:12:53)",
" at fulfilled (/home/node/app/node_modules/graphql-tools/dist/stitching/makeRemoteExecutableSchema.js:3:58)",
" at process._tickCallback (internal/process/next_tick.js:68:7)"
]
}
}
}
],
"data": {
"updateSektion": null
}
}
Those of you who just want to be able to use two endpoints can use Directional Composition. That is if you don't want to deal with the issues with stitching. But I think directional composition only works with two endpoints, not more.
https://github.com/apollographql/react-apollo/issues/1863#issuecomment-394427535
https://www.apollographql.com/docs/link/composition#directional
Was this issue fixed? Thanks
I am using these packages
"apollo-server": "^2.4.8",
"apollo-server-core": "^2.5.0",
"apollo-server-express": "^2.4.8",
This should be fixed in graphql-tools-fork. Please let me know if not working for you.
Using graphql-tools-fork helped, but the error (Argument Validation Error in my case) is still being reported as INTERNAL_SERVER_ERROR, I would expect the code to be more specific as described here.
If you could share a reproduction, I could take a look?
Thanks for help, the problem is in the type-graphql library I am using that does not provide error codes.
I use this:
formatError, we get an error completely.import { onError } from 'apollo-link-error';
...
const ErrorLink = onError(({ graphQLErrors, response }) => {
if (graphQLErrors && graphQLErrors.length === 1) {
graphQLErrors.length = 2;
}
});
formatError handler we generate new ApolloError`s from standard function in apollo-server-errors repo.import { formatApolloErrors } from 'apollo-server-errors';
...
const formatError = (error) => {
let errors = [];
if(!error.originalError || !error.originalError.errors || error.originalError.errors.length <= 0) {
return error;
}
error.originalError.errors.forEach((currentError) => {
if(!currentError) return;
let currentOriginal = currentError.originalError;
const compiledErrors = formatApolloErrors(currentOriginal.errors || []);
if(compiledErrors === null) {
return;
}
errors.push(...compiledErrors.filter(err => !!err));
});
return (errors.length > 1) ? errors : errors[0];
};
For result: I throw Forbidden exception in the microservice and get response in the gateway service:
{
"errors": [
{
"message": "Not authenticated as user.",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"asset"
],
"extensions": {
"code": "FORBIDDEN",
"exception": {
"stacktrace": [
"ForbiddenError: Not authenticated as user.",
" at isAuthenticated (/usr/app/src/resolvers/authorization.js:5:32)",
" at /usr/app/node_modules/graphql-resolvers/lib/combineResolvers.js:28:48"
]
}
}
}
],
"data": {
"asset": null
}
}
And if exception thrown in the getaway service:
{
"errors": [
{
"message": "Cannot query field \"asdasd\" on type \"Asset\". Did you mean \"alias\"?",
"locations": [
{
"line": 6,
"column": 9
}
],
"extensions": {
"code": "GRAPHQL_VALIDATION_FAILED",
"exception": {
"stacktrace": [
"GraphQLError: Cannot query field \"asdasd\" on type \"Asset\". Did you mean \"alias\"?",
" at Object.Field (/usr/app/node_modules/graphql/validation/rules/FieldsOnCorrectType.js:53:31)",
" at Object.enter (/usr/app/node_modules/graphql/language/visitor.js:324:29)",
" at Object.enter (/usr/app/node_modules/graphql/language/visitor.js:375:25)",
" at visit (/usr/app/node_modules/graphql/language/visitor.js:242:26)",
" at Object.validate (/usr/app/node_modules/graphql/validation/validate.js:54:22)",
" at validate (/usr/app/node_modules/apollo-server-core/src/requestPipeline.ts:426:22)",
" at Object.<anonymous> (/usr/app/node_modules/apollo-server-core/src/requestPipeline.ts:250:32)",
" at Generator.next (<anonymous>)",
" at fulfilled (/usr/app/node_modules/apollo-server-core/dist/requestPipeline.js:4:58)",
" at processTicksAndRejections (internal/process/task_queues.js:85:5)"
]
}
}
}
]
}
Unfortunately @OlivierCuyp's solution didn't work in our case either. The upstream service errors weren't being exposed to
formatError.This flow, (_flow 1_)
makeRemoteExecutableSchema => createResolver => checkResultsAndHandleErrorsraised the upstream error.However the subsequent (_flow 2_)
MergeSchemas => delegateToSchema => applies CheckResultsAndHandleErrors as transformraised an error that had lost the upstream error context.Narrowed it down to this line in checkResultAndHandleErrors. When this error is thrown, it's received by the
mergeSchemasflow but has lost the real error context.Workaround was to add an error handler to the
httpLinkthat appended an additional error so that aCombinedErrorgot thrown fromcheckResultAndHandleErrorslike soconst errorLink = onError(({ graphQLErrors, response }) => { if (graphQLErrors && graphQLErrors.length === 1) { response.errors = graphQLErrors.concat(new ApolloError("hax0r")); } });Then flow 2 received an error with the right context that was then conveyed to
formatError.
Has anyone tried this with apollo federation?
Should be fixed by #1307.
Not sure if it's related, but I encountered a similar issue where an error on a field in an underlying schema was being ignored by the stitched schema gateway. The gateway would try to return null for the entire object instead of passing through the error. This would then produce spurious errors that the object's id field can't be null, instead of the actual error from the underlying schema.
I am using [email protected], so this is not fixed by #1307.
It looks like this is caused by the code in handleNull at https://github.com/ardatan/graphql-tools/blob/v6.0.9/packages/delegate/src/results/handleNull.ts#L22 but I don't really understand well enough the thinking behind what that's trying to do, to say if it's wrong or not.
My solution for now has been to use a transformer that throws on all errors, like this:
import { CombinedError, stitchSchemas } from 'graphql-tools';
const throwOnAllResultErrors = {
transformResult(result: any) {
// always throw if an error is returned by the underlying schema
if (result.errors != null && result.errors.length > 0) {
throw new CombinedError(result.errors);
}
return result;
}
};
stitchSchemas({
subschemas: [
{
schema,
transforms: [throwOnAllResultErrors],
},
{
remoteSchema,
transforms: [throwOnAllResultErrors],
},
]
});
Thanks for sharing your workaround.
It would be extremely helpful if you could also open a new issue with a minimal reproduction, including:
Thanks!
@sgarner
Key question is whether subschema provided path on error, see #1641 for example
Your workaround would seem to prevent partial GraphQL response, right?
Most helpful comment
In my case, I have micro-services connected to an api gateway that makes the stitching.
The orignal error was deep buried:
error.orignalError.errors[0].originalError.errors[0]Here is how I solved the issue:
In User micro-service
directrives/auth.jsIn Api Gateway
helpers/searchOriginalError.jsIn Api Gateway
index.jsI hope, this helps.