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

Errors Lifecycle: user extensions > engine reporting > formatError #1272

Merged
merged 9 commits into from
Jun 29, 2018
16 changes: 15 additions & 1 deletion packages/apollo-server-core/src/ApolloServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ import {
FileUploadOptions,
} from './types';

import { FormatErrorExtension } from './formatters';

import { gql } from './index';

const NoIntrospection = (context: ValidationContext) => ({
Expand Down Expand Up @@ -180,11 +182,23 @@ export class ApolloServerBase {
// or cacheControl.
this.extensions = [];

// Error formatting should happen after the engine reporting agent, so that
// engine gets the unmasked errors if necessary
if (this.requestOptions.formatError) {
this.extensions.push(
() =>
new FormatErrorExtension(
this.requestOptions.formatError,
this.requestOptions.debug,
),
);
}

if (engine || (engine !== false && process.env.ENGINE_API_KEY)) {
this.engineReportingAgent = new EngineReportingAgent(
engine === true ? {} : engine,
);
// Let's keep this extension first so it wraps everything.
// Let's keep this extension second so it wraps everything, except error formatting
this.extensions.push(() => this.engineReportingAgent.newExtension());
}

Expand Down
29 changes: 29 additions & 0 deletions packages/apollo-server-core/src/formatters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { GraphQLExtension, GraphQLResponse } from 'graphql-extensions';
import { formatApolloErrors } from 'apollo-server-errors';

export class FormatErrorExtension extends GraphQLExtension {
private formatError: Function;
private debug: boolean;

public constructor(formatError: Function, debug: boolean = false) {
super();
this.formatError = formatError;
this.debug = debug;
}

public willSendResponse(o: {
graphqlResponse: GraphQLResponse;
}): void | { graphqlResponse: GraphQLResponse } {
if (o.graphqlResponse.errors) {
return {
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmmm, I'm wondering if modifying the GraphQLResponse passed in instead of returning a new one wouldn't be clearer as an API for extensions.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's totally an option, though I feel that this API is more self documenting. It's still possible to modify the graphqlResponse in-place too

graphqlResponse: {
...o.graphqlResponse,
errors: formatApolloErrors(o.graphqlResponse.errors, {
formatter: this.formatError,
debug: this.debug,
}),
},
};
}
}
}
53 changes: 38 additions & 15 deletions packages/apollo-server-core/src/runHttpQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,18 +69,23 @@ export class HttpQueryError extends Error {
}
}

function throwHttpGraphQLError(
statusCode,
errors: Array<Error>,
optionsObject,
/**
* If optionsObject is specified, then the errors array will be formatted
*/
function throwHttpGraphQLError<E extends Error>(
statusCode: number,
errors: Array<E>,
optionsObject?: Partial<GraphQLOptions>,
) {
throw new HttpQueryError(
statusCode,
prettyJSONStringify({
errors: formatApolloErrors(errors, {
debug: optionsObject.debug,
formatter: optionsObject.formatError,
}),
errors: optionsObject
? formatApolloErrors(errors, {
debug: optionsObject.debug,
formatter: optionsObject.formatError,
})
: errors,
}),
true,
{
Expand Down Expand Up @@ -302,7 +307,17 @@ export async function runHttpQuery(
context = await context();
} catch (e) {
e.message = `Context creation failed: ${e.message}`;
throwHttpGraphQLError(500, [e], optionsObject);
// For errors that are not internal, such as authentication, we
// should provide a 400 response
if (
e.extensions &&
e.extensions.code &&
e.extensions.code !== 'INTERNAL_SERVER_ERROR'
) {
throwHttpGraphQLError(400, [e], optionsObject);
} else {
throwHttpGraphQLError(500, [e], optionsObject);
}
}
} else {
// Always clone the context if it's not a function, because that preserves
Expand Down Expand Up @@ -402,16 +417,23 @@ export async function runHttpQuery(
throw e;
}

// This error will be uncaught, so we need to wrap it and treat it as an
// internal server error
return {
errors: formatApolloErrors([e], {
formatter: optionsObject.formatError,
debug: optionsObject.debug,
}),
errors: formatApolloErrors([e], optionsObject),
};
}
}) as Array<Promise<ExecutionResult & { extensions?: Record<string, any> }>>;

const responses = await Promise.all(requests);
let responses;
try {
responses = await Promise.all(requests);
} catch (e) {
if (e.name === 'HttpQueryError') {
throw e;
}
throwHttpGraphQLError(500, [e], optionsObject);
}

const responseInit: ApolloServerHttpResponse = {
headers: {
Expand Down Expand Up @@ -449,7 +471,8 @@ export async function runHttpQuery(
// This code is run on parse/validation errors and any other error that
// doesn't reach GraphQL execution
if (graphqlResponse.errors && typeof graphqlResponse.data === 'undefined') {
throwHttpGraphQLError(400, graphqlResponse.errors as any, optionsObject);
// don't include optionsObject, since the errors have already been formatted
throwHttpGraphQLError(400, graphqlResponse.errors as any);
}
const stringified = prettyJSONStringify(graphqlResponse);

Expand Down
7 changes: 2 additions & 5 deletions packages/apollo-server-core/src/runQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,6 @@ function doRunQuery(options: QueryOptions): Promise<GraphQLResponse> {
}),
],
{
formatter: options.formatError,
debug,
},
);
Expand Down Expand Up @@ -199,7 +198,6 @@ function doRunQuery(options: QueryOptions): Promise<GraphQLResponse> {
fromGraphQLError(err, { errorClass: ValidationError }),
),
{
formatter: options.formatError,
debug,
},
);
Expand Down Expand Up @@ -250,7 +248,6 @@ function doRunQuery(options: QueryOptions): Promise<GraphQLResponse> {

if (result.errors) {
response.errors = formatApolloErrors([...result.errors], {
formatter: options.formatError,
debug,
});
}
Expand All @@ -277,8 +274,8 @@ function doRunQuery(options: QueryOptions): Promise<GraphQLResponse> {
throw err;
})
.then(graphqlResponse => {
extensionStack.willSendResponse({ graphqlResponse });
const response = extensionStack.willSendResponse({ graphqlResponse });
requestDidEnd();
return graphqlResponse;
return response.graphqlResponse;
});
}
68 changes: 0 additions & 68 deletions packages/apollo-server-express/src/apolloServerHttp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -417,74 +417,6 @@ describe(`GraphQL-HTTP (apolloServer) tests for ${version} express`, () => {
expect(response.status).to.equal(400);
});

it('allows for custom error formatting to sanitize', async () => {
const app = express();

app.use('/graphql', bodyParser.json());
app.use(
'/graphql',
graphqlExpress({
schema: TestSchema,
formatError(error) {
return { message: 'Custom error format: ' + error.message };
},
}),
);

const response = await request(app)
.post('/graphql')
.send({
query: '{thrower}',
});

expect(response.status).to.equal(200);
expect(JSON.parse(response.text)).to.deep.equal({
data: null,
errors: [
{
message: 'Custom error format: Throws!',
},
],
});
});

it('allows for custom error formatting to elaborate', async () => {
const app = express();

app.use('/graphql', bodyParser.json());
app.use(
'/graphql',
graphqlExpress({
schema: TestSchema,
formatError(error) {
return {
message: error.message,
locations: error.locations,
stack: 'Stack trace',
};
},
}),
);

const response = await request(app)
.post('/graphql')
.send({
query: '{thrower}',
});

expect(response.status).to.equal(200);
expect(JSON.parse(response.text)).to.deep.equal({
data: null,
errors: [
{
message: 'Throws!',
locations: [{ line: 1, column: 2 }],
stack: 'Stack trace',
},
],
});
});

it('handles unsupported HTTP methods', async () => {
const app = express();

Expand Down
1 change: 0 additions & 1 deletion packages/apollo-server-hapi/src/ApolloServer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,6 @@ describe('apollo-server-hapi', () => {

const apolloFetch = createApolloFetch({ uri }).useAfter(
(response, next) => {
console.log(response.response.headers);
expect(
response.response.headers.get('access-control-expose-headers'),
).to.deep.equal(
Expand Down
4 changes: 4 additions & 0 deletions packages/apollo-server-integration-testsuite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,15 @@
"apollo-server-core": "^2.0.0-rc.5"
},
"devDependencies": {
"@types/body-parser": "1.17.0",
"apollo-engine-reporting-protobuf": "0.0.0-beta.7",
"apollo-fetch": "^0.7.0",
"apollo-link": "^1.2.2",
"apollo-link-http": "^1.5.4",
"apollo-link-persisted-queries": "^0.2.1",
"apollo-server-env": "^2.0.0-rc.3",
"body-parser": "^1.18.3",
"graphql-extensions": "^0.1.0-beta.15",
"graphql-subscriptions": "^0.5.8",
"graphql-tag": "^2.9.2",
"js-sha256": "^0.9.0",
Expand Down
Loading