Skip to content

Commit e9a0d6e

Browse files
authored
feat: allow stringifyResult to return a Promise<string> (#7803)
* allow `stringifyResult` to return a `Promise<string>` * call `stringifyResult` in previously missed error response cases as well Users who implemented the `stringifyResult` hook can now expect error responses to be formatted with the hook as well. Please take care when updating to this version to ensure this is the desired behavior, or implement the desired behavior accordingly in your `stringifyResult` hook. This was considered a non-breaking change as we consider that it was an oversight in the original PR that introduced `stringifyResult` hook.
1 parent 9bd7748 commit e9a0d6e

File tree

5 files changed

+125
-20
lines changed

5 files changed

+125
-20
lines changed

.changeset/thirty-hairs-change.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@apollo/server": minor
3+
---
4+
5+
allow `stringifyResult` to return a `Promise<string>`
6+
7+
Users who implemented the `stringifyResult` hook can now expect error responses to be formatted with the hook as well. Please take care when updating to this version to ensure this is the desired behavior, or implement the desired behavior accordingly in your `stringifyResult` hook. This was considered a non-breaking change as we consider that it was an oversight in the original PR that introduced `stringifyResult` hook.

packages/server/src/ApolloServer.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,9 @@ export interface ApolloServerInternals<TContext extends BaseContext> {
180180
// flip default behavior.
181181
status400ForVariableCoercionErrors?: boolean;
182182
__testing_incrementalExecutionResults?: GraphQLExperimentalIncrementalExecutionResults;
183-
stringifyResult: (value: FormattedExecutionResult) => string;
183+
stringifyResult: (
184+
value: FormattedExecutionResult,
185+
) => string | Promise<string>;
184186
}
185187

186188
function defaultLogger(): Logger {
@@ -1015,7 +1017,7 @@ export class ApolloServer<in out TContext extends BaseContext = BaseContext> {
10151017
// This is typically either the masked error from when background startup
10161018
// failed, or related to invoking this function before startup or
10171019
// during/after shutdown (due to lack of draining).
1018-
return this.errorResponse(error, httpGraphQLRequest);
1020+
return await this.errorResponse(error, httpGraphQLRequest);
10191021
}
10201022

10211023
if (
@@ -1031,7 +1033,7 @@ export class ApolloServer<in out TContext extends BaseContext = BaseContext> {
10311033
} catch (maybeError: unknown) {
10321034
const error = ensureError(maybeError);
10331035
this.logger.error(`Landing page \`html\` function threw: ${error}`);
1034-
return this.errorResponse(error, httpGraphQLRequest);
1036+
return await this.errorResponse(error, httpGraphQLRequest);
10351037
}
10361038
}
10371039

@@ -1076,7 +1078,7 @@ export class ApolloServer<in out TContext extends BaseContext = BaseContext> {
10761078
// If some random function threw, add a helpful prefix when converting
10771079
// to GraphQLError. If it was already a GraphQLError, trust that the
10781080
// message was chosen thoughtfully and leave off the prefix.
1079-
return this.errorResponse(
1081+
return await this.errorResponse(
10801082
ensureGraphQLError(error, 'Context creation failed: '),
10811083
httpGraphQLRequest,
10821084
);
@@ -1108,14 +1110,14 @@ export class ApolloServer<in out TContext extends BaseContext = BaseContext> {
11081110
);
11091111
}
11101112
}
1111-
return this.errorResponse(maybeError, httpGraphQLRequest);
1113+
return await this.errorResponse(maybeError, httpGraphQLRequest);
11121114
}
11131115
}
11141116

1115-
private errorResponse(
1117+
private async errorResponse(
11161118
error: unknown,
11171119
requestHead: HTTPGraphQLHead,
1118-
): HTTPGraphQLResponse {
1120+
): Promise<HTTPGraphQLResponse> {
11191121
const { formattedErrors, httpFromErrors } = normalizeAndFormatErrors(
11201122
[error],
11211123
{
@@ -1144,7 +1146,7 @@ export class ApolloServer<in out TContext extends BaseContext = BaseContext> {
11441146
]),
11451147
body: {
11461148
kind: 'complete',
1147-
string: prettyJSONStringify({
1149+
string: await this.internals.stringifyResult({
11481150
errors: formattedErrors,
11491151
}),
11501152
},

packages/server/src/__tests__/ApolloServer.test.ts

Lines changed: 104 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
1-
import { ApolloServer, HeaderMap } from '..';
2-
import type { ApolloServerOptions } from '..';
1+
import type { GatewayInterface } from '@apollo/server-gateway-interface';
2+
import { makeExecutableSchema } from '@graphql-tools/schema';
3+
import { describe, expect, it, jest } from '@jest/globals';
4+
import assert from 'assert';
35
import {
46
FormattedExecutionResult,
57
GraphQLError,
68
GraphQLSchema,
7-
parse,
89
TypedQueryDocumentNode,
10+
parse,
911
} from 'graphql';
12+
import gql from 'graphql-tag';
13+
import type { ApolloServerOptions } from '..';
14+
import { ApolloServer, HeaderMap } from '..';
1015
import type { ApolloServerPlugin, BaseContext } from '../externalTypes';
16+
import type { GraphQLResponseBody } from '../externalTypes/graphql';
1117
import { ApolloServerPluginCacheControlDisabled } from '../plugin/disabled/index.js';
1218
import { ApolloServerPluginUsageReporting } from '../plugin/usageReporting/index.js';
13-
import { makeExecutableSchema } from '@graphql-tools/schema';
1419
import { mockLogger } from './mockLogger.js';
15-
import gql from 'graphql-tag';
16-
import type { GatewayInterface } from '@apollo/server-gateway-interface';
17-
import { jest, describe, it, expect } from '@jest/globals';
18-
import type { GraphQLResponseBody } from '../externalTypes/graphql';
19-
import assert from 'assert';
2020

2121
const typeDefs = gql`
2222
type Query {
@@ -176,6 +176,100 @@ describe('ApolloServer construction', () => {
176176
`);
177177
await server.stop();
178178
});
179+
180+
it('async stringifyResult', async () => {
181+
const server = new ApolloServer({
182+
typeDefs,
183+
resolvers,
184+
stringifyResult: async (value: FormattedExecutionResult) => {
185+
let result = await Promise.resolve(
186+
JSON.stringify(value, null, 10000),
187+
);
188+
result = result.replace('world', 'stringifyResults works!'); // replace text with something custom
189+
return result;
190+
},
191+
});
192+
193+
await server.start();
194+
195+
const request = {
196+
httpGraphQLRequest: {
197+
method: 'POST',
198+
headers: new HeaderMap([['content-type', 'application-json']]),
199+
body: { query: '{ hello }' },
200+
search: '',
201+
},
202+
context: async () => ({}),
203+
};
204+
205+
const { body } = await server.executeHTTPGraphQLRequest(request);
206+
assert(body.kind === 'complete');
207+
expect(body.string).toMatchInlineSnapshot(`
208+
"{
209+
"data": {
210+
"hello": "stringifyResults works!"
211+
}
212+
}"
213+
`);
214+
await server.stop();
215+
});
216+
217+
it('throws the custom parsed error from stringifyResult', async () => {
218+
const server = new ApolloServer({
219+
typeDefs,
220+
resolvers,
221+
stringifyResult: (_: FormattedExecutionResult) => {
222+
throw new Error('A custom synchronous error');
223+
},
224+
});
225+
226+
await server.start();
227+
228+
const request = {
229+
httpGraphQLRequest: {
230+
method: 'POST',
231+
headers: new HeaderMap([['content-type', 'application-json']]),
232+
body: { query: '{ error }' },
233+
search: '',
234+
},
235+
context: async () => ({}),
236+
};
237+
238+
await expect(
239+
server.executeHTTPGraphQLRequest(request),
240+
).rejects.toThrowErrorMatchingInlineSnapshot(
241+
`"A custom synchronous error"`,
242+
);
243+
244+
await server.stop();
245+
});
246+
247+
it('throws the custom parsed error from async stringifyResult', async () => {
248+
const server = new ApolloServer({
249+
typeDefs,
250+
resolvers,
251+
stringifyResult: async (_: FormattedExecutionResult) =>
252+
Promise.reject('A custom asynchronous error'),
253+
});
254+
255+
await server.start();
256+
257+
const request = {
258+
httpGraphQLRequest: {
259+
method: 'POST',
260+
headers: new HeaderMap([['content-type', 'application-json']]),
261+
body: { query: '{ error }' },
262+
search: '',
263+
},
264+
context: async () => ({}),
265+
};
266+
267+
await expect(
268+
server.executeHTTPGraphQLRequest(request),
269+
).rejects.toMatchInlineSnapshot(`"A custom asynchronous error"`);
270+
271+
await server.stop();
272+
});
179273
});
180274

181275
it('throws when an API key is not a valid header value', () => {
@@ -501,7 +595,7 @@ describe('ApolloServer executeOperation', () => {
501595

502596
const { body, http } = await server.executeOperation({
503597
query: `#graphql
504-
query NeedsArg($arg: CompoundInput!) { needsCompoundArg(aCompound: $arg) }
598+
query NeedsArg($arg: CompoundInput!) { needsCompoundArg(aCompound: $arg) }
505599
`,
506600
// @ts-expect-error for `null` case
507601
variables,

packages/server/src/externalTypes/constructor.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,9 @@ interface ApolloServerOptionsBase<TContext extends BaseContext> {
8989
includeStacktraceInErrorResponses?: boolean;
9090
logger?: Logger;
9191
allowBatchedHttpRequests?: boolean;
92-
stringifyResult?: (value: FormattedExecutionResult) => string;
92+
stringifyResult?: (
93+
value: FormattedExecutionResult,
94+
) => string | Promise<string>;
9395
introspection?: boolean;
9496
plugins?: ApolloServerPlugin<TContext>[];
9597
persistedQueries?: PersistedQueryOptions | false;

packages/server/src/runHttpQuery.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ export async function runHttpQuery<TContext extends BaseContext>({
260260
...graphQLResponse.http,
261261
body: {
262262
kind: 'complete',
263-
string: internals.stringifyResult(
263+
string: await internals.stringifyResult(
264264
orderExecutionResultFields(graphQLResponse.body.singleResult),
265265
),
266266
},

0 commit comments

Comments
 (0)