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

Remove exports of named errors #6705

Merged
merged 5 commits into from
Jul 20, 2022
Merged
Show file tree
Hide file tree
Changes from 4 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
41 changes: 25 additions & 16 deletions packages/integration-testsuite/src/apolloServerTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,7 @@ import {
ApolloFetch,
ParsedResponse,
} from './apolloFetch.js';
import {
AuthenticationError,
UserInputError,
import type {
ApolloServerOptions,
ApolloServer,
GatewayInterface,
Expand Down Expand Up @@ -825,7 +823,10 @@ export function defineIntegrationTestSuiteApolloServerTests(
});

const formatError = jest.fn(() => {
return new UserInputError('User Input Error').toJSON();
return {
message: 'User Input Error',
extensions: { code: 'BAD_USER_INPUT' },
};
});

const uri = await createServerAndGetUrl({
Expand Down Expand Up @@ -1673,7 +1674,9 @@ export function defineIntegrationTestSuiteApolloServerTests(
},
{
context: async () => {
throw new AuthenticationError('valid result');
throw new GraphQLError('valid result', {
extensions: { code: 'SOME_CODE' },
});
},
},
);
Expand All @@ -1687,14 +1690,14 @@ export function defineIntegrationTestSuiteApolloServerTests(
const e = result.errors[0];
expect(e.message).toMatch('valid result');
expect(e.extensions).toBeDefined();
expect(e.extensions.code).toEqual('UNAUTHENTICATED');
expect(e.extensions.code).toEqual('SOME_CODE');
expect(e.extensions.exception.stacktrace).toBeDefined();

expect(contextCreationDidFail.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
Object {
"error": [AuthenticationError: Context creation failed: valid result],
"error": [GraphQLError: Context creation failed: valid result],
},
],
]
Expand Down Expand Up @@ -1743,7 +1746,9 @@ export function defineIntegrationTestSuiteApolloServerTests(
resolvers: {
Query: {
fieldWhichWillError: () => {
throw new AuthenticationError('we the best music');
throw new GraphQLError('we the best music', {
extensions: { code: 'SOME_CODE' },
});
},
},
},
Expand All @@ -1759,7 +1764,7 @@ export function defineIntegrationTestSuiteApolloServerTests(

expect(result.errors).toBeDefined();
expect(result.errors.length).toEqual(1);
expect(result.errors[0].extensions.code).toEqual('UNAUTHENTICATED');
expect(result.errors[0].extensions.code).toEqual('SOME_CODE');
expect(result.errors[0].extensions.exception).toBeUndefined();
});

Expand All @@ -1773,7 +1778,9 @@ export function defineIntegrationTestSuiteApolloServerTests(
resolvers: {
Query: {
fieldWhichWillError: () => {
throw new AuthenticationError('we the best music');
throw new GraphQLError('we the best music', {
extensions: { code: 'SOME_CODE' },
});
},
},
},
Expand All @@ -1788,7 +1795,7 @@ export function defineIntegrationTestSuiteApolloServerTests(

expect(result.errors).toBeDefined();
expect(result.errors.length).toEqual(1);
expect(result.errors[0].extensions.code).toEqual('UNAUTHENTICATED');
expect(result.errors[0].extensions.code).toEqual('SOME_CODE');
expect(result.errors[0].extensions.exception).toBeUndefined();
});

Expand All @@ -1802,7 +1809,9 @@ export function defineIntegrationTestSuiteApolloServerTests(
resolvers: {
Query: {
fieldWhichWillError: () => {
throw new AuthenticationError('we the best music');
throw new GraphQLError('we the best music', {
extensions: { code: 'SOME_CODE' },
});
},
},
},
Expand All @@ -1818,7 +1827,7 @@ export function defineIntegrationTestSuiteApolloServerTests(

expect(result.errors).toBeDefined();
expect(result.errors.length).toEqual(1);
expect(result.errors[0].extensions.code).toEqual('UNAUTHENTICATED');
expect(result.errors[0].extensions.code).toEqual('SOME_CODE');
expect(result.errors[0].extensions.exception).toBeDefined();
expect(result.errors[0].extensions.exception.stacktrace).toBeDefined();
});
Expand All @@ -1833,8 +1842,8 @@ export function defineIntegrationTestSuiteApolloServerTests(
resolvers: {
Query: {
fieldWhichWillError: () => {
throw new AuthenticationError('Some message', {
extensions: { ext1: 'myExt' },
throw new GraphQLError('Some message', {
extensions: { ext1: 'myExt', code: 'SOME_CODE' },
});
},
},
Expand All @@ -1854,7 +1863,7 @@ export function defineIntegrationTestSuiteApolloServerTests(
path: ['fieldWhichWillError'],
locations: [{ line: 1, column: 2 }],
extensions: {
code: 'UNAUTHENTICATED',
code: 'SOME_CODE',
ext1: 'myExt',
},
},
Expand Down
8 changes: 8 additions & 0 deletions packages/server/errors/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "@apollo/server/errors",
"type": "module",
"main": "../dist/cjs/errors/index.js",
"module": "../dist/esm/errors/index.js",
"types": "../dist/esm/errors/index.d.ts",
"sideEffects": false
}
5 changes: 5 additions & 0 deletions packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
"import": "./dist/esm/index.js",
"require": "./dist/cjs/index.js"
},
"./errors": {
"types": "./dist/esm/errors/index.d.ts",
"import": "./dist/esm/errors/index.js",
"require": "./dist/cjs/errors/index.js"
},
"./express4": {
"types": "./dist/esm/express4/index.d.ts",
"import": "./dist/esm/express4/index.js",
Expand Down
12 changes: 6 additions & 6 deletions packages/server/src/ApolloServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,8 @@ import Negotiator from 'negotiator';
import * as uuid from 'uuid';
import { newCachePolicy } from './cachePolicy.js';
import { determineApolloConfig } from './determineApolloConfig.js';
import {
BadRequestError,
ensureError,
normalizeAndFormatErrors,
} from './errors.js';
import { ensureError, normalizeAndFormatErrors } from './errorNormalize.js';
import { ApolloServerErrorCode } from './errors/index.js';
import type {
ApolloServerPlugin,
BaseContext,
Expand Down Expand Up @@ -1018,7 +1015,10 @@ export class ApolloServer<in out TContext extends BaseContext = BaseContext> {
);
} catch (maybeError_: unknown) {
const maybeError = maybeError_; // fixes inference because catch vars are not const
if (maybeError instanceof BadRequestError) {
if (
maybeError instanceof GraphQLError &&
maybeError.extensions.code === ApolloServerErrorCode.BAD_REQUEST
) {
try {
await Promise.all(
this.internals.plugins.map(async (plugin) =>
Expand Down
27 changes: 0 additions & 27 deletions packages/server/src/__tests__/ApolloError.test.ts

This file was deleted.

83 changes: 1 addition & 82 deletions packages/server/src/__tests__/errors.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
import { GraphQLError } from 'graphql';

import {
AuthenticationError,
ForbiddenError,
ValidationError,
UserInputError,
SyntaxError,
} from '..';
import { normalizeAndFormatErrors } from '../errors.js';
import { normalizeAndFormatErrors } from '../errorNormalize.js';

describe('Errors', () => {
describe('normalizeAndFormatErrors', () => {
Expand Down Expand Up @@ -108,78 +101,4 @@ describe('Errors', () => {
expect(JSON.parse(JSON.stringify(formattedError)).message).toBe('Hello');
});
});
describe('Named Errors', () => {
const message = 'message';
function verifyError(
error: GraphQLError,
{
code,
errorClass,
name,
}: { code: string; errorClass: any; name: string },
) {
expect(error.message).toEqual(message);
expect(error.extensions.code).toEqual(code);
expect(error.name).toEqual(name);
expect(error instanceof GraphQLError).toBe(true);
expect(error instanceof errorClass).toBe(true);
}

it('provides an authentication error', () => {
verifyError(new AuthenticationError(message), {
code: 'UNAUTHENTICATED',
errorClass: AuthenticationError,
name: 'AuthenticationError',
});
});
it('provides a forbidden error', () => {
verifyError(new ForbiddenError(message), {
code: 'FORBIDDEN',
errorClass: ForbiddenError,
name: 'ForbiddenError',
});
});
it('provides a syntax error', () => {
verifyError(new SyntaxError(new GraphQLError(message)), {
code: 'GRAPHQL_PARSE_FAILED',
errorClass: SyntaxError,
name: 'SyntaxError',
});
});
it('provides a validation error', () => {
verifyError(new ValidationError(new GraphQLError(message)), {
code: 'GRAPHQL_VALIDATION_FAILED',
errorClass: ValidationError,
name: 'ValidationError',
});
});
it('provides a user input error', () => {
const error = new UserInputError(message, {
extensions: {
field1: 'property1',
field2: 'property2',
},
});
verifyError(error, {
code: 'BAD_USER_INPUT',
errorClass: UserInputError,
name: 'UserInputError',
});

const formattedError = normalizeAndFormatErrors([
new GraphQLError(
error.message,
undefined,
undefined,
undefined,
undefined,
error,
),
])[0];

expect(formattedError.extensions?.field1).toEqual('property1');
expect(formattedError.extensions?.field2).toEqual('property2');
expect(formattedError.extensions?.exception).toBeUndefined();
});
});
});
95 changes: 95 additions & 0 deletions packages/server/src/errorNormalize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// The functions in this file are not part of Apollo Server's external API.
Copy link
Member

@trevor-scheer trevor-scheer Jul 20, 2022

Choose a reason for hiding this comment

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

Seems like this still now belongs in src/errors/ but fine either way

Copy link
Member

Choose a reason for hiding this comment

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

Hmm. My thought was that since this code is entirely consumed outside of that sub-export, then it didn't make sense for it to be in the subdirectory? I guess that the tree shaking bundlers we care about probably won't scoop up those files if you don't do the top export, but since the whole point is to minimize src/errors to the stuff that clients might need it seemed like a bad idea to put unrelated stuff there. I thought about doing src/internalErrors or something but it seemed like overkill. Thoughts?

Copy link
Member

Choose a reason for hiding this comment

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

Fine as is, didn't feel too strongly about this. Agree with not doing an internalErrors folder.


import {
GraphQLError,
GraphQLErrorExtensions,
GraphQLFormattedError,
} from 'graphql';

declare module 'graphql' {
export interface GraphQLErrorExtensions {
code?: string;
exception?: {
stacktrace?: ReadonlyArray<string>;
};
}
}

// This function accepts any value that were thrown and convert it to GraphQLFormatterError.
// It also add default extensions.code and copy stack trace onto an extension if requested.
// This function should not throw.
export function normalizeAndFormatErrors(
errors: ReadonlyArray<unknown>,
options: {
formatError?: (
formattedError: GraphQLFormattedError,
error: unknown,
) => GraphQLFormattedError;
includeStackTracesInErrorResponses?: boolean;
} = {},
): Array<GraphQLFormattedError> {
const formatError = options.formatError ?? ((error) => error);
return errors.map((error) => {
try {
return formatError(enrichError(error), error);
} catch (formattingError) {
if (options.includeStackTracesInErrorResponses) {
// includeStackTracesInErrorResponses is used in development
// so it will be helpful to show errors thrown by formatError hooks in that mode
return enrichError(formattingError);
} else {
// obscure error
return {
message: 'Internal server error',
extensions: { code: 'INTERNAL_SERVER_ERROR' },
};
}
}
});

function enrichError(maybeError: unknown): GraphQLFormattedError {
const graphqlError = ensureGraphQLError(maybeError);

const extensions: GraphQLErrorExtensions = {
...graphqlError.extensions,
code: graphqlError.extensions.code ?? 'INTERNAL_SERVER_ERROR',
};

const { originalError } = graphqlError;
if (originalError != null && !(originalError instanceof GraphQLError)) {
const originalErrorEnumerableEntries = Object.entries(
originalError,
).filter(([key]) => key !== 'extensions');

if (originalErrorEnumerableEntries.length > 0) {
extensions.exception = {
...extensions.exception,
...Object.fromEntries(originalErrorEnumerableEntries),
};
}
}

if (options.includeStackTracesInErrorResponses) {
extensions.exception = {
...extensions.exception,
stacktrace: graphqlError.stack?.split('\n'),
};
}

return { ...graphqlError.toJSON(), extensions };
}
}

export function ensureError(maybeError: unknown): Error {
return maybeError instanceof Error
? maybeError
: new GraphQLError('Unexpected error value: ' + String(maybeError));
}

export function ensureGraphQLError(maybeError: unknown): GraphQLError {
const error: Error = ensureError(maybeError);

return error instanceof GraphQLError
? error
: new GraphQLError(error.message, { originalError: error });
}
Loading