Skip to content

Commit c0eaeb4

Browse files
authored
feat(handler): Error formatter function (#78)
1 parent cabf8a9 commit c0eaeb4

File tree

4 files changed

+102
-9
lines changed

4 files changed

+102
-9
lines changed

docs/interfaces/handler.HandlerOptions.md

+13
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
- [context](handler.HandlerOptions.md#context)
2020
- [execute](handler.HandlerOptions.md#execute)
21+
- [formatError](handler.HandlerOptions.md#formaterror)
2122
- [getOperationAST](handler.HandlerOptions.md#getoperationast)
2223
- [onOperation](handler.HandlerOptions.md#onoperation)
2324
- [onSubscribe](handler.HandlerOptions.md#onsubscribe)
@@ -62,6 +63,18 @@ used to execute the query and mutation operations.
6263

6364
___
6465

66+
### formatError
67+
68+
`Optional` **formatError**: [`FormatError`](../modules/handler.md#formaterror)
69+
70+
Format handled errors to your satisfaction. Either GraphQL errors
71+
or safe request processing errors are meant by "handleded errors".
72+
73+
If multiple errors have occured, all of them will be mapped using
74+
this formatter.
75+
76+
___
77+
6578
### getOperationAST
6679

6780
`Optional` **getOperationAST**: (`documentAST`: `DocumentNode`, `operationName?`: `Maybe`<`string`\>) => `Maybe`<`OperationDefinitionNode`\>

docs/modules/handler.md

+25-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
### Type Aliases
1414

1515
- [AcceptableMediaType](handler.md#acceptablemediatype)
16+
- [FormatError](handler.md#formaterror)
1617
- [Handler](handler.md#handler)
1718
- [OperationArgs](handler.md#operationargs)
1819
- [OperationContext](handler.md#operationcontext)
@@ -38,6 +39,28 @@ Request's Media-Type that the server accepts.
3839

3940
___
4041

42+
### FormatError
43+
44+
Ƭ **FormatError**: (`err`: `Readonly`<`GraphQLError` \| `Error`\>) => `GraphQLError` \| `Error`
45+
46+
#### Type declaration
47+
48+
▸ (`err`): `GraphQLError` \| `Error`
49+
50+
The (GraphQL) error formatter function.
51+
52+
##### Parameters
53+
54+
| Name | Type |
55+
| :------ | :------ |
56+
| `err` | `Readonly`<`GraphQLError` \| `Error`\> |
57+
58+
##### Returns
59+
60+
`GraphQLError` \| `Error`
61+
62+
___
63+
4164
### Handler
4265

4366
Ƭ **Handler**<`RequestRaw`, `RequestContext`\>: (`req`: [`Request`](../interfaces/handler.Request.md)<`RequestRaw`, `RequestContext`\>) => `Promise`<[`Response`](handler.md#response)\>
@@ -246,7 +269,7 @@ ___
246269

247270
### makeResponse
248271

249-
**makeResponse**(`resultOrErrors`, `acceptedMediaType`): [`Response`](handler.md#response)
272+
**makeResponse**(`resultOrErrors`, `acceptedMediaType`, `formatError`): [`Response`](handler.md#response)
250273

251274
Creates an appropriate GraphQL over HTTP response following the provided arguments.
252275

@@ -264,6 +287,7 @@ error will be present in the `ExecutionResult` style.
264287
| :------ | :------ |
265288
| `resultOrErrors` | readonly `GraphQLError`[] \| `Readonly`<`ExecutionResult`<`ObjMap`<`unknown`\>, `ObjMap`<`unknown`\>\>\> \| `Readonly`<`GraphQLError`\> \| `Readonly`<`Error`\> |
266289
| `acceptedMediaType` | [`AcceptableMediaType`](handler.md#acceptablemediatype) |
290+
| `formatError` | [`FormatError`](handler.md#formaterror) |
267291

268292
#### Returns
269293

src/__tests__/handler.ts

+23
Original file line numberDiff line numberDiff line change
@@ -188,3 +188,26 @@ it('should print plain errors in detail', async () => {
188188
`"{"errors":[{"message":"Unparsable JSON body"}]}"`,
189189
);
190190
});
191+
192+
it('should format errors using the formatter', async () => {
193+
const formatErrorFn = jest.fn((_err) => new Error('Formatted'));
194+
const server = startTServer({
195+
formatError: formatErrorFn,
196+
});
197+
const url = new URL(server.url);
198+
url.searchParams.set('query', '{ idontexist }');
199+
const res = await fetch(url.toString());
200+
expect(res.json()).resolves.toMatchInlineSnapshot(`
201+
{
202+
"errors": [
203+
{
204+
"message": "Formatted",
205+
},
206+
],
207+
}
208+
`);
209+
expect(formatErrorFn).toBeCalledTimes(1);
210+
expect(formatErrorFn.mock.lastCall?.[0]).toMatchInlineSnapshot(
211+
`[GraphQLError: Cannot query field "idontexist" on type "Query".]`,
212+
);
213+
});

src/handler.ts

+41-8
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,15 @@ export type OperationContext =
156156
| undefined
157157
| null;
158158

159+
/**
160+
* The (GraphQL) error formatter function.
161+
*
162+
* @category Server
163+
*/
164+
export type FormatError = (
165+
err: Readonly<GraphQLError | Error>,
166+
) => GraphQLError | Error;
167+
159168
/** @category Server */
160169
export type OperationArgs<Context extends OperationContext = undefined> =
161170
ExecutionArgs & { contextValue?: Context };
@@ -313,6 +322,14 @@ export interface HandlerOptions<
313322
| ExecutionResult
314323
| Response
315324
| void;
325+
/**
326+
* Format handled errors to your satisfaction. Either GraphQL errors
327+
* or safe request processing errors are meant by "handleded errors".
328+
*
329+
* If multiple errors have occured, all of them will be mapped using
330+
* this formatter.
331+
*/
332+
formatError?: FormatError;
316333
}
317334

318335
/**
@@ -402,6 +419,7 @@ export function createHandler<
402419
rootValue,
403420
onSubscribe,
404421
onOperation,
422+
formatError = (err) => err,
405423
} = options;
406424

407425
return async function handler(req) {
@@ -525,7 +543,7 @@ export function createHandler<
525543
// request parameters are checked and now complete
526544
params = partParams as RequestParams;
527545
} catch (err) {
528-
return makeResponse(err, acceptedMediaType);
546+
return makeResponse(err, acceptedMediaType, formatError);
529547
}
530548

531549
let args: OperationArgs<Context>;
@@ -535,7 +553,7 @@ export function createHandler<
535553
isExecutionResult(maybeResErrsOrArgs) ||
536554
areGraphQLErrors(maybeResErrsOrArgs)
537555
)
538-
return makeResponse(maybeResErrsOrArgs, acceptedMediaType);
556+
return makeResponse(maybeResErrsOrArgs, acceptedMediaType, formatError);
539557
else if (maybeResErrsOrArgs) args = maybeResErrsOrArgs;
540558
else {
541559
if (!schema) throw new Error('The GraphQL schema is not provided');
@@ -546,7 +564,7 @@ export function createHandler<
546564
try {
547565
document = parse(query);
548566
} catch (err) {
549-
return makeResponse(err, acceptedMediaType);
567+
return makeResponse(err, acceptedMediaType, formatError);
550568
}
551569

552570
const resOrContext =
@@ -582,7 +600,7 @@ export function createHandler<
582600
}
583601
const validationErrs = validate(args.schema, args.document, rules);
584602
if (validationErrs.length) {
585-
return makeResponse(validationErrs, acceptedMediaType);
603+
return makeResponse(validationErrs, acceptedMediaType, formatError);
586604
}
587605
}
588606

@@ -595,13 +613,15 @@ export function createHandler<
595613
return makeResponse(
596614
new GraphQLError('Unable to detect operation AST'),
597615
acceptedMediaType,
616+
formatError,
598617
);
599618
}
600619

601620
if (operation === 'subscription') {
602621
return makeResponse(
603622
new GraphQLError('Subscriptions are not supported'),
604623
acceptedMediaType,
624+
formatError,
605625
);
606626
}
607627

@@ -642,10 +662,11 @@ export function createHandler<
642662
return makeResponse(
643663
new GraphQLError('Subscriptions are not supported'),
644664
acceptedMediaType,
665+
formatError,
645666
);
646667
}
647668

648-
return makeResponse(result, acceptedMediaType);
669+
return makeResponse(result, acceptedMediaType, formatError);
649670
};
650671
}
651672

@@ -720,14 +741,18 @@ export function makeResponse(
720741
| Readonly<GraphQLError>
721742
| Readonly<Error>,
722743
acceptedMediaType: AcceptableMediaType,
744+
formatError: FormatError,
723745
): Response {
724746
if (
725747
resultOrErrors instanceof Error &&
726748
// because GraphQLError extends the Error class
727749
!isGraphQLError(resultOrErrors)
728750
) {
729751
return [
730-
JSON.stringify({ errors: [resultOrErrors] }, jsonErrorReplacer),
752+
JSON.stringify(
753+
{ errors: [formatError(resultOrErrors)] },
754+
jsonErrorReplacer,
755+
),
731756
{
732757
status: 400,
733758
statusText: 'Bad Request',
@@ -744,7 +769,7 @@ export function makeResponse(
744769
: null;
745770
if (errors) {
746771
return [
747-
JSON.stringify({ errors }, jsonErrorReplacer),
772+
JSON.stringify({ errors: errors.map(formatError) }, jsonErrorReplacer),
748773
{
749774
...(acceptedMediaType === 'application/json'
750775
? {
@@ -766,7 +791,15 @@ export function makeResponse(
766791
}
767792

768793
return [
769-
JSON.stringify(resultOrErrors, jsonErrorReplacer),
794+
JSON.stringify(
795+
'errors' in resultOrErrors && resultOrErrors.errors
796+
? {
797+
...resultOrErrors,
798+
errors: resultOrErrors.errors.map(formatError),
799+
}
800+
: resultOrErrors,
801+
jsonErrorReplacer,
802+
),
770803
{
771804
status: 200,
772805
statusText: 'OK',

0 commit comments

Comments
 (0)