Skip to content

fix: describe behaviour for onValidate maskError (see discussion question) #3899

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
97 changes: 97 additions & 0 deletions packages/graphql-yoga/src/plugins/plugins.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { AfterValidateHook } from '@envelop/core';
import { createGraphQLError } from '@graphql-tools/utils';
import { createSchema } from '../schema.js';
import { createYoga } from '../server.js';
import { maskError } from '../utils/mask-error.js';
import { Plugin } from './types.js';

const schema = createSchema({
Expand Down Expand Up @@ -54,4 +55,100 @@ describe('Yoga Plugins', () => {
],
});
});

describe('onValidate', () => {
it('should mask errors', async () => {
const onValidatePlugin: Plugin = {
onValidate({ setResult }) {
setResult([
createGraphQLError('UNMASKED ERROR', { extensions: { code: 'VALIDATION_ERROR' } }),
Copy link
Member

@ardatan ardatan Mar 17, 2025

Choose a reason for hiding this comment

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

GraphQLError instances are not masked. The errors are masked only if it is an error is not an instance of GraphQLError.
And the result of validate function is GraphQLError[] so it is expected to return an array of GraphQLError. Yoga doesn't expect validate to return non GraphQL errors so they are not sent to maskError function. So running an extra maskError for validation errors are unnecesary.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks @ardatan! We realize that the default maskError function does not act on GraphQLErrors at all, but we want to mask GraphQLErrors as well.

However, even if we write a custom maskError function, it is not triggered at all for GraphQLErrors.

Would it be better to use onResultProcess to update the result with the masked error?

Copy link
Member

@ardatan ardatan Mar 20, 2025

Choose a reason for hiding this comment

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

Yes!, You can use onResultProcess to modify the final response, or you can basically use onValidateDone if you want to modify validation errors specifically.

]);
},
};
const yoga = createYoga({
plugins: [onValidatePlugin],
schema,
maskedErrors: {
maskError(error) {
return maskError(error, 'MASKED ERROR');
},
},
});
const response = await yoga.fetch('http://localhost:3000/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: '{hello}',
}),
});
const result = await response.json();
expect(result).toMatchObject({
errors: [{ message: 'MASKED ERROR' }],
});
});

it('should consistently return masked errors and log the original error across requests', async () => {
const customMaskingFunction = jest.fn().mockImplementation(error => {
return maskError(error, 'MASKED ERROR');
});
const loggerFn = jest.fn();
const onValidatePlugin: Plugin = {
onValidate({ setResult }) {
return ({ result }) => {
// this receives masked error from validation cache
loggerFn(result[0].message);
const maskedError = customMaskingFunction(
createGraphQLError('MASKED ERROR', { extensions: { code: 'VALIDATION_ERROR' } }),
);
setResult([maskedError]);
};
},
};

const yoga = createYoga({
plugins: [onValidatePlugin],
schema,
maskedErrors: {
maskError: customMaskingFunction,
},
});

const makeRequest = () =>
yoga.fetch('http://localhost:3000/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: '{invalid_hello}',
}),
});

const [response1, response2] = await Promise.all([makeRequest(), makeRequest()]);

const result1 = await response1.json();
const result2 = await response2.json();

expect(result1).toMatchObject({
errors: [{ message: 'MASKED ERROR' }],
});
expect(result2).toMatchObject({
errors: [{ message: 'MASKED ERROR' }],
});

expect(loggerFn.mock.calls.length).toEqual(2);
expect(loggerFn.mock.calls[0][0]).toEqual(
`Cannot query field "invalid_hello" on type "Query".`,
);

//fails with
// Expected: "Cannot query field \"invalid_hello\" on type \"Query\"."
// Received: "MASKED ERROR"
expect(loggerFn.mock.calls[1][0]).toEqual(
`Cannot query field "invalid_hello" on type "Query".`,
);
});
});
});
Loading