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

Fix ApolloError bug and GraphQLError spec compliance #3408

Merged
merged 7 commits into from
Oct 15, 2019
Merged
Show file tree
Hide file tree
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ The version headers in this history reflect the versions of Apollo Server itself
- `@apollo/gateway`, `@apollo/federation`, `apollo-engine-reporting`: Update `apollo-graphql` dependency to bring in [`apollo-tooling`'s #1551](https://github.com/apollographql/apollo-tooling/pull/1551) which resolve runtime errors when its source is minified. While this fixes a particular minification bug when Apollo Server packages are minified, we _do not_ recommend minification of server code in most cases. [PR #3387](https://github.com/apollographql/apollo-server/pull/3387) [Issue #3335](https://github.com/apollographql/apollo-server/issues/3335)
- `apollo-server-koa`: Correctly declare dependency on `koa-compose`. [PR #3356](https://github.com/apollographql/apollo-server/pull/3356)
- `apollo-server-core`: Preserve any `extensions` that have been placed on the response when pre-execution errors occur. [PR #3394](https://github.com/apollographql/apollo-server/pull/3394)
- `apollo-server-errors`: Fix `ApolloError` bug and `GraphQLError` spec compliance [#3408](https://github.com/apollographql/apollo-server/pull/3408)

### v2.9.3

Expand Down
3 changes: 3 additions & 0 deletions packages/apollo-server-errors/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const config = require('../../jest.config.base');

module.exports = Object.assign(Object.create(null), config);
49 changes: 49 additions & 0 deletions packages/apollo-server-errors/src/__tests__/ApolloError.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { ApolloError } from '..';

describe('ApolloError', () => {
it("doesn't overwrite extensions when provided in the constructor", () => {
const error = new ApolloError('My message', 'A_CODE', {
arbitrary: 'user_data',
});

expect(error.extensions).toEqual({
code: 'A_CODE',
arbitrary: 'user_data',
});
});

it("a code property doesn't overwrite the code provided to the constructor", () => {
const error = new ApolloError('My message', 'A_CODE', {
code: 'CANT_OVERWRITE',
});

expect(error.extensions).toEqual({
code: 'A_CODE',
});
});

// This is a byproduct of how we currently assign properties from the 3rd constructor
// argument onto properties of the class itself. This is expected, but deprecated behavior
// and as such this test should be deleted in the future when we make that breaking change.
it("a message property doesn't overwrite the message provided to the constructor", () => {
const error = new ApolloError('My original message', 'A_CODE', {
message:
"This message can't overwrite the original message, but it does end up in extensions",
});

expect(error.message).toEqual('My original message');
expect(error.extensions.message).toEqual(
"This message can't overwrite the original message, but it does end up in extensions",
);
});

it('(back-compat) sets extensions correctly for users who use an extensions key in the third constructor argument', () => {
const error = new ApolloError('My original message', 'A_CODE', {
extensions: {
arbitrary: 'user_data',
},
});

expect(error.extensions.arbitrary).toEqual('user_data');
});
});
7 changes: 7 additions & 0 deletions packages/apollo-server-errors/src/__tests__/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": "../../../../tsconfig.test.base",
"include": ["**/*"],
"references": [
{ "path": "../../" }
]
}
35 changes: 27 additions & 8 deletions packages/apollo-server-errors/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,43 @@ export class ApolloError extends Error implements GraphQLError {
constructor(
message: string,
code?: string,
properties?: Record<string, any>,
extensions?: Record<string, any>,
) {
super(message);

if (properties) {
Object.keys(properties).forEach(key => {
this[key] = properties[key];
});
// This variable was previously named `properties`, which allowed users to set
// arbitrary properties on the ApolloError object. This use case is still supported,
// but deprecated in favor of using the ApolloError.extensions object instead.
// This change intends to comply with the GraphQL spec on errors. See:
// https://github.com/graphql/graphql-spec/blob/master/spec/Section%207%20--%20Response.md#response-format
//
// Going forward, users should use the ApolloError.extensions object for storing
// and reading arbitrary data on an error, as arbitrary properties on the ApolloError
// itself won't be supported in the future.
//
// XXX Filter 'message' and 'extensions' specifically so they don't overwrite the class property.
// We _could_ filter all of the class properties, but have chosen to only do
// so if it's an issue for other users. Please feel free to open an issue if you
// find yourself here with this exact problem.
if (extensions) {
Object.keys(extensions)
.filter(keyName => keyName !== 'message' && keyName !== 'extensions')
.forEach(key => {
this[key] = extensions[key];
});
}

// if no name provided, use the default. defineProperty ensures that it stays non-enumerable
if (!this.name) {
Object.defineProperty(this, 'name', { value: 'ApolloError' });
}

// extensions are flattened to be included in the root of GraphQLError's, so
// don't add properties to extensions
this.extensions = { code };
// Before the mentioned change to extensions, users could previously set the extensions
// object by providing it as a key on the third argument to the constructor.
// This step provides backwards compatibility for those hypothetical users.
const userProvidedExtensions = (extensions && extensions.extensions) || null;

this.extensions = { ...extensions, ...userProvidedExtensions, code };
}
}

Expand Down
1 change: 1 addition & 0 deletions tsconfig.test.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
{ "path": "./packages/apollo-server-caching/src/__tests__/" },
{ "path": "./packages/apollo-server-cloud-functions/src/__tests__/" },
{ "path": "./packages/apollo-server-core/src/__tests__/" },
{ "path": "./packages/apollo-server-errors/src/__tests__/" },
{ "path": "./packages/apollo-server-express/src/__tests__/" },
{ "path": "./packages/apollo-server-fastify/src/__tests__/" },
{ "path": "./packages/apollo-server-hapi/src/__tests__/" },
Expand Down