Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions docs/interfaces/handler.HandlerOptions.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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