Skip to content
This repository was archived by the owner on Mar 20, 2023. It is now read-only.

Commit aa62e24

Browse files
committed
catch errors from AsyncIterable
1 parent c84ac09 commit aa62e24

File tree

4 files changed

+153
-28
lines changed

4 files changed

+153
-28
lines changed

src/__tests__/http-test.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2356,6 +2356,86 @@ function runTests(server: Server) {
23562356
'{"errors":[{"message":"I did something wrong"}]}',
23572357
);
23582358
});
2359+
2360+
it('catches first error thrown from custom execute function that returns an AsyncIterable', async () => {
2361+
const app = server();
2362+
2363+
app.get(
2364+
urlString(),
2365+
graphqlHTTP(() => ({
2366+
schema: TestSchema,
2367+
customExecuteFn() {
2368+
return {
2369+
[Symbol.asyncIterator]: () => ({
2370+
next: () => Promise.reject(new Error('I did something wrong')),
2371+
}),
2372+
};
2373+
},
2374+
})),
2375+
);
2376+
2377+
const response = await app.request().get(urlString({ query: '{test}' }));
2378+
expect(response.status).to.equal(400);
2379+
expect(response.text).to.equal(
2380+
'{"errors":[{"message":"I did something wrong"}]}',
2381+
);
2382+
});
2383+
2384+
it('catches subsequent errors thrown from custom execute function that returns an AsyncIterable', async () => {
2385+
const app = server();
2386+
2387+
app.get(
2388+
urlString(),
2389+
graphqlHTTP(() => ({
2390+
schema: TestSchema,
2391+
async *customExecuteFn() {
2392+
await new Promise((r) => {
2393+
setTimeout(r, 1);
2394+
});
2395+
yield {
2396+
data: {
2397+
test2: 'Modification',
2398+
},
2399+
hasNext: true,
2400+
};
2401+
throw new Error('I did something wrong');
2402+
},
2403+
})),
2404+
);
2405+
2406+
const response = await app
2407+
.request()
2408+
.get(urlString({ query: '{test}' }))
2409+
.parse((res, cb) => {
2410+
res.on('data', (data) => {
2411+
res.text = `${res.text || ''}${data.toString('utf8') as string}`;
2412+
});
2413+
res.on('end', (err) => {
2414+
cb(err, null);
2415+
});
2416+
});
2417+
2418+
expect(response.status).to.equal(200);
2419+
expect(response.text).to.equal(
2420+
[
2421+
'',
2422+
'---',
2423+
'Content-Type: application/json; charset=utf-8',
2424+
'Content-Length: 48',
2425+
'',
2426+
'{"data":{"test2":"Modification"},"hasNext":true}',
2427+
'',
2428+
'---',
2429+
'Content-Type: application/json; charset=utf-8',
2430+
'Content-Length: 64',
2431+
'',
2432+
'{"errors":[{"message":"I did something wrong"}],"hasNext":false}',
2433+
'',
2434+
'-----',
2435+
'',
2436+
].join('\r\n'),
2437+
);
2438+
});
23592439
});
23602440

23612441
describe('Custom parse function', () => {

src/__tests__/isAsyncIterable-test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { expect } from 'chai';
2+
import { describe, it } from 'mocha';
3+
4+
import { isAsyncIterable } from '../isAsyncIterable';
5+
6+
describe('isAsyncIterable', () => {
7+
it('returns false for null', () => {
8+
expect(isAsyncIterable(null)).to.equal(false);
9+
});
10+
it('returns false for non-object', () => {
11+
expect(isAsyncIterable(1)).to.equal(false);
12+
});
13+
it('returns true for async generator function', () => {
14+
// istanbul ignore next: test function
15+
// eslint-disable-next-line @typescript-eslint/no-empty-function
16+
const myGen = async function* () {};
17+
const result = myGen();
18+
expect(isAsyncIterable(result)).to.equal(true);
19+
});
20+
});

src/index.ts

Lines changed: 53 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ export function graphqlHTTP(options: Options): Middleware {
275275
// https://graphql.github.io/graphql-spec/#sec-Response-Format
276276
if (optionsData.extensions) {
277277
extensionsFn = (payload: AsyncExecutionResult) => {
278-
/* istanbul ignore next condition not reachable, required for flow */
278+
/* istanbul ignore else: condition not reachable, required for typescript */
279279
if (optionsData.extensions) {
280280
return optionsData.extensions({
281281
document: documentAST,
@@ -285,6 +285,8 @@ export function graphqlHTTP(options: Options): Middleware {
285285
context,
286286
});
287287
}
288+
/* istanbul ignore next: condition not reachable, required for typescript */
289+
return undefined;
288290
};
289291
}
290292

@@ -362,23 +364,25 @@ export function graphqlHTTP(options: Options): Middleware {
362364
fieldResolver,
363365
typeResolver,
364366
});
367+
368+
if (isAsyncIterable(executeResult)) {
369+
// Get first payload from AsyncIterator. http status will reflect status
370+
// of this payload.
371+
const asyncIterator = getAsyncIterator<ExecutionResult>(
372+
executeResult,
373+
);
374+
const { value } = await asyncIterator.next();
375+
result = value;
376+
} else {
377+
result = executeResult;
378+
}
365379
} catch (contextError: unknown) {
366380
// Return 400: Bad Request if any execution context errors exist.
367381
throw httpError(400, 'GraphQL execution context error.', {
368382
graphqlErrors: [contextError],
369383
});
370384
}
371385

372-
if (isAsyncIterable(executeResult)) {
373-
// Get first payload from AsyncIterator. http status will reflect status
374-
// of this payload.
375-
const asyncIterator = getAsyncIterator<ExecutionResult>(executeResult);
376-
const { value } = await asyncIterator.next();
377-
result = value;
378-
} else {
379-
result = executeResult;
380-
}
381-
382386
if (extensionsFn) {
383387
const extensions = await extensionsFn(result);
384388

@@ -412,9 +416,12 @@ export function graphqlHTTP(options: Options): Middleware {
412416
undefined,
413417
error,
414418
);
415-
result = { data: undefined, errors: [graphqlError] };
419+
executeResult = result = { data: undefined, errors: [graphqlError] };
416420
} else {
417-
result = { data: undefined, errors: error.graphqlErrors };
421+
executeResult = result = {
422+
data: undefined,
423+
errors: error.graphqlErrors,
424+
};
418425
}
419426
}
420427

@@ -436,22 +443,41 @@ export function graphqlHTTP(options: Options): Middleware {
436443
if (isAsyncIterable(executeResult)) {
437444
response.setHeader('Content-Type', 'multipart/mixed; boundary="-"');
438445
sendPartialResponse(pretty, response, formattedResult);
439-
for await (let payload of executeResult) {
440-
// Collect and apply any metadata extensions if a function was provided.
441-
// https://graphql.github.io/graphql-spec/#sec-Response-Format
442-
if (extensionsFn) {
443-
const extensions = await extensionsFn(payload);
444-
445-
if (extensions != null) {
446-
payload = { ...payload, extensions };
446+
try {
447+
for await (let payload of executeResult) {
448+
// Collect and apply any metadata extensions if a function was provided.
449+
// https://graphql.github.io/graphql-spec/#sec-Response-Format
450+
if (extensionsFn) {
451+
const extensions = await extensionsFn(payload);
452+
453+
if (extensions != null) {
454+
payload = { ...payload, extensions };
455+
}
447456
}
457+
const formattedPayload: FormattedExecutionPatchResult = {
458+
// first payload is already consumed, all subsequent payloads typed as ExecutionPatchResult
459+
...(payload as ExecutionPatchResult),
460+
errors: payload.errors?.map(formatErrorFn),
461+
};
462+
sendPartialResponse(pretty, response, formattedPayload);
448463
}
449-
const formattedPayload: FormattedExecutionPatchResult = {
450-
// first payload is already consumed, all subsequent payloads typed as ExecutionPatchResult
451-
...(payload as ExecutionPatchResult),
452-
errors: payload.errors?.map(formatErrorFn),
453-
};
454-
sendPartialResponse(pretty, response, formattedPayload);
464+
} catch (rawError: unknown) {
465+
/* istanbul ignore next: Thrown by underlying library. */
466+
const error =
467+
rawError instanceof Error ? rawError : new Error(String(rawError));
468+
const graphqlError = new GraphQLError(
469+
error.message,
470+
undefined,
471+
undefined,
472+
undefined,
473+
undefined,
474+
error,
475+
);
476+
sendPartialResponse(pretty, response, {
477+
data: undefined,
478+
errors: [formatErrorFn(graphqlError)],
479+
hasNext: false,
480+
});
455481
}
456482
response.write('\r\n-----\r\n');
457483
response.end();

src/isAsyncIterable.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
export function isAsyncIterable<T>(
22
maybeAsyncIterable: any,
3-
// eslint-disable-next-line no-undef
43
): maybeAsyncIterable is AsyncIterable<T> {
54
if (maybeAsyncIterable == null || typeof maybeAsyncIterable !== 'object') {
65
return false;

0 commit comments

Comments
 (0)