feat!: improve afterError hook to accept array of functions, change to object args (#8389)

Changes the `afterError` hook structure, adds tests / more docs.
Ensures that the `req.responseHeaders` property is respected in the
error handler.

**Breaking**
`afterError` now accepts an array of functions instead of a single
function:
```diff
- afterError: () => {...}
+ afterError: [() => {...}]
```

The args are changed to accept an object with the following properties:
| Argument | Description |
| ------------------- |
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
| **`error`** | The error that occurred. |
| **`context`** | Custom context passed between Hooks. [More
details](./context). |
| **`graphqlResult`** | The GraphQL result object, available if the hook
is executed within a GraphQL context. |
| **`req`** | The
[Request](https://developer.mozilla.org/en-US/docs/Web/API/Request)
object containing the currently authenticated `user` |
| **`collection`** | The [Collection](../configuration/collections) in
which this Hook is running against. This will be `undefined` if the hook
is executed from a non-collection endpoint or GraphQL. |
| **`result`** | The formatted error result object, available if the
hook is executed from a REST context. |
This commit is contained in:
Sasha
2024-09-24 20:29:53 +03:00
committed by GitHub
parent 6da4f06205
commit 28ea0c59e8
12 changed files with 238 additions and 77 deletions

View File

@@ -186,7 +186,6 @@ export interface PayloadLockedDocument {
relationTo: 'users';
value: string | User;
} | null);
editedAt?: string | null;
globalSlug?: string | null;
user: {
relationTo: 'users';

View File

@@ -2,7 +2,7 @@ import { fileURLToPath } from 'node:url'
import path from 'path'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
import type { CollectionConfig } from 'payload'
import { APIError, type CollectionConfig } from 'payload'
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
import { devUser } from '../credentials.js'
@@ -288,6 +288,14 @@ export default buildConfigWithDefaults({
method: 'get',
path: '/internal-error-here',
},
{
handler: () => {
// Throwing an internal error with potentially sensitive data
throw new APIError('Connected to the Pentagon. Secret data: ******')
},
method: 'get',
path: '/api-error-here',
},
],
onInit: async (payload) => {
await payload.create({

View File

@@ -1,6 +1,8 @@
import type { Payload, SanitizedCollectionConfig } from 'payload'
import { randomBytes, randomUUID } from 'crypto'
import path from 'path'
import { NotFound, type Payload } from 'payload'
import { APIError, NotFound } from 'payload'
import { fileURLToPath } from 'url'
import type { NextRESTClient } from '../helpers/NextRESTClient.js'
@@ -1533,6 +1535,75 @@ describe('collections-rest', () => {
expect(Array.isArray(result.errors)).toEqual(true)
expect(result.errors[0].message).toStrictEqual('Something went wrong.')
})
it('should execute afterError hook on root level and modify result/status', async () => {
let err: unknown
let errResult: any
payload.config.hooks.afterError = [
({ error, result }) => {
err = error
errResult = result
return { status: 400, response: { modified: true } }
},
]
const response = await restClient.GET(`/api-error-here`)
expect(response.status).toBe(400)
expect(err).toBeInstanceOf(APIError)
expect(errResult).toStrictEqual({
errors: [
{
message: 'Something went wrong.',
},
],
})
const result = await response.json()
expect(result.modified).toBe(true)
payload.config.hooks.afterError = []
})
it('should execute afterError hook on collection level and modify result', async () => {
let err: unknown
let errResult: any
let collection: SanitizedCollectionConfig
payload.collections.posts.config.hooks.afterError = [
({ error, result, collection: incomingCollection }) => {
err = error
errResult = result
collection = incomingCollection
return { response: { modified: true } }
},
]
const post = await createPost({})
const response = await restClient.GET(
`/${slug}/${typeof post.id === 'number' ? 1000 : randomUUID()}`,
)
expect(response.status).toBe(404)
expect(collection.slug).toBe(slug)
expect(err).toBeInstanceOf(NotFound)
expect(errResult).toStrictEqual({
errors: [
{
message: 'Not Found',
},
],
})
const result = await response.json()
expect(result.modified).toBe(true)
payload.collections.posts.config.hooks.afterError = []
})
})
describe('Local', () => {

View File

@@ -45,14 +45,14 @@ export const HooksConfig: Promise<SanitizedConfig> = buildConfigWithDefaults({
},
],
hooks: {
afterError: () => console.log('Running afterError hook'),
afterError: [() => console.log('Running afterError hook')],
},
onInit: async (payload) => {
await seedHooksUsers(payload)
await payload.create({
collection: hooksSlug,
data: {
check: 'update',
check: true,
fieldBeforeValidate: false,
collectionBeforeValidate: false,
fieldBeforeChange: false,