Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions .changeset/fluffy-fans-feel.md
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😍

Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
---
'graphql-yoga': minor
---

Add experimental support for
[`coordinate` error attribute proposal](https://github.com/graphql/graphql-spec/pull/1200).

The `coordinate` attribute indicates the coordinate in the schema of the resolver which experienced
the errors. It allows for an easier error source identification than with the `path` which can be
difficult to walk, or even lead to unsolvable ambiguities when using Union or Interface types.

## Usage

Since this is experimental, it has to be explicitly enabled by adding the appropriate plugin to the
Yoga instance:

```ts
import { createYoga, useErrorCoordinate } from 'graphql-yoga'
import { schema } from './schema'

export const yoga = createYoga({
schema,
plugins: [useErrorCoordinate()]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we enable it by default, since error masking will strip it out anyways?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so, since it is experimental. If the user has it's own masking function, it could leak if it is not updated ^^'

And the OTEL plugin will be able to automatically enable it as needed by using addPlugin (which is also why it's a plugin and not an option!)

})
```

Once enabled, located errors will gain the `coordinate` attribute:

```ts
const myPlugin = {
onExecutionResult({ result }) {
if (result.errors) {
for (const error of result.errors) {
console.log('Error at', error.coordinate, ':', error.message)
}
}
}
}
```

## Security concerns

Adding a schema coordinate to errors exposes information about the schema, which can be an attack
vector if you rely on the fact your schema is private and secret.

This is why the `coordinate` attribute is not serialized by default, and will not be exposed to
clients.

If you want to send this information to client, override either each `toJSON` error's method, or add
a dedicated extension.

```ts
import { GraphQLError } from 'graphql'
import { createYoga, maskError, useErrorCoordinate } from 'graphql-yoga'
import { schema } from './schema'

export const yoga = createYoga({
schema,
plugins: [useErrorCoordinate()],
maskedErrors: {
isDev: process.env['NODE_ENV'] === 'development', // when `isDev` is true, errors are not masked
maskError: (error, message, isDev) => {
if (error instanceof GraphQLError) {
error.toJSON = () => {
// Get default graphql serialized error representation
const json = GraphQLError.prototype.toJSON.apply(error)
// Manually add the coordinate attribute. You can also use extensions instead.
json.coordinate = error.coordinate
return json
}
}

// Keep the default error masking implementation
return maskError(error, message, isDev)
}
}
})
```
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@
},
"overrides": {
"axios": "1.13.2",
"@graphql-tools/executor": "1.5.0-alpha-20251120140925-6fcd4c6ff2d2eb40d5890db29418a15cc4cc3956",
"@graphql-tools/utils": "10.11.0-alpha-20251120140925-6fcd4c6ff2d2eb40d5890db29418a15cc4cc3956",
"estree-util-value-to-estree": "3.5.0",
"eslint-plugin-unicorn": "56.0.1",
"esbuild": "0.25.12",
Expand Down
67 changes: 67 additions & 0 deletions packages/graphql-yoga/__tests__/error-masking.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { ExecutionResult, GraphQLError } from 'graphql';
import { inspect } from '@graphql-tools/utils';
import { createGraphQLError, createLogger, createSchema, createYoga } from '../src/index.js';
import { useErrorCoordinate } from '../src/plugins/use-error-coordinate.js';
import { eventStream } from './utilities.js';

describe('error masking', () => {
Expand Down Expand Up @@ -859,4 +861,69 @@ describe('error masking', () => {
}
`);
});

it('should mask experimental coordinate error attribute on production env', async () => {
let error: GraphQLError | undefined;
const yoga = createYoga({
logging: false,
plugins: [
useErrorCoordinate(),
{
onExecutionResult({ result }) {
error = (result as ExecutionResult).errors?.[0];
},
},
],
schema: createSchema({
typeDefs: /* GraphQL */ `
type Query {
a: String!
b: String!
}
`,
resolvers: {
Query: {
a: () => {
throw createGraphQLError('Test Error');
},
b: () => {
throw new Error('Test Error');
},
},
},
}),
});

const r1 = await yoga.fetch('http://yoga/graphql', {
method: 'POST',
headers: {
accept: 'application/graphql-response+json',
'content-type': 'application/json',
},
body: JSON.stringify({ query: '{ a }' }),
});
const b1 = await r1.json();

expect(error).toMatchObject({
message: 'Test Error',
coordinate: 'Query.a',
});
expect(b1.errors[0].coordinate).toBeUndefined();

const r2 = await yoga.fetch('http://yoga/graphql', {
method: 'POST',
headers: {
accept: 'application/graphql-response+json',
'content-type': 'application/json',
},
body: JSON.stringify({ query: '{ b }' }),
});
const b2 = await r2.json();

expect(error).toMatchObject({
message: 'Unexpected error.',
coordinate: 'Query.b',
});
expect(b2.errors[0].coordinate).toBeUndefined();
});
});
1 change: 1 addition & 0 deletions packages/graphql-yoga/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export { createGraphQLError, isPromise, mapMaybePromise } from '@graphql-tools/u
export { getSSEProcessor } from './plugins/result-processor/sse.js';
export { processRegularResult } from './plugins/result-processor/regular.js';
export { useExecutionCancellation } from './plugins/use-execution-cancellation.js';
export { useErrorCoordinate } from './plugins/use-error-coordinate.js';
export {
type LandingPageRenderer,
type LandingPageRendererOpts,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { GraphQLError } from 'graphql';
import { createGraphQLError } from '@graphql-tools/utils';
import { createGraphQLError, getSchemaCoordinate } from '@graphql-tools/utils';
import { isGraphQLError } from '../../error.js';
import { MaybeArray } from '../../types.js';
import { ExecutionResultWithSerializer } from '../types.js';
Expand Down Expand Up @@ -50,6 +50,7 @@ function omitInternalsFromError<E extends GraphQLError | Error | undefined>(err:
path: err.path,
originalError: omitInternalsFromError(err.originalError || undefined),
extensions: Object.keys(extensions).length ? extensions : undefined,
coordinate: getSchemaCoordinate(err),
}) as E;
}
return err;
Expand Down
10 changes: 10 additions & 0 deletions packages/graphql-yoga/src/plugins/use-error-coordinate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ExecutionArgs } from '@graphql-tools/executor';
import { Plugin } from './types.js';

export function useErrorCoordinate(): Plugin {
return {
onExecute({ args }) {
(args as ExecutionArgs).schemaCoordinateInErrors = true;
},
};
}
3 changes: 2 additions & 1 deletion packages/graphql-yoga/src/utils/mask-error.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createGraphQLError } from '@graphql-tools/utils';
import { createGraphQLError, getSchemaCoordinate } from '@graphql-tools/utils';
import { isGraphQLError, isOriginalGraphQLError } from '../error.js';
import { MaskError } from '../types.js';

Expand Down Expand Up @@ -36,6 +36,7 @@ export const maskError: MaskError = (
errorOptions.source = error.source;
errorOptions.positions = error.positions;
errorOptions.path = error.path;
errorOptions.coordinate = getSchemaCoordinate(error);
if (isDev && error.originalError) {
errorExtensions['originalError'] = serializeError(error.originalError);
}
Expand Down
Loading
Loading