How It Works
The plugin hooks into Craft's ErrorHandler::EVENT_BEFORE_HANDLE_EXCEPTION event. This fires at the end of the request lifecycle, after Craft's routing has already failed. This is different from Craft's built-in redirects via config/redirects.php, which fire at the start of the request before a page is even rendered.
mermaid
flowchart TD
A[404 Exception] --> B{EVENT_BEFORE_HANDLE}
B -->|canceled| C[Craft renders 404 template]
B -->|continues| D[EVENT_DEFINE_URI]
D --> E[DB Lookup]
E --> F{Match found?}
F -->|Yes| G[Log as handled]
G --> H[EVENT_BEFORE_REDIRECT]
H --> I{Status code?}
I -->|301 / 302 / 307| J[Redirect + end]
I -->|404| J2[Fast 404 text response]
I -->|410| K[410 Gone response]
J --> L2[EVENT_AFTER_REDIRECT]
F -->|No| L[Log as unhandled]
L --> M[Craft renders 404 page]
style B fill: #f9f, stroke: #333
style F fill: #f9f, stroke: #333
style I fill: #f9f, stroke: #333
style C fill: #fee, stroke: #c33
style J fill: #efe, stroke: #3c3
style J2 fill: #fee, stroke: #c33
style K fill: #fee, stroke: #c33
style M fill: #fee, stroke: #c33Pipeline
Every 404 flows through this pipeline. Events on NotFoundUriService allow you to customize each stage.
EVENT_BEFORE_HANDLEfires. Cancel to skip plugin handling entirely (Craft renders its normal 404 template).EVENT_DEFINE_URIfires. Modify the URI used for redirect matching (defaults to$request->getPathInfo()).- Exact-match redirects are checked first via a fast SQL query (filtered by
enabled,siteId,startDate/endDate). - If no exact match, pattern and regex redirects are loaded and tested in priority order.
- If a match is found:
- The 404 is logged as "handled"
EVENT_BEFORE_REDIRECTfires. Modify the destination URL or cancel the redirect.- The visitor is redirected (301/302/307), shown a 410 Gone page, or given a fast 404 text response.
EVENT_AFTER_REDIRECTfires. Post-redirect hook for analytics or logging.
- If no match is found, the 404 is logged as "unhandled" and the error page renders normally.
- The referrer (if present) is recorded for audit.