Skip to content

Commit 4f58f0e

Browse files
committed
Don't override custom code extensions from scalar parseValue errors
Adding `BAD_USER_INPUT` is a nice default (and overrides the inappropriate default of `INTERNAL_SERVER_ERROR`) but if somebody has set a `code` already, we shouldn't override. (Note that there's a separate issue where graphql-js throws out extensions from the thrown error itself and only pays attention to extensions on the error's originalError; we're trying to fix that in graphql/graphql-js#3785 but this is orthogonal.) Fixes #7178.
1 parent 37b3b7f commit 4f58f0e

File tree

3 files changed

+114
-2
lines changed

3 files changed

+114
-2
lines changed

.changeset/tiny-deers-compare.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@apollo/server-integration-testsuite': patch
3+
'@apollo/server': patch
4+
---
5+
6+
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.)

packages/integration-testsuite/src/apolloServerTests.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
printSchema,
1818
FieldNode,
1919
GraphQLFormattedError,
20+
GraphQLScalarType,
2021
} from 'graphql';
2122

2223
// Note that by doing deep imports here we don't need to install React.
@@ -467,6 +468,106 @@ export function defineIntegrationTestSuiteApolloServerTests(
467468
);
468469
expect(result.errors[0].extensions.code).toBe('BAD_USER_INPUT');
469470
});
471+
472+
it('catches custom scalar parseValue and returns BAD_USER_INPUT', async () => {
473+
const uri = await createServerAndGetUrl({
474+
typeDefs: gql`
475+
scalar CustomScalar
476+
type Query {
477+
hello(x: CustomScalar): String
478+
}
479+
`,
480+
resolvers: {
481+
CustomScalar: new GraphQLScalarType({
482+
name: 'CustomScalar',
483+
parseValue() {
484+
// Work-around for https://github.com/graphql/graphql-js/pull/3785
485+
// Once that's fixed, we can just directly throw this error.
486+
const e = new GraphQLError('Something bad happened', {
487+
extensions: { custom: 'foo' },
488+
});
489+
throw new GraphQLError(e.message, { originalError: e });
490+
},
491+
}),
492+
},
493+
});
494+
495+
const apolloFetch = createApolloFetch({ uri });
496+
497+
const result = await apolloFetch({
498+
query: `query ($x:CustomScalar) {hello(x:$x)}`,
499+
variables: { x: 'foo' },
500+
});
501+
expect(result).toMatchInlineSnapshot(`
502+
{
503+
"errors": [
504+
{
505+
"extensions": {
506+
"code": "BAD_USER_INPUT",
507+
"custom": "foo",
508+
},
509+
"locations": [
510+
{
511+
"column": 8,
512+
"line": 1,
513+
},
514+
],
515+
"message": "Variable "$x" got invalid value "foo"; Something bad happened",
516+
},
517+
],
518+
}
519+
`);
520+
});
521+
522+
it('catches custom scalar parseValue and preserves code', async () => {
523+
const uri = await createServerAndGetUrl({
524+
typeDefs: gql`
525+
scalar CustomScalar
526+
type Query {
527+
hello(x: CustomScalar): String
528+
}
529+
`,
530+
resolvers: {
531+
CustomScalar: new GraphQLScalarType({
532+
name: 'CustomScalar',
533+
parseValue() {
534+
// Work-around for https://github.com/graphql/graphql-js/pull/3785
535+
// Once that's fixed, we can just directly throw this error.
536+
const e = new GraphQLError('Something bad happened', {
537+
extensions: { custom: 'foo', code: 'CUSTOMIZED' },
538+
});
539+
throw new GraphQLError(e.message, { originalError: e });
540+
},
541+
}),
542+
},
543+
});
544+
545+
const apolloFetch = createApolloFetch({ uri });
546+
547+
const result = await apolloFetch({
548+
query: `query ($x:CustomScalar) {hello(x:$x)}`,
549+
variables: { x: 'foo' },
550+
});
551+
expect(result).toMatchInlineSnapshot(`
552+
{
553+
"errors": [
554+
{
555+
"extensions": {
556+
"code": "CUSTOMIZED",
557+
"custom": "foo",
558+
},
559+
"locations": [
560+
{
561+
"column": 8,
562+
"line": 1,
563+
},
564+
],
565+
"message": "Variable "$x" got invalid value "foo"; Something bad happened",
566+
},
567+
],
568+
}
569+
`);
570+
});
470571
});
471572

472573
describe('schema creation', () => {

packages/server/src/requestPipeline.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -447,13 +447,18 @@ export async function processGraphQLRequest<TContext extends BaseContext>(
447447
// variables are required and get non-null values. If any of these things
448448
// lead to errors, we change them into UserInputError so that their code
449449
// doesn't end up being INTERNAL_SERVER_ERROR, since these are client
450-
// errors.
450+
// errors. (But if the error already has a code, perhaps because the
451+
// original error was thrown from a custom scalar parseValue, we leave it
452+
// alone. We check that here instead of as part of
453+
// isBadUserInputGraphQLError since perhaps that function will one day be
454+
// changed to something we can get directly from graphql-js, but the
455+
// `code` check is AS-specific.)
451456
//
452457
// This is hacky! Hopefully graphql-js will give us a way to separate
453458
// variable resolution from execution later; see
454459
// https://github.com/graphql/graphql-js/issues/3169
455460
const resultErrors = result.errors?.map((e) => {
456-
if (isBadUserInputGraphQLError(e)) {
461+
if (isBadUserInputGraphQLError(e) && !e.extensions?.code) {
457462
return new UserInputError(e);
458463
}
459464
return e;

0 commit comments

Comments
 (0)