Skip to content

Commit ebd91ad

Browse files
committed
feat(handler): onSubscribe option can return an array of GraphQL errors
1 parent 3306cae commit ebd91ad

File tree

5 files changed

+66
-8
lines changed

5 files changed

+66
-8
lines changed

docs/interfaces/HandlerOptions.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,11 +123,11 @@ ___
123123

124124
### onSubscribe
125125

126-
`Optional` **onSubscribe**: (`req`: [`Request`](Request.md)<`RawRequest`\>, `params`: [`RequestParams`](RequestParams.md)) => `void` \| [`Response`](../README.md#response) \| `ExecutionArgs` \| `Promise`<`void` \| [`Response`](../README.md#response) \| `ExecutionArgs`\>
126+
`Optional` **onSubscribe**: (`req`: [`Request`](Request.md)<`RawRequest`\>, `params`: [`RequestParams`](RequestParams.md)) => `void` \| `GraphQLError`[] \| [`Response`](../README.md#response) \| `ExecutionArgs` \| `Promise`<`void` \| `GraphQLError`[] \| [`Response`](../README.md#response) \| `ExecutionArgs`\>
127127

128128
#### Type declaration
129129

130-
▸ (`req`, `params`): `void` \| [`Response`](../README.md#response) \| `ExecutionArgs` \| `Promise`<`void` \| [`Response`](../README.md#response) \| `ExecutionArgs`\>
130+
▸ (`req`, `params`): `void` \| `GraphQLError`[] \| [`Response`](../README.md#response) \| `ExecutionArgs` \| `Promise`<`void` \| `GraphQLError`[] \| [`Response`](../README.md#response) \| `ExecutionArgs`\>
131131

132132
The subscribe callback executed right after processing the request
133133
before proceeding with the GraphQL operation execution.
@@ -136,6 +136,9 @@ If you return `ExecutionArgs` from the callback, it will be used instead of
136136
trying to build one internally. In this case, you are responsible for providing
137137
a ready set of arguments which will be directly plugged in the operation execution.
138138

139+
If you return an array of `GraphQLError` from the callback, they will be reported
140+
to the client while complying with the spec.
141+
139142
Omitting the fields `contextValue` from the returned `ExecutionArgs` will use the
140143
provided `context` option, if available.
141144

@@ -156,7 +159,7 @@ further execution.
156159

157160
##### Returns
158161

159-
`void` \| [`Response`](../README.md#response) \| `ExecutionArgs` \| `Promise`<`void` \| [`Response`](../README.md#response) \| `ExecutionArgs`\>
162+
`void` \| `GraphQLError`[] \| [`Response`](../README.md#response) \| `ExecutionArgs` \| `Promise`<`void` \| `GraphQLError`[] \| [`Response`](../README.md#response) \| `ExecutionArgs`\>
160163

161164
___
162165

docs/interfaces/ResponseInit.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,4 @@ ___
2929

3030
### statusText
3131

32-
`Optional` `Readonly` **statusText**: `string`
32+
`Readonly` **statusText**: `string`

src/__tests__/handler.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { GraphQLError } from 'graphql';
12
import fetch from 'node-fetch';
23
import { startTServer } from './utils/tserver';
34

@@ -16,3 +17,17 @@ it.each(['schema', 'context', 'onSubscribe', 'onOperation'])(
1617
expect(res.status).toBe(418);
1718
},
1819
);
20+
21+
it('should report graphql errors returned from onSubscribe', async () => {
22+
const server = startTServer({
23+
onSubscribe: () => {
24+
return [new GraphQLError('Woah!')];
25+
},
26+
});
27+
28+
const url = new URL(server.url);
29+
url.searchParams.set('query', '{ __typename }');
30+
const res = await fetch(url.toString());
31+
expect(res.status).toBe(400);
32+
expect(res.json()).resolves.toEqual({ errors: [{ message: 'Woah!' }] });
33+
});

src/handler.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@ import {
1414
DocumentNode,
1515
getOperationAST as graphqlGetOperationAST,
1616
OperationTypeNode,
17+
GraphQLError,
1718
} from 'graphql';
1819
import { isResponse, Request, RequestParams, Response } from './common';
20+
import { areGraphQLErrors } from './utils';
1921

2022
/**
2123
* A concrete GraphQL execution context value type.
@@ -98,6 +100,9 @@ export interface HandlerOptions<RawRequest = unknown> {
98100
* trying to build one internally. In this case, you are responsible for providing
99101
* a ready set of arguments which will be directly plugged in the operation execution.
100102
*
103+
* If you return an array of `GraphQLError` from the callback, they will be reported
104+
* to the client while complying with the spec.
105+
*
101106
* Omitting the fields `contextValue` from the returned `ExecutionArgs` will use the
102107
* provided `context` option, if available.
103108
*
@@ -113,8 +118,9 @@ export interface HandlerOptions<RawRequest = unknown> {
113118
req: Request<RawRequest>,
114119
params: RequestParams,
115120
) =>
116-
| Promise<ExecutionArgs | Response | void>
121+
| Promise<ExecutionArgs | GraphQLError[] | Response | void>
117122
| ExecutionArgs
123+
| GraphQLError[]
118124
| Response
119125
| void;
120126
/**
@@ -379,9 +385,30 @@ export function createHandler<RawRequest = unknown>(
379385
}
380386

381387
let args: ExecutionArgs;
382-
const maybeResOrExecArgs = await onSubscribe?.(req, params);
383-
if (isResponse(maybeResOrExecArgs)) return maybeResOrExecArgs;
384-
else if (maybeResOrExecArgs) args = maybeResOrExecArgs;
388+
const maybeResErrsOrArgs = await onSubscribe?.(req, params);
389+
if (isResponse(maybeResErrsOrArgs)) return maybeResErrsOrArgs;
390+
else if (areGraphQLErrors(maybeResErrsOrArgs))
391+
return [
392+
JSON.stringify({ errors: maybeResErrsOrArgs }),
393+
{
394+
...(acceptedMediaType === 'application/json'
395+
? {
396+
status: 200,
397+
statusText: 'OK',
398+
}
399+
: {
400+
status: 400,
401+
statusText: 'Bad Request',
402+
}),
403+
headers: {
404+
'content-type':
405+
acceptedMediaType === 'application/json'
406+
? 'application/json; charset=utf-8'
407+
: 'application/graphql+json; charset=utf-8',
408+
},
409+
},
410+
];
411+
else if (maybeResErrsOrArgs) args = maybeResErrsOrArgs;
385412
else {
386413
if (!schema) throw new Error('The GraphQL schema is not provided');
387414

src/utils.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,20 @@
44
*
55
*/
66

7+
import type { GraphQLError } from 'graphql';
8+
79
/** @private */
810
export function isObject(val: unknown): val is Record<PropertyKey, unknown> {
911
return typeof val === 'object' && val !== null;
1012
}
13+
14+
/** @private */
15+
export function areGraphQLErrors(obj: unknown): obj is GraphQLError[] {
16+
return (
17+
Array.isArray(obj) &&
18+
// must be at least one error
19+
obj.length > 0 &&
20+
// error has at least a message
21+
obj.every((ob) => 'message' in ob)
22+
);
23+
}

0 commit comments

Comments
 (0)