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

Async format error #2081

Closed
wants to merge 4 commits into from
Closed
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
49 changes: 38 additions & 11 deletions packages/apollo-cache-control/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import {

import { GraphQLExtension, GraphQLResponse } from 'graphql-extensions';

function isPromise(x: any): x is Promise<any> {
return x && typeof x.then === 'function';
}

export interface CacheControlFormat {
version: 1;
hints: ({ path: (string | number)[] } & CacheHint)[];
Expand Down Expand Up @@ -136,18 +140,41 @@ export class CacheControlExtension<TContext = any>
];
}

public willSendResponse?(o: { graphqlResponse: GraphQLResponse }) {
if (this.options.calculateHttpHeaders && o.graphqlResponse.http) {
const overallCachePolicy = this.computeOverallCachePolicy();

if (overallCachePolicy) {
o.graphqlResponse.http.headers.set(
'Cache-Control',
`max-age=${
overallCachePolicy.maxAge
}, ${overallCachePolicy.scope.toLowerCase()}`,
);
public willSendResponse?(
o:
| { graphqlResponse: GraphQLResponse }
| Promise<{ graphqlResponse: GraphQLResponse }>,
) {
if (!isPromise(o)) {
if (this.options.calculateHttpHeaders && o.graphqlResponse.http) {
const overallCachePolicy = this.computeOverallCachePolicy();

if (overallCachePolicy) {
o.graphqlResponse.http.headers.set(
'Cache-Control',
`max-age=${
overallCachePolicy.maxAge
}, ${overallCachePolicy.scope.toLowerCase()}`,
);
}
}
return;
} else {
return o.then(p => {
if (this.options.calculateHttpHeaders && p.graphqlResponse.http) {
const overallCachePolicy = this.computeOverallCachePolicy();

if (overallCachePolicy) {
p.graphqlResponse.http.headers.set(
'Cache-Control',
`max-age=${
overallCachePolicy.maxAge
}, ${overallCachePolicy.scope.toLowerCase()}`,
);
}
}
return;
});
}
}

Expand Down
85 changes: 63 additions & 22 deletions packages/apollo-engine-reporting/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ import { EngineReportingOptions, GenerateClientInfo } from './agent';
import { defaultSignature } from './signature';
import { GraphQLRequestContext } from 'apollo-server-core/dist/requestPipelineAPI';

function isPromise(x: any): x is Promise<any> {
return x && typeof x.then === 'function';
}

const clientNameHeaderKey = 'apollographql-client-name';
const clientReferenceIdHeaderKey = 'apollographql-client-reference-id';
const clientVersionHeaderKey = 'apollographql-client-version';
Expand Down Expand Up @@ -275,31 +279,68 @@ export class EngineReportingExtension<TContext = any>
};
}

public willSendResponse(o: { graphqlResponse: GraphQLResponse }) {
const { errors } = o.graphqlResponse;
if (errors) {
errors.forEach((error: GraphQLError) => {
// By default, put errors on the root node.
let node = this.nodes.get('');
if (error.path) {
const specificNode = this.nodes.get(error.path.join('.'));
if (specificNode) {
node = specificNode;
public willSendResponse(
o:
| { graphqlResponse: GraphQLResponse }
| Promise<{ graphqlResponse: GraphQLResponse }>,
) {
if (!isPromise(o)) {
const { errors } = o.graphqlResponse;
if (errors) {
errors.forEach((error: GraphQLError) => {
// By default, put errors on the root node.
let node = this.nodes.get('');
if (error.path) {
const specificNode = this.nodes.get(error.path.join('.'));
if (specificNode) {
node = specificNode;
}
}
}

// Always send the trace errors, so that the UI acknowledges that there is an error.
const errorInfo = this.options.maskErrorDetails
? { message: '<masked>' }
: {
message: error.message,
location: (error.locations || []).map(
({ line, column }) => new Trace.Location({ line, column }),
),
json: JSON.stringify(error),
};
// Always send the trace errors, so that the UI acknowledges that there is an error.
const errorInfo = this.options.maskErrorDetails
? { message: '<masked>' }
: {
message: error.message,
location: (error.locations || []).map(
({ line, column }) => new Trace.Location({ line, column }),
),
json: JSON.stringify(error),
};

node!.error!.push(new Trace.Error(errorInfo));
node!.error!.push(new Trace.Error(errorInfo));
});
}
return;
} else {
return o.then(p => {
const { errors } = p.graphqlResponse;
if (errors) {
errors.forEach((error: GraphQLError) => {
// By default, put errors on the root node.
let node = this.nodes.get('');
if (error.path) {
const specificNode = this.nodes.get(error.path.join('.'));
if (specificNode) {
node = specificNode;
}
}

// Always send the trace errors, so that the UI acknowledges that there is an error.
const errorInfo = this.options.maskErrorDetails
? { message: '<masked>' }
: {
message: error.message,
location: (error.locations || []).map(
({ line, column }) => new Trace.Location({ line, column }),
),
json: JSON.stringify(error),
};

node!.error!.push(new Trace.Error(errorInfo));
});
}
return;
});
}
}
Expand Down
4 changes: 2 additions & 2 deletions packages/apollo-server-core/src/ApolloServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -438,10 +438,10 @@ export class ApolloServerBase {
? await this.context({ connection, payload: message.payload })
: context;
} catch (e) {
throw formatApolloErrors([e], {
throw (await formatApolloErrors([e], {
formatter: this.requestOptions.formatError,
debug: this.requestOptions.debug,
})[0];
}))[0];
}

return { ...connection, context };
Expand Down
62 changes: 54 additions & 8 deletions packages/apollo-server-core/src/formatters.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { GraphQLExtension, GraphQLResponse } from 'graphql-extensions';
import { formatApolloErrors } from 'apollo-server-errors';

function isPromise(x: any): x is Promise<any> {
return x && typeof x.then === 'function';
}

export class FormatErrorExtension<TContext = any> extends GraphQLExtension {
private formatError?: Function;
private debug: boolean;
Expand All @@ -11,21 +15,63 @@ export class FormatErrorExtension<TContext = any> extends GraphQLExtension {
this.debug = debug;
}

public willSendResponse(o: {
graphqlResponse: GraphQLResponse;
context: TContext;
}): void | { graphqlResponse: GraphQLResponse; context: TContext } {
if (o.graphqlResponse.errors) {
public willSendResponse(
o:
| Promise<{
graphqlResponse: GraphQLResponse;
context: TContext;
}>
| {
graphqlResponse: GraphQLResponse;
context: TContext;
},
):
| Promise<{ graphqlResponse: GraphQLResponse; context: TContext } | void>
| { graphqlResponse: GraphQLResponse; context: TContext }
| void {
if (isPromise(o)) {
return this.asyncWillSendResponse(o);
} else if (o.graphqlResponse.errors) {
let formattedErrors = formatApolloErrors(o.graphqlResponse.errors, {
formatter: this.formatError,
debug: this.debug,
});
if (isPromise(formattedErrors)) {
return this.asyncWillSendResponse(o);
} else if (o) {
return {
...o,
graphqlResponse: {
...o.graphqlResponse,
errors: formattedErrors,
},
};
}
}
return o;
}

public async asyncWillSendResponse(
o:
| Promise<{
graphqlResponse: GraphQLResponse;
context: TContext;
}>
| { graphqlResponse: GraphQLResponse; context: TContext },
): Promise<{ graphqlResponse: GraphQLResponse; context: TContext } | void> {
let p = await o;
if (p.graphqlResponse.errors) {
return {
...o,
...p,
graphqlResponse: {
...o.graphqlResponse,
errors: formatApolloErrors(o.graphqlResponse.errors, {
...p.graphqlResponse,
errors: await formatApolloErrors(p.graphqlResponse.errors, {
formatter: this.formatError,
debug: this.debug,
}),
},
};
}
return o;
}
}
4 changes: 2 additions & 2 deletions packages/apollo-server-core/src/requestPipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,15 +330,15 @@ export async function processGraphQLRequest<TContext>(
): Promise<GraphQLResponse> {
// We override errors, data, and extensions with the passed in response,
// but keep other properties (like http)
requestContext.response = extensionStack.willSendResponse({
requestContext.response = (await extensionStack.willSendResponse({
graphqlResponse: {
...requestContext.response,
errors: response.errors,
data: response.data,
extensions: response.extensions,
},
context: requestContext.context,
}).graphqlResponse;
})).graphqlResponse;
await dispatcher.invokeHookAsync(
'willSendResponse',
requestContext as WithRequired<typeof requestContext, 'response'>,
Expand Down
20 changes: 10 additions & 10 deletions packages/apollo-server-core/src/runHttpQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,16 +69,16 @@ export class HttpQueryError extends Error {
/**
* If options is specified, then the errors array will be formatted
*/
function throwHttpGraphQLError<E extends Error>(
async function throwHttpGraphQLError<E extends Error>(
statusCode: number,
errors: Array<E>,
options?: Pick<GraphQLOptions, 'debug' | 'formatError'>,
): never {
): Promise<never> | never {
throw new HttpQueryError(
statusCode,
prettyJSONStringify({
errors: options
? formatApolloErrors(errors, {
? await formatApolloErrors(errors, {
debug: options.debug,
formatter: options.formatError,
})
Expand Down Expand Up @@ -110,7 +110,7 @@ export async function runHttpQuery(
if (!debugDefault) {
e.warning = `To remove the stacktrace, set the NODE_ENV environment variable to production if the options creation can fail`;
}
return throwHttpGraphQLError(500, [e], { debug: debugDefault });
return await throwHttpGraphQLError(500, [e], { debug: debugDefault });
}
if (options.debug === undefined) {
options.debug = debugDefault;
Expand All @@ -135,9 +135,9 @@ export async function runHttpQuery(
e.extensions.code &&
e.extensions.code !== 'INTERNAL_SERVER_ERROR'
) {
return throwHttpGraphQLError(400, [e], options);
return await throwHttpGraphQLError(400, [e], options);
} else {
return throwHttpGraphQLError(500, [e], options);
return await throwHttpGraphQLError(500, [e], options);
}
}
}
Expand Down Expand Up @@ -265,7 +265,7 @@ export async function processHTTPRequest<TContext>(
// A batch can contain another query that returns data,
// so we don't error out the entire request with an HttpError
return {
errors: formatApolloErrors([error], options),
errors: await formatApolloErrors([error], options),
};
}
}),
Expand All @@ -284,7 +284,7 @@ export async function processHTTPRequest<TContext>(
// doesn't reach GraphQL execution
if (response.errors && typeof response.data === 'undefined') {
// don't include options, since the errors have already been formatted
return throwHttpGraphQLError(400, response.errors as any);
return await throwHttpGraphQLError(400, response.errors as any);
}

if (response.http) {
Expand All @@ -301,7 +301,7 @@ export async function processHTTPRequest<TContext>(
error instanceof PersistedQueryNotSupportedError ||
error instanceof PersistedQueryNotFoundError
) {
return throwHttpGraphQLError(200, [error], options);
return await throwHttpGraphQLError(200, [error], options);
} else {
throw error;
}
Expand All @@ -311,7 +311,7 @@ export async function processHTTPRequest<TContext>(
if (error instanceof HttpQueryError) {
throw error;
}
return throwHttpGraphQLError(500, [error], options);
return await throwHttpGraphQLError(500, [error], options);
}

responseInit.headers!['Content-Length'] = Buffer.byteLength(
Expand Down
Loading