83 lines
3.4 KiB
Markdown
83 lines
3.4 KiB
Markdown
---
|
|
title: Always Call e.Next() and Use e.App Inside Hook Handlers
|
|
impact: CRITICAL
|
|
impactDescription: Forgetting e.Next() silently breaks the execution chain; reusing parent-scope app causes deadlocks
|
|
tags: hooks, events, extending, transactions, deadlock
|
|
---
|
|
|
|
## Always Call e.Next() and Use e.App Inside Hook Handlers
|
|
|
|
Every PocketBase event hook handler is part of an execution chain. If the handler does not call `e.Next()` (Go) or `e.next()` (JS), **the remaining handlers and the core framework action are skipped silently**. Also, hooks may run inside a DB transaction - any database call made through a captured parent-scope `app`/`$app` instead of the event's own `e.App`/`e.app` will deadlock against the transaction.
|
|
|
|
**Incorrect (missing `Next`, captured parent-scope app, global mutex):**
|
|
|
|
```go
|
|
var mu sync.Mutex // ❌ global lock invoked recursively by cascade hooks = deadlock
|
|
app := pocketbase.New()
|
|
|
|
app.OnRecordAfterCreateSuccess("articles").BindFunc(func(e *core.RecordEvent) error {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
|
|
// ❌ uses outer `app`, not `e.App` - deadlocks when the hook fires
|
|
// inside a transaction, because the outer app is blocked on the
|
|
// transaction's write lock
|
|
_, err := app.FindRecordById("audit", e.Record.Id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil // ❌ forgot e.Next() - framework never persists the record
|
|
})
|
|
```
|
|
|
|
```javascript
|
|
// JSVM
|
|
onRecordAfterCreateSuccess((e) => {
|
|
// ❌ no e.next() = downstream hooks and response serialization skipped
|
|
console.log("created", e.record.id);
|
|
}, "articles");
|
|
```
|
|
|
|
**Correct (call Next, use `e.App`, attach an Id for later unbinding):**
|
|
|
|
```go
|
|
app := pocketbase.New()
|
|
|
|
app.OnRecordAfterCreateSuccess("articles").Bind(&hook.Handler[*core.RecordEvent]{
|
|
Id: "audit-article-create",
|
|
Priority: 10, // higher = later; default 0 = order of registration
|
|
Func: func(e *core.RecordEvent) error {
|
|
// Always use e.App - it is the transactional app when inside a tx
|
|
audit := core.NewRecord(/* ... */)
|
|
audit.Set("record", e.Record.Id)
|
|
if err := e.App.Save(audit); err != nil {
|
|
return err
|
|
}
|
|
return e.Next() // REQUIRED
|
|
},
|
|
})
|
|
|
|
// Later: app.OnRecordAfterCreateSuccess("articles").Unbind("audit-article-create")
|
|
```
|
|
|
|
```javascript
|
|
// JSVM - e.app is the transactional app instance
|
|
onRecordAfterCreateSuccess((e) => {
|
|
const audit = new Record($app.findCollectionByNameOrId("audit"));
|
|
audit.set("record", e.record.id);
|
|
e.app.save(audit);
|
|
|
|
e.next(); // REQUIRED
|
|
}, "articles");
|
|
```
|
|
|
|
**Rules of the execution chain:**
|
|
|
|
- `Bind(handler)` vs `BindFunc(func)`: `Bind` lets you set `Id` (for `Unbind`) and `Priority`; `BindFunc` auto-generates both.
|
|
- Priority defaults to `0` = order of source registration. Lower numbers run first, negative priorities run before defaults (the built-in middlewares use priorities like `-1010`, `-1000`, `-990`).
|
|
- **Never hold a global mutex across `e.Next()`** - cascade-delete and nested saves can re-enter the same hook and deadlock.
|
|
- `Unbind(id)` removes a specific handler; `UnbindAll()` also removes **system handlers**, so only call it if you really mean to replace the default behavior.
|
|
- `Trigger(event, ...)` is almost never needed in user code.
|
|
|
|
Reference: [Go Event hooks](https://pocketbase.io/docs/go-event-hooks/) · [JS Event hooks](https://pocketbase.io/docs/js-event-hooks/)
|