Skip to content

Commit 66c370c

Browse files
feat(graphql-yoga): add support for experimental error coordinate (#4288)
* feat(graphql-yoga): add support for experimental error coordinate feat(graphql-yoga): add support for experimental error coordinate * prettier * changeset * fix tests, and add normal error * remove only * export useErrorCorrdinate plugin * use toJSON to maske coordinates * coordinate is never serialized, but should apear in error instance * changeset * update to latest graphql-tools * chore(dependencies): updated changesets for modified dependencies * update lockfile --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 1315ea2 commit 66c370c

18 files changed

Lines changed: 332 additions & 158 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@graphql-yoga/plugin-apollo-usage-report": patch
3+
---
4+
dependencies updates:
5+
- Updated dependency [`@graphql-tools/utils@^10.11.0` ↗︎](https://www.npmjs.com/package/@graphql-tools/utils/v/10.11.0) (from `^10.9.1`, in `dependencies`)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@graphql-yoga/plugin-defer-stream": patch
3+
---
4+
dependencies updates:
5+
- Updated dependency [`@graphql-tools/utils@^10.11.0` ↗︎](https://www.npmjs.com/package/@graphql-tools/utils/v/10.11.0) (from `^10.6.1`, in `dependencies`)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@graphql-yoga/plugin-sofa": patch
3+
---
4+
dependencies updates:
5+
- Updated dependency [`@graphql-tools/utils@^10.11.0` ↗︎](https://www.npmjs.com/package/@graphql-tools/utils/v/10.11.0) (from `^10.3.2`, in `dependencies`)

.changeset/fluffy-fans-feel.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
---
2+
'graphql-yoga': minor
3+
---
4+
5+
Add experimental support for
6+
[`coordinate` error attribute proposal](https://github.com/graphql/graphql-spec/pull/1200).
7+
8+
The `coordinate` attribute indicates the coordinate in the schema of the resolver which experienced
9+
the errors. It allows for an easier error source identification than with the `path` which can be
10+
difficult to walk, or even lead to unsolvable ambiguities when using Union or Interface types.
11+
12+
## Usage
13+
14+
Since this is experimental, it has to be explicitly enabled by adding the appropriate plugin to the
15+
Yoga instance:
16+
17+
```ts
18+
import { createYoga, useErrorCoordinate } from 'graphql-yoga'
19+
import { schema } from './schema'
20+
21+
export const yoga = createYoga({
22+
schema,
23+
plugins: [useErrorCoordinate()]
24+
})
25+
```
26+
27+
Once enabled, located errors will gain the `coordinate` attribute:
28+
29+
```ts
30+
const myPlugin = {
31+
onExecutionResult({ result }) {
32+
if (result.errors) {
33+
for (const error of result.errors) {
34+
console.log('Error at', error.coordinate, ':', error.message)
35+
}
36+
}
37+
}
38+
}
39+
```
40+
41+
## Security concerns
42+
43+
Adding a schema coordinate to errors exposes information about the schema, which can be an attack
44+
vector if you rely on the fact your schema is private and secret.
45+
46+
This is why the `coordinate` attribute is not serialized by default, and will not be exposed to
47+
clients.
48+
49+
If you want to send this information to client, override either each `toJSON` error's method, or add
50+
a dedicated extension.
51+
52+
```ts
53+
import { GraphQLError } from 'graphql'
54+
import { createYoga, maskError, useErrorCoordinate } from 'graphql-yoga'
55+
import { schema } from './schema'
56+
57+
export const yoga = createYoga({
58+
schema,
59+
plugins: [useErrorCoordinate()],
60+
maskedErrors: {
61+
isDev: process.env['NODE_ENV'] === 'development', // when `isDev` is true, errors are not masked
62+
maskError: (error, message, isDev) => {
63+
if (error instanceof GraphQLError) {
64+
error.toJSON = () => {
65+
// Get default graphql serialized error representation
66+
const json = GraphQLError.prototype.toJSON.apply(error)
67+
// Manually add the coordinate attribute. You can also use extensions instead.
68+
json.coordinate = error.coordinate
69+
return json
70+
}
71+
}
72+
73+
// Keep the default error masking implementation
74+
return maskError(error, message, isDev)
75+
}
76+
}
77+
})
78+
```
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"graphql-yoga": patch
3+
---
4+
dependencies updates:
5+
- Updated dependency [`@graphql-tools/executor@^1.5.0` ↗︎](https://www.npmjs.com/package/@graphql-tools/executor/v/1.5.0) (from `^1.4.0`, in `dependencies`)
6+
- Updated dependency [`@graphql-tools/utils@^10.11.0` ↗︎](https://www.npmjs.com/package/@graphql-tools/utils/v/10.11.0) (from `^10.6.2`, in `dependencies`)

examples/egg/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"transpile": "tsc"
99
},
1010
"dependencies": {
11-
"@graphql-tools/utils": "10.10.1",
11+
"@graphql-tools/utils": "10.11.0",
1212
"egg": "3.31.0",
1313
"egg-cors": "3.0.1",
1414
"graphql": "16.12.0",

examples/fastify/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"pino-pretty": "13.1.2"
1414
},
1515
"devDependencies": {
16-
"@graphql-tools/utils": "^10.6.1",
16+
"@graphql-tools/utils": "^10.11.0",
1717
"@types/node": "24.10.0",
1818
"ts-node": "10.9.2"
1919
}

examples/live-query/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
},
1010
"dependencies": {
1111
"@envelop/live-query": "10.0.0",
12-
"@graphql-tools/utils": "10.10.1",
12+
"@graphql-tools/utils": "10.11.0",
1313
"@n1ru4l/graphql-live-query": "0.10.0",
1414
"@n1ru4l/in-memory-live-query-store": "0.10.0",
1515
"graphql": "16.12.0",

packages/graphql-yoga/__tests__/error-masking.spec.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import { ExecutionResult, GraphQLError } from 'graphql';
12
import { inspect } from '@graphql-tools/utils';
23
import { createGraphQLError, createLogger, createSchema, createYoga } from '../src/index.js';
4+
import { useErrorCoordinate } from '../src/plugins/use-error-coordinate.js';
35
import { eventStream } from './utilities.js';
46

57
describe('error masking', () => {
@@ -859,4 +861,69 @@ describe('error masking', () => {
859861
}
860862
`);
861863
});
864+
865+
it('should mask experimental coordinate error attribute on production env', async () => {
866+
let error: GraphQLError | undefined;
867+
const yoga = createYoga({
868+
logging: false,
869+
plugins: [
870+
useErrorCoordinate(),
871+
{
872+
onExecutionResult({ result }) {
873+
error = (result as ExecutionResult).errors?.[0];
874+
},
875+
},
876+
],
877+
schema: createSchema({
878+
typeDefs: /* GraphQL */ `
879+
type Query {
880+
a: String!
881+
b: String!
882+
}
883+
`,
884+
resolvers: {
885+
Query: {
886+
a: () => {
887+
throw createGraphQLError('Test Error');
888+
},
889+
b: () => {
890+
throw new Error('Test Error');
891+
},
892+
},
893+
},
894+
}),
895+
});
896+
897+
const r1 = await yoga.fetch('http://yoga/graphql', {
898+
method: 'POST',
899+
headers: {
900+
accept: 'application/graphql-response+json',
901+
'content-type': 'application/json',
902+
},
903+
body: JSON.stringify({ query: '{ a }' }),
904+
});
905+
const b1 = await r1.json();
906+
907+
expect(error).toMatchObject({
908+
message: 'Test Error',
909+
coordinate: 'Query.a',
910+
});
911+
expect(b1.errors[0].coordinate).toBeUndefined();
912+
913+
const r2 = await yoga.fetch('http://yoga/graphql', {
914+
method: 'POST',
915+
headers: {
916+
accept: 'application/graphql-response+json',
917+
'content-type': 'application/json',
918+
},
919+
body: JSON.stringify({ query: '{ b }' }),
920+
});
921+
const b2 = await r2.json();
922+
923+
expect(error).toMatchObject({
924+
message: 'Unexpected error.',
925+
coordinate: 'Query.b',
926+
});
927+
expect(b2.errors[0].coordinate).toBeUndefined();
928+
});
862929
});

packages/graphql-yoga/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@
5151
"dependencies": {
5252
"@envelop/core": "^5.3.0",
5353
"@envelop/instrumentation": "^1.0.0",
54-
"@graphql-tools/executor": "^1.4.0",
54+
"@graphql-tools/executor": "^1.5.0",
5555
"@graphql-tools/schema": "^10.0.11",
56-
"@graphql-tools/utils": "^10.6.2",
56+
"@graphql-tools/utils": "^10.11.0",
5757
"@graphql-yoga/logger": "workspace:^",
5858
"@graphql-yoga/subscription": "workspace:^",
5959
"@whatwg-node/fetch": "^0.10.6",

0 commit comments

Comments
 (0)