Skip to content

Commit

Permalink
Merge pull request #1113 from apollographql/server-2.0/context-fix
Browse files Browse the repository at this point in the history
AS2: Fix handling errors from context constructor and merged schemas
  • Loading branch information
evans authored May 31, 2018
2 parents 5bb652a + 63d0f72 commit 38c8713
Show file tree
Hide file tree
Showing 7 changed files with 328 additions and 34 deletions.
125 changes: 125 additions & 0 deletions packages/apollo-server-core/src/ApolloServer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Object.assign(global, {

import { createApolloFetch } from 'apollo-fetch';
import { ApolloServerBase } from './ApolloServer';
import { AuthenticationError } from './errors';
import { runHttpQuery } from './runHttpQuery';
import gqlTag from 'graphql-tag';

Expand Down Expand Up @@ -367,6 +368,130 @@ describe('ApolloServerBase', () => {
expect(spy.calledTwice).true;
await server.stop();
});

it('returns thrown context error as a valid graphql result', async () => {
const nodeEnv = process.env.NODE_ENV;
delete process.env.NODE_ENV;
const typeDefs = gql`
type Query {
hello: String
}
`;
const resolvers = {
Query: {
hello: (parent, args, context) => {
throw Error('never get here');
},
},
};
const server = new ApolloServerBase({
typeDefs,
resolvers,
context: ({ req }) => {
throw new AuthenticationError('valid result');
},
});
const httpServer = createHttpServer(server);
server.use({
getHttp: () => httpServer,
path: '/',
});

const { url: uri } = await server.listen();
const apolloFetch = createApolloFetch({ uri });

const result = await apolloFetch({ query: '{hello}' });
expect(result.errors.length).to.equal(1);
expect(result.data).not.to.exist;

const e = result.errors[0];
expect(e.message).to.contain('valid result');
expect(e.extensions).to.exist;
expect(e.extensions.code).to.equal('UNAUTHENTICATED');
expect(e.extensions.exception.stacktrace).to.exist;

process.env.NODE_ENV = nodeEnv;
await server.stop();
});

it('propogates error codes in production', async () => {
const nodeEnv = process.env.NODE_ENV;
process.env.NODE_ENV = 'production';

const server = new ApolloServerBase({
typeDefs: gql`
type Query {
error: String
}
`,
resolvers: {
Query: {
error: () => {
throw new AuthenticationError('we the best music');
},
},
},
});
const httpServer = createHttpServer(server);

server.use({
getHttp: () => httpServer,
path: '/graphql',
});
const { url: uri } = await server.listen();
const apolloFetch = createApolloFetch({ uri });

const result = await apolloFetch({ query: `{error}` });
expect(result.data).to.exist;
expect(result.data).to.deep.equal({ error: null });

expect(result.errors, 'errors should exist').to.exist;
expect(result.errors.length).to.equal(1);
expect(result.errors[0].extensions.code).to.equal('UNAUTHENTICATED');
expect(result.errors[0].extensions.exception).not.to.exist;

process.env.NODE_ENV = nodeEnv;
await server.stop();
});

it('propogates error codes with null response in production', async () => {
const nodeEnv = process.env.NODE_ENV;
process.env.NODE_ENV = 'production';

const server = new ApolloServerBase({
typeDefs: gql`
type Query {
error: String!
}
`,
resolvers: {
Query: {
error: () => {
throw new AuthenticationError('we the best music');
},
},
},
});
const httpServer = createHttpServer(server);

server.use({
getHttp: () => httpServer,
path: '/graphql',
});
const { url: uri } = await server.listen();
const apolloFetch = createApolloFetch({ uri });

const result = await apolloFetch({ query: `{error}` });
expect(result.data).null;

expect(result.errors, 'errors should exist').to.exist;
expect(result.errors.length).to.equal(1);
expect(result.errors[0].extensions.code).to.equal('UNAUTHENTICATED');
expect(result.errors[0].extensions.exception).not.to.exist;

process.env.NODE_ENV = nodeEnv;
await server.stop();
});
});

describe('engine', () => {
Expand Down
16 changes: 11 additions & 5 deletions packages/apollo-server-core/src/ApolloServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,11 +330,17 @@ export class ApolloServerBase<Request = RequestInit> {
request(request: Request) {
let context: Context = this.context ? this.context : { request };

//Defer context resolution to inside of runQuery
context =
typeof this.context === 'function'
? () => this.context({ req: request })
: context;
try {
context =
typeof this.context === 'function'
? this.context({ req: request })
: context;
} catch (error) {
//Defer context error resolution to inside of runQuery
context = () => {
throw error;
};
}

return {
schema: this.schema,
Expand Down
25 changes: 1 addition & 24 deletions packages/apollo-server-core/src/errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,7 @@ import { expect } from 'chai';
import { stub, spy } from 'sinon';
import 'mocha';

import {
GraphQLSchema,
GraphQLObjectType,
GraphQLString,
GraphQLInt,
GraphQLError,
} from 'graphql';
import { GraphQLError } from 'graphql';

import {
ApolloError,
Expand All @@ -20,22 +14,6 @@ import {
SyntaxError,
} from './errors';

const queryType = new GraphQLObjectType({
name: 'QueryType',
fields: {
testString: {
type: GraphQLString,
resolve() {
return 'it works';
},
},
},
});

const schema = new GraphQLSchema({
query: queryType,
});

describe('Errors', () => {
describe('ApolloError', () => {
const message = 'message';
Expand Down Expand Up @@ -172,7 +150,6 @@ describe('Errors', () => {
});
});
it('provides a forbidden error', () => {
debugger;
verifyError(new ForbiddenError(message), {
code: 'FORBIDDEN',
errorClass: ForbiddenError,
Expand Down
32 changes: 30 additions & 2 deletions packages/apollo-server-core/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { LogStep, LogAction, LogMessage, LogFunction } from './logging';

export class ApolloError extends Error implements GraphQLError {
public extensions: Record<string, any>;
readonly name = 'ApolloError';
readonly name;
readonly locations;
readonly path;
readonly source;
Expand All @@ -29,6 +29,11 @@ export class ApolloError extends Error implements GraphQLError {
});
}

//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 };
Expand Down Expand Up @@ -207,7 +212,30 @@ export function formatApolloErrors(
}
const { formatter, debug, logFunction } = options;

const enrichedErrors = errors.map(error => enrichError(error, debug));
const flattenedErrors = [];
errors.forEach(error => {
// Errors that occur in graphql-tools can contain an errors array that contains the errors thrown in a merged schema
// https://github.com/apollographql/graphql-tools/blob/3d53986ca/src/stitching/errors.ts#L104-L107
//
// They are are wrapped in an extra GraphQL error
// https://github.com/apollographql/graphql-tools/blob/3d53986ca/src/stitching/errors.ts#L109-L113
// which calls:
// https://github.com/graphql/graphql-js/blob/0a30b62964/src/error/locatedError.js#L18-L37
if (Array.isArray((error as any).errors)) {
(error as any).errors.forEach(e => flattenedErrors.push(e));
} else if (
(error as any).originalError &&
Array.isArray((error as any).originalError.errors)
) {
(error as any).originalError.errors.forEach(e => flattenedErrors.push(e));
} else {
flattenedErrors.push(error);
}
});

const enrichedErrors = flattenedErrors.map(error =>
enrichError(error, debug),
);

if (!formatter) {
return enrichedErrors;
Expand Down
Loading

0 comments on commit 38c8713

Please sign in to comment.