Skip to content

Commit d4a1362

Browse files
taionIvanGoncharov
authored andcommitted
Make error handling consistent in createSourceEventStream (#1467)
1 parent 6faa515 commit d4a1362

File tree

2 files changed

+137
-10
lines changed

2 files changed

+137
-10
lines changed

src/subscription/__tests__/subscribe-test.js

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import { expect } from 'chai';
44
import { describe, it } from 'mocha';
55
import EventEmitter from 'events';
66
import eventEmitterAsyncIterator from './eventEmitterAsyncIterator';
7-
import { subscribe } from '../subscribe';
7+
import { createSourceEventStream, subscribe } from '../subscribe';
8+
import { GraphQLError } from '../../error';
89
import { parse } from '../../language';
910
import {
1011
GraphQLSchema,
@@ -431,6 +432,58 @@ describe('Subscription Initialization Phase', () => {
431432
}
432433
});
433434

435+
it('resolves to an error for source event stream resolver errors', async () => {
436+
// Returning an error
437+
const subscriptionReturningErrorSchema = emailSchemaWithResolvers(() => {
438+
return new Error('test error');
439+
});
440+
await testReportsError(subscriptionReturningErrorSchema);
441+
442+
// Throwing an error
443+
const subscriptionThrowingErrorSchema = emailSchemaWithResolvers(() => {
444+
throw new Error('test error');
445+
});
446+
await testReportsError(subscriptionThrowingErrorSchema);
447+
448+
// Resolving to an error
449+
const subscriptionResolvingErrorSchema = emailSchemaWithResolvers(
450+
async () => {
451+
return new Error('test error');
452+
},
453+
);
454+
await testReportsError(subscriptionResolvingErrorSchema);
455+
456+
// Rejecting with an error
457+
const subscriptionRejectingErrorSchema = emailSchemaWithResolvers(
458+
async () => {
459+
throw new Error('test error');
460+
},
461+
);
462+
await testReportsError(subscriptionRejectingErrorSchema);
463+
464+
async function testReportsError(schema) {
465+
// Promise<AsyncIterable<ExecutionResult> | ExecutionResult>
466+
const result = await createSourceEventStream(
467+
schema,
468+
parse(`
469+
subscription {
470+
importantEmail
471+
}
472+
`),
473+
);
474+
475+
expect(result).to.deep.equal({
476+
errors: [
477+
{
478+
message: 'test error',
479+
locations: [{ line: 3, column: 13 }],
480+
path: ['importantEmail'],
481+
},
482+
],
483+
});
484+
}
485+
});
486+
434487
it('resolves to an error if variables were wrong type', async () => {
435488
// If we receive variables that cannot be coerced correctly, subscribe()
436489
// will resolve to an ExecutionResult that contains an informative error
@@ -937,4 +990,59 @@ describe('Subscription Publish Phase', () => {
937990
value: undefined,
938991
});
939992
});
993+
994+
it('should resolve GraphQL error from source event stream', async () => {
995+
const erroringEmailSchema = emailSchemaWithResolvers(
996+
async function*() {
997+
yield { email: { subject: 'Hello' } };
998+
throw new GraphQLError('test error');
999+
},
1000+
email => email,
1001+
);
1002+
1003+
const subscription = await subscribe(
1004+
erroringEmailSchema,
1005+
parse(`
1006+
subscription {
1007+
importantEmail {
1008+
email {
1009+
subject
1010+
}
1011+
}
1012+
}
1013+
`),
1014+
);
1015+
1016+
const payload1 = await subscription.next();
1017+
expect(payload1).to.deep.equal({
1018+
done: false,
1019+
value: {
1020+
data: {
1021+
importantEmail: {
1022+
email: {
1023+
subject: 'Hello',
1024+
},
1025+
},
1026+
},
1027+
},
1028+
});
1029+
1030+
const payload2 = await subscription.next();
1031+
expect(payload2).to.deep.equal({
1032+
done: false,
1033+
value: {
1034+
errors: [
1035+
{
1036+
message: 'test error',
1037+
},
1038+
],
1039+
},
1040+
});
1041+
1042+
const payload3 = await subscription.next();
1043+
expect(payload3).to.deep.equal({
1044+
done: true,
1045+
value: undefined,
1046+
});
1047+
});
9401048
});

src/subscription/subscribe.js

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,9 @@ export type SubscriptionArgs = {|
3838
* Implements the "Subscribe" algorithm described in the GraphQL specification.
3939
*
4040
* Returns a Promise which resolves to either an AsyncIterator (if successful)
41-
* or an ExecutionResult (client error). The promise will be rejected if a
42-
* server error occurs.
41+
* or an ExecutionResult (error). The promise will be rejected if the schema or
42+
* other arguments to this function are invalid, or if the resolved event stream
43+
* is not an async iterable.
4344
*
4445
* If the client-provided arguments to this function do not result in a
4546
* compliant subscription, a GraphQL Response (ExecutionResult) with
@@ -160,19 +161,28 @@ function subscribeImpl(
160161
reportGraphQLError,
161162
)
162163
: ((resultOrStream: any): ExecutionResult),
163-
reportGraphQLError,
164164
);
165165
}
166166

167167
/**
168168
* Implements the "CreateSourceEventStream" algorithm described in the
169169
* GraphQL specification, resolving the subscription source event stream.
170170
*
171-
* Returns a Promise<AsyncIterable>.
171+
* Returns a Promise which resolves to either an AsyncIterable (if successful)
172+
* or an ExecutionResult (error). The promise will be rejected if the schema or
173+
* other arguments to this function are invalid, or if the resolved event stream
174+
* is not an async iterable.
172175
*
173-
* If the client-provided invalid arguments, the source stream could not be
174-
* created, or the resolver did not return an AsyncIterable, this function will
175-
* will throw an error, which should be caught and handled by the caller.
176+
* If the client-provided arguments to this function do not result in a
177+
* compliant subscription, a GraphQL Response (ExecutionResult) with
178+
* descriptive errors and no data will be returned.
179+
*
180+
* If the the source stream could not be created due to faulty subscription
181+
* resolver logic or underlying systems, the promise will resolve to a single
182+
* ExecutionResult containing `errors` and no `data`.
183+
*
184+
* If the operation succeeded, the promise resolves to the AsyncIterable for the
185+
* event stream returned by the resolver.
176186
*
177187
* A Source Event Stream represents a sequence of events, each of which triggers
178188
* a GraphQL execution for that event.
@@ -259,7 +269,11 @@ export function createSourceEventStream(
259269
return Promise.resolve(result).then(eventStream => {
260270
// If eventStream is an Error, rethrow a located error.
261271
if (eventStream instanceof Error) {
262-
throw locatedError(eventStream, fieldNodes, responsePathAsArray(path));
272+
return {
273+
errors: [
274+
locatedError(eventStream, fieldNodes, responsePathAsArray(path)),
275+
],
276+
};
263277
}
264278

265279
// Assert field returned an event stream, otherwise yield an error.
@@ -273,6 +287,11 @@ export function createSourceEventStream(
273287
);
274288
});
275289
} catch (error) {
276-
return Promise.reject(error);
290+
// As with reportGraphQLError above, if the error is a GraphQLError, report
291+
// it as an ExecutionResult; otherwise treat it as a system-class error and
292+
// re-throw it.
293+
return error instanceof GraphQLError
294+
? Promise.resolve({ errors: [error] })
295+
: Promise.reject(error);
277296
}
278297
}

0 commit comments

Comments
 (0)