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

Commit

Permalink
catch errors from AsyncIterable
Browse files Browse the repository at this point in the history
  • Loading branch information
robrichard committed Nov 11, 2020
1 parent c84ac09 commit aa62e24
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 28 deletions.
80 changes: 80 additions & 0 deletions src/__tests__/http-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2356,6 +2356,86 @@ function runTests(server: Server) {
'{"errors":[{"message":"I did something wrong"}]}',
);
});

it('catches first error thrown from custom execute function that returns an AsyncIterable', async () => {
const app = server();

app.get(
urlString(),
graphqlHTTP(() => ({
schema: TestSchema,
customExecuteFn() {
return {
[Symbol.asyncIterator]: () => ({
next: () => Promise.reject(new Error('I did something wrong')),
}),
};
},
})),
);

const response = await app.request().get(urlString({ query: '{test}' }));
expect(response.status).to.equal(400);
expect(response.text).to.equal(
'{"errors":[{"message":"I did something wrong"}]}',
);
});

it('catches subsequent errors thrown from custom execute function that returns an AsyncIterable', async () => {
const app = server();

app.get(
urlString(),
graphqlHTTP(() => ({
schema: TestSchema,
async *customExecuteFn() {
await new Promise((r) => {
setTimeout(r, 1);
});
yield {
data: {
test2: 'Modification',
},
hasNext: true,
};
throw new Error('I did something wrong');
},
})),
);

const response = await app
.request()
.get(urlString({ query: '{test}' }))
.parse((res, cb) => {
res.on('data', (data) => {
res.text = `${res.text || ''}${data.toString('utf8') as string}`;
});
res.on('end', (err) => {
cb(err, null);
});
});

expect(response.status).to.equal(200);
expect(response.text).to.equal(
[
'',
'---',
'Content-Type: application/json; charset=utf-8',
'Content-Length: 48',
'',
'{"data":{"test2":"Modification"},"hasNext":true}',
'',
'---',
'Content-Type: application/json; charset=utf-8',
'Content-Length: 64',
'',
'{"errors":[{"message":"I did something wrong"}],"hasNext":false}',
'',
'-----',
'',
].join('\r\n'),
);
});
});

describe('Custom parse function', () => {
Expand Down
20 changes: 20 additions & 0 deletions src/__tests__/isAsyncIterable-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { expect } from 'chai';
import { describe, it } from 'mocha';

import { isAsyncIterable } from '../isAsyncIterable';

describe('isAsyncIterable', () => {
it('returns false for null', () => {
expect(isAsyncIterable(null)).to.equal(false);
});
it('returns false for non-object', () => {
expect(isAsyncIterable(1)).to.equal(false);
});
it('returns true for async generator function', () => {
// istanbul ignore next: test function
// eslint-disable-next-line @typescript-eslint/no-empty-function
const myGen = async function* () {};
const result = myGen();
expect(isAsyncIterable(result)).to.equal(true);
});
});
80 changes: 53 additions & 27 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ export function graphqlHTTP(options: Options): Middleware {
// https://graphql.github.io/graphql-spec/#sec-Response-Format
if (optionsData.extensions) {
extensionsFn = (payload: AsyncExecutionResult) => {
/* istanbul ignore next condition not reachable, required for flow */
/* istanbul ignore else: condition not reachable, required for typescript */
if (optionsData.extensions) {
return optionsData.extensions({
document: documentAST,
Expand All @@ -285,6 +285,8 @@ export function graphqlHTTP(options: Options): Middleware {
context,
});
}
/* istanbul ignore next: condition not reachable, required for typescript */
return undefined;
};
}

Expand Down Expand Up @@ -362,23 +364,25 @@ export function graphqlHTTP(options: Options): Middleware {
fieldResolver,
typeResolver,
});

if (isAsyncIterable(executeResult)) {
// Get first payload from AsyncIterator. http status will reflect status
// of this payload.
const asyncIterator = getAsyncIterator<ExecutionResult>(
executeResult,
);
const { value } = await asyncIterator.next();
result = value;
} else {
result = executeResult;
}
} catch (contextError: unknown) {
// Return 400: Bad Request if any execution context errors exist.
throw httpError(400, 'GraphQL execution context error.', {
graphqlErrors: [contextError],
});
}

if (isAsyncIterable(executeResult)) {
// Get first payload from AsyncIterator. http status will reflect status
// of this payload.
const asyncIterator = getAsyncIterator<ExecutionResult>(executeResult);
const { value } = await asyncIterator.next();
result = value;
} else {
result = executeResult;
}

if (extensionsFn) {
const extensions = await extensionsFn(result);

Expand Down Expand Up @@ -412,9 +416,12 @@ export function graphqlHTTP(options: Options): Middleware {
undefined,
error,
);
result = { data: undefined, errors: [graphqlError] };
executeResult = result = { data: undefined, errors: [graphqlError] };
} else {
result = { data: undefined, errors: error.graphqlErrors };
executeResult = result = {
data: undefined,
errors: error.graphqlErrors,
};
}
}

Expand All @@ -436,22 +443,41 @@ export function graphqlHTTP(options: Options): Middleware {
if (isAsyncIterable(executeResult)) {
response.setHeader('Content-Type', 'multipart/mixed; boundary="-"');
sendPartialResponse(pretty, response, formattedResult);
for await (let payload of executeResult) {
// Collect and apply any metadata extensions if a function was provided.
// https://graphql.github.io/graphql-spec/#sec-Response-Format
if (extensionsFn) {
const extensions = await extensionsFn(payload);

if (extensions != null) {
payload = { ...payload, extensions };
try {
for await (let payload of executeResult) {
// Collect and apply any metadata extensions if a function was provided.
// https://graphql.github.io/graphql-spec/#sec-Response-Format
if (extensionsFn) {
const extensions = await extensionsFn(payload);

if (extensions != null) {
payload = { ...payload, extensions };
}
}
const formattedPayload: FormattedExecutionPatchResult = {
// first payload is already consumed, all subsequent payloads typed as ExecutionPatchResult
...(payload as ExecutionPatchResult),
errors: payload.errors?.map(formatErrorFn),
};
sendPartialResponse(pretty, response, formattedPayload);
}
const formattedPayload: FormattedExecutionPatchResult = {
// first payload is already consumed, all subsequent payloads typed as ExecutionPatchResult
...(payload as ExecutionPatchResult),
errors: payload.errors?.map(formatErrorFn),
};
sendPartialResponse(pretty, response, formattedPayload);
} catch (rawError: unknown) {
/* istanbul ignore next: Thrown by underlying library. */
const error =
rawError instanceof Error ? rawError : new Error(String(rawError));
const graphqlError = new GraphQLError(
error.message,
undefined,
undefined,
undefined,
undefined,
error,
);
sendPartialResponse(pretty, response, {
data: undefined,
errors: [formatErrorFn(graphqlError)],
hasNext: false,
});
}
response.write('\r\n-----\r\n');
response.end();
Expand Down
1 change: 0 additions & 1 deletion src/isAsyncIterable.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
export function isAsyncIterable<T>(
maybeAsyncIterable: any,
// eslint-disable-next-line no-undef
): maybeAsyncIterable is AsyncIterable<T> {
if (maybeAsyncIterable == null || typeof maybeAsyncIterable !== 'object') {
return false;
Expand Down

0 comments on commit aa62e24

Please sign in to comment.