Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@graphql-yoga/plugin-apollo-usage-report": patch
---
dependencies updates:
- 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`)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@graphql-yoga/plugin-defer-stream": patch
---
dependencies updates:
- 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`)
5 changes: 5 additions & 0 deletions .changeset/@graphql-yoga_plugin-sofa-4288-dependencies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@graphql-yoga/plugin-sofa": patch
---
dependencies updates:
- 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`)
78 changes: 78 additions & 0 deletions .changeset/fluffy-fans-feel.md
Copy link
Copy Markdown
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()]
Comment thread
n1ru4l marked this conversation as resolved.
})
```

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)
}
}
})
```
6 changes: 6 additions & 0 deletions .changeset/graphql-yoga-4288-dependencies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"graphql-yoga": patch
---
dependencies updates:
- 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`)
- 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`)
2 changes: 1 addition & 1 deletion examples/egg/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"transpile": "tsc"
},
"dependencies": {
"@graphql-tools/utils": "10.10.1",
"@graphql-tools/utils": "10.11.0",
"egg": "3.31.0",
"egg-cors": "3.0.1",
"graphql": "16.12.0",
Expand Down
2 changes: 1 addition & 1 deletion examples/fastify/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"pino-pretty": "13.1.2"
},
"devDependencies": {
"@graphql-tools/utils": "^10.6.1",
"@graphql-tools/utils": "^10.11.0",
"@types/node": "24.10.0",
"ts-node": "10.9.2"
}
Expand Down
2 changes: 1 addition & 1 deletion examples/live-query/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
},
"dependencies": {
"@envelop/live-query": "10.0.0",
"@graphql-tools/utils": "10.10.1",
"@graphql-tools/utils": "10.11.0",
"@n1ru4l/graphql-live-query": "0.10.0",
"@n1ru4l/in-memory-live-query-store": "0.10.0",
"graphql": "16.12.0",
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();
});
});
4 changes: 2 additions & 2 deletions packages/graphql-yoga/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@
"dependencies": {
"@envelop/core": "^5.3.0",
"@envelop/instrumentation": "^1.0.0",
"@graphql-tools/executor": "^1.4.0",
"@graphql-tools/executor": "^1.5.0",
"@graphql-tools/schema": "^10.0.11",
"@graphql-tools/utils": "^10.6.2",
"@graphql-tools/utils": "^10.11.0",
"@graphql-yoga/logger": "workspace:^",
"@graphql-yoga/subscription": "workspace:^",
"@whatwg-node/fetch": "^0.10.6",
Expand Down
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
2 changes: 1 addition & 1 deletion packages/plugins/apollo-usage-report/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"@apollo/server-gateway-interface": "^2.0.0",
"@apollo/usage-reporting-protobuf": "^4.1.1",
"@apollo/utils.usagereporting": "^2.1.0",
"@graphql-tools/utils": "^10.9.1",
"@graphql-tools/utils": "^10.11.0",
"@graphql-yoga/plugin-apollo-inline-trace": "workspace:^",
"@whatwg-node/promise-helpers": "^1.2.4",
"tslib": "^2.8.1"
Expand Down
2 changes: 1 addition & 1 deletion packages/plugins/defer-stream/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"graphql-yoga": "workspace:^"
},
"dependencies": {
"@graphql-tools/utils": "^10.6.1"
"@graphql-tools/utils": "^10.11.0"
},
"devDependencies": {
"@graphql-tools/executor-http": "^3.0.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/plugins/sofa/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"graphql-yoga": "workspace:^"
},
"dependencies": {
"@graphql-tools/utils": "^10.3.2",
"@graphql-tools/utils": "^10.11.0",
"@whatwg-node/promise-helpers": "^1.2.4",
"sofa-api": "^0.18.8"
},
Expand Down
Loading
Loading