Skip to content

Commit

Permalink
feat(handler): Error formatter function (#78)
Browse files Browse the repository at this point in the history
  • Loading branch information
enisdenjo committed Apr 14, 2023
1 parent cabf8a9 commit c0eaeb4
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 9 deletions.
13 changes: 13 additions & 0 deletions docs/interfaces/handler.HandlerOptions.md
Expand Up @@ -18,6 +18,7 @@

- [context](handler.HandlerOptions.md#context)
- [execute](handler.HandlerOptions.md#execute)
- [formatError](handler.HandlerOptions.md#formaterror)
- [getOperationAST](handler.HandlerOptions.md#getoperationast)
- [onOperation](handler.HandlerOptions.md#onoperation)
- [onSubscribe](handler.HandlerOptions.md#onsubscribe)
Expand Down Expand Up @@ -62,6 +63,18 @@ used to execute the query and mutation operations.

___

### formatError

`Optional` **formatError**: [`FormatError`](../modules/handler.md#formaterror)

Format handled errors to your satisfaction. Either GraphQL errors
or safe request processing errors are meant by "handleded errors".

If multiple errors have occured, all of them will be mapped using
this formatter.

___

### getOperationAST

`Optional` **getOperationAST**: (`documentAST`: `DocumentNode`, `operationName?`: `Maybe`<`string`\>) => `Maybe`<`OperationDefinitionNode`\>
Expand Down
26 changes: 25 additions & 1 deletion docs/modules/handler.md
Expand Up @@ -13,6 +13,7 @@
### Type Aliases

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

___

### FormatError

Ƭ **FormatError**: (`err`: `Readonly`<`GraphQLError` \| `Error`\>) => `GraphQLError` \| `Error`

#### Type declaration

▸ (`err`): `GraphQLError` \| `Error`

The (GraphQL) error formatter function.

##### Parameters

| Name | Type |
| :------ | :------ |
| `err` | `Readonly`<`GraphQLError` \| `Error`\> |

##### Returns

`GraphQLError` \| `Error`

___

### Handler

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

### makeResponse

**makeResponse**(`resultOrErrors`, `acceptedMediaType`): [`Response`](handler.md#response)
**makeResponse**(`resultOrErrors`, `acceptedMediaType`, `formatError`): [`Response`](handler.md#response)

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

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

#### Returns

Expand Down
23 changes: 23 additions & 0 deletions src/__tests__/handler.ts
Expand Up @@ -188,3 +188,26 @@ it('should print plain errors in detail', async () => {
`"{"errors":[{"message":"Unparsable JSON body"}]}"`,
);
});

it('should format errors using the formatter', async () => {
const formatErrorFn = jest.fn((_err) => new Error('Formatted'));
const server = startTServer({
formatError: formatErrorFn,
});
const url = new URL(server.url);
url.searchParams.set('query', '{ idontexist }');
const res = await fetch(url.toString());
expect(res.json()).resolves.toMatchInlineSnapshot(`
{
"errors": [
{
"message": "Formatted",
},
],
}
`);
expect(formatErrorFn).toBeCalledTimes(1);
expect(formatErrorFn.mock.lastCall?.[0]).toMatchInlineSnapshot(
`[GraphQLError: Cannot query field "idontexist" on type "Query".]`,
);
});
49 changes: 41 additions & 8 deletions src/handler.ts
Expand Up @@ -156,6 +156,15 @@ export type OperationContext =
| undefined
| null;

/**
* The (GraphQL) error formatter function.
*
* @category Server
*/
export type FormatError = (
err: Readonly<GraphQLError | Error>,
) => GraphQLError | Error;

/** @category Server */
export type OperationArgs<Context extends OperationContext = undefined> =
ExecutionArgs & { contextValue?: Context };
Expand Down Expand Up @@ -313,6 +322,14 @@ export interface HandlerOptions<
| ExecutionResult
| Response
| void;
/**
* Format handled errors to your satisfaction. Either GraphQL errors
* or safe request processing errors are meant by "handleded errors".
*
* If multiple errors have occured, all of them will be mapped using
* this formatter.
*/
formatError?: FormatError;
}

/**
Expand Down Expand Up @@ -402,6 +419,7 @@ export function createHandler<
rootValue,
onSubscribe,
onOperation,
formatError = (err) => err,
} = options;

return async function handler(req) {
Expand Down Expand Up @@ -525,7 +543,7 @@ export function createHandler<
// request parameters are checked and now complete
params = partParams as RequestParams;
} catch (err) {
return makeResponse(err, acceptedMediaType);
return makeResponse(err, acceptedMediaType, formatError);
}

let args: OperationArgs<Context>;
Expand All @@ -535,7 +553,7 @@ export function createHandler<
isExecutionResult(maybeResErrsOrArgs) ||
areGraphQLErrors(maybeResErrsOrArgs)
)
return makeResponse(maybeResErrsOrArgs, acceptedMediaType);
return makeResponse(maybeResErrsOrArgs, acceptedMediaType, formatError);
else if (maybeResErrsOrArgs) args = maybeResErrsOrArgs;
else {
if (!schema) throw new Error('The GraphQL schema is not provided');
Expand All @@ -546,7 +564,7 @@ export function createHandler<
try {
document = parse(query);
} catch (err) {
return makeResponse(err, acceptedMediaType);
return makeResponse(err, acceptedMediaType, formatError);
}

const resOrContext =
Expand Down Expand Up @@ -582,7 +600,7 @@ export function createHandler<
}
const validationErrs = validate(args.schema, args.document, rules);
if (validationErrs.length) {
return makeResponse(validationErrs, acceptedMediaType);
return makeResponse(validationErrs, acceptedMediaType, formatError);
}
}

Expand All @@ -595,13 +613,15 @@ export function createHandler<
return makeResponse(
new GraphQLError('Unable to detect operation AST'),
acceptedMediaType,
formatError,
);
}

if (operation === 'subscription') {
return makeResponse(
new GraphQLError('Subscriptions are not supported'),
acceptedMediaType,
formatError,
);
}

Expand Down Expand Up @@ -642,10 +662,11 @@ export function createHandler<
return makeResponse(
new GraphQLError('Subscriptions are not supported'),
acceptedMediaType,
formatError,
);
}

return makeResponse(result, acceptedMediaType);
return makeResponse(result, acceptedMediaType, formatError);
};
}

Expand Down Expand Up @@ -720,14 +741,18 @@ export function makeResponse(
| Readonly<GraphQLError>
| Readonly<Error>,
acceptedMediaType: AcceptableMediaType,
formatError: FormatError,
): Response {
if (
resultOrErrors instanceof Error &&
// because GraphQLError extends the Error class
!isGraphQLError(resultOrErrors)
) {
return [
JSON.stringify({ errors: [resultOrErrors] }, jsonErrorReplacer),
JSON.stringify(
{ errors: [formatError(resultOrErrors)] },
jsonErrorReplacer,
),
{
status: 400,
statusText: 'Bad Request',
Expand All @@ -744,7 +769,7 @@ export function makeResponse(
: null;
if (errors) {
return [
JSON.stringify({ errors }, jsonErrorReplacer),
JSON.stringify({ errors: errors.map(formatError) }, jsonErrorReplacer),
{
...(acceptedMediaType === 'application/json'
? {
Expand All @@ -766,7 +791,15 @@ export function makeResponse(
}

return [
JSON.stringify(resultOrErrors, jsonErrorReplacer),
JSON.stringify(
'errors' in resultOrErrors && resultOrErrors.errors
? {
...resultOrErrors,
errors: resultOrErrors.errors.map(formatError),
}
: resultOrErrors,
jsonErrorReplacer,
),
{
status: 200,
statusText: 'OK',
Expand Down

0 comments on commit c0eaeb4

Please sign in to comment.