Skip to content
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

Don't override custom code extensions from scalar parseValue errors #7183

Merged
merged 3 commits into from Nov 23, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/tiny-deers-compare.md
@@ -0,0 +1,6 @@
---
'@apollo/server-integration-testsuite': patch
'@apollo/server': patch
---

Apollo Server tries to detect if execution errors are variable coercion errors in order to give them a `code` extension of `BAD_USER_INPUT` rather than `INTERNAL_SERVER_ERROR`. Previously this would unconditionally set the `code`; now, it only sets the `code` if no `code` is already set, so that (for example) custom scalar `parseValue` methods can throw errors with specific `code`s. (Note that a separate graphql-js bug can lead to these extensions being lost; see https://github.com/graphql/graphql-js/pull/3785 for details.)
101 changes: 101 additions & 0 deletions packages/integration-testsuite/src/apolloServerTests.ts
Expand Up @@ -17,6 +17,7 @@ import {
printSchema,
FieldNode,
GraphQLFormattedError,
GraphQLScalarType,
} from 'graphql';

// Note that by doing deep imports here we don't need to install React.
Expand Down Expand Up @@ -467,6 +468,106 @@ export function defineIntegrationTestSuiteApolloServerTests(
);
expect(result.errors[0].extensions.code).toBe('BAD_USER_INPUT');
});

it('catches custom scalar parseValue and returns BAD_USER_INPUT', async () => {
const uri = await createServerAndGetUrl({
typeDefs: gql`
scalar CustomScalar
type Query {
hello(x: CustomScalar): String
}
`,
resolvers: {
CustomScalar: new GraphQLScalarType({
name: 'CustomScalar',
parseValue() {
// Work-around for https://github.com/graphql/graphql-js/pull/3785
// Once that's fixed, we can just directly throw this error.
const e = new GraphQLError('Something bad happened', {
extensions: { custom: 'foo' },
});
throw new GraphQLError(e.message, { originalError: e });
},
}),
},
});

const apolloFetch = createApolloFetch({ uri });

const result = await apolloFetch({
query: `query ($x:CustomScalar) {hello(x:$x)}`,
variables: { x: 'foo' },
});
expect(result).toMatchInlineSnapshot(`
{
"errors": [
{
"extensions": {
"code": "BAD_USER_INPUT",
"custom": "foo",
},
"locations": [
{
"column": 8,
"line": 1,
},
],
"message": "Variable "$x" got invalid value "foo"; Something bad happened",
},
],
}
`);
});

it('catches custom scalar parseValue and preserves code', async () => {
const uri = await createServerAndGetUrl({
typeDefs: gql`
scalar CustomScalar
type Query {
hello(x: CustomScalar): String
}
`,
resolvers: {
CustomScalar: new GraphQLScalarType({
name: 'CustomScalar',
parseValue() {
// Work-around for https://github.com/graphql/graphql-js/pull/3785
// Once that's fixed, we can just directly throw this error.
const e = new GraphQLError('Something bad happened', {
extensions: { custom: 'foo', code: 'CUSTOMIZED' },
});
throw new GraphQLError(e.message, { originalError: e });
},
}),
},
});

const apolloFetch = createApolloFetch({ uri });

const result = await apolloFetch({
query: `query ($x:CustomScalar) {hello(x:$x)}`,
variables: { x: 'foo' },
});
expect(result).toMatchInlineSnapshot(`
{
"errors": [
{
"extensions": {
"code": "CUSTOMIZED",
"custom": "foo",
},
"locations": [
{
"column": 8,
"line": 1,
},
],
"message": "Variable "$x" got invalid value "foo"; Something bad happened",
},
],
}
`);
});
});

describe('schema creation', () => {
Expand Down
9 changes: 7 additions & 2 deletions packages/server/src/requestPipeline.ts
Expand Up @@ -447,13 +447,18 @@ export async function processGraphQLRequest<TContext extends BaseContext>(
// variables are required and get non-null values. If any of these things
// lead to errors, we change them into UserInputError so that their code
// doesn't end up being INTERNAL_SERVER_ERROR, since these are client
// errors.
// errors. (But if the error already has a code, perhaps because the
// original error was thrown from a custom scalar parseValue, we leave it
// alone. We check that here instead of as part of
// isBadUserInputGraphQLError since perhaps that function will one day be
// changed to something we can get directly from graphql-js, but the
// `code` check is AS-specific.)
//
// This is hacky! Hopefully graphql-js will give us a way to separate
// variable resolution from execution later; see
// https://github.com/graphql/graphql-js/issues/3169
const resultErrors = result.errors?.map((e) => {
if (isBadUserInputGraphQLError(e)) {
if (isBadUserInputGraphQLError(e) && e.extensions?.code != null) {
glasser marked this conversation as resolved.
Show resolved Hide resolved
return new UserInputError(e);
}
return e;
Expand Down