Files
XC_VM/docs/en/guides/error-handling.md
T
Divarion_D a453cd9620 docs: fix stale references to the removed prelude files
The prelude shims (Paths/AppConfig/Binaries/ErrorCodes.php) were deleted and the
$rErrorCodes global is gone. Point the developer guides at the new homes:
constants → ConstantsInitializer (paths()/appConfig()/binaries() maps), error
catalogue → ErrorResponder::codes(). Also refresh the now-outdated "refactored
later" note in build/rector.php's skip list.

Only docs/en is edited (docs/ru is regenerated from it before a release);
make docs-build passes.
2026-09-16 18:52:01 +03:00

380 lines
16 KiB
Markdown

# Error Handling Model
XC_VM error handling has three layers:
- **Error codes** -- what failed (centralized registry of named error strings)
- **Error handlers** -- how the client HTTP response is produced (`generateError()`, `generate404()`)
- **Logger subsystem** -- runtime capture of PHP errors, uncaught exceptions, and fatal crashes
---
## Flow Overview
```text
Application code
|
+-- generateError('CODE') // deliberate error response
| -> debug mode: styled HTML page with code + description
| -> production: generate404() or explicit HTTP code
|
+-- PHP warning / notice / error // runtime errors
| -> Logger::handleError()
| -> maps errno to level (ERROR, WARNING, NOTICE, INFO)
| -> writes base64-encoded JSON to error_log.log
| -> optionally displays on screen
|
+-- Uncaught exception // unhandled Throwable
| -> Logger::handleException()
| -> logs as EXCEPTION with full chained trace
|
+-- Fatal error at shutdown // E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR
-> Logger::handleFatal()
-> logs as FATAL (no stack trace available)
```
---
## Error Code Registry
All codes are declared by the `ErrorResponder::codes()` method in `src/Core/Error/ErrorResponder.php`, which returns the code => English-description array.
Code format:
- Key: uppercase string (example: `INVALID_CREDENTIALS`)
- Value: human-readable English description
Use centralized code definitions only. Do not hardcode error text in endpoint handlers.
### Full code list
| Code | Description |
| --- | --- |
| `API_IP_NOT_ALLOWED` | IP is not allowed to access the API. |
| `ARCHIVE_DOESNT_EXIST` | Archive files are missing for this stream ID. |
| `ASN_BLOCKED` | ASN has been blocked. |
| `BANNED` | Line has been banned. |
| `BLOCKED_USER_AGENT` | User-agent has been blocked. |
| `CACHE_INCOMPLETE` | Cache is being generated... |
| `DEVICE_NOT_ALLOWED` | MAG & Enigma devices are not allowed to access this. |
| `DISABLED` | Line has been disabled. |
| `DOWNLOAD_LIMIT_REACHED` | Reached the simultaneous download limit. |
| `E2_DEVICE_LOCK_FAILED` | Device lock checks failed. |
| `E2_DISABLED` | Device has been disabled. |
| `E2_NO_TOKEN` | No token has been specified. |
| `E2_TOKEN_DOESNT_MATCH` | Token doesn't match records. |
| `E2_WATCHDOG_TIMEOUT` | Time limit reached. |
| `EMPTY_USER_AGENT` | Empty user-agents are disallowed. |
| `EPG_DISABLED` | EPG has been disabled. |
| `EPG_FILE_MISSING` | Cached EPG files are missing. |
| `EXPIRED` | Line has expired. |
| `FORCED_COUNTRY_INVALID` | Country does not match forced country. |
| `GENERATE_PLAYLIST_FAILED` | Playlist failed to generate. |
| `HLS_DISABLED` | HLS has been disabled. |
| `HOSTING_DETECT` | Hosting server has been detected. |
| `INVALID_API_PASSWORD` | API password is invalid. |
| `INVALID_CREDENTIALS` | Username or password is invalid. |
| `INVALID_HOST` | Domain name not recognised. |
| `INVALID_STREAM_ID` | Stream ID doesn't exist. |
| `INVALID_TYPE_TOKEN` | Tokens can't be used for this stream type. |
| `IP_BLOCKED` | IP has been blocked. |
| `IP_MISMATCH` | Current IP doesn't match initial connection IP. |
| `ISP_BLOCKED` | ISP has been blocked. |
| `LB_TOKEN_INVALID` | AES Token cannot be decrypted. |
| `LEGACY_EPG_DISABLED` | Legacy epg.php access has been disabled. |
| `LEGACY_GET_DISABLED` | Legacy get.php access has been disabled. |
| `LEGACY_PANEL_API_DISABLED` | Legacy panel_api.php access has been disabled. |
| `LINE_CREATE_FAIL` | Line failed to insert into database. |
| `NO_CREDENTIALS` | No credentials have been specified. |
| `NO_SERVERS_AVAILABLE` | No servers are currently available for this stream. |
| `NO_TIMESTAMP` | No archive timestamp has been specified. |
| `NO_TOKEN_SPECIFIED` | No AES encrypted token has been specified. |
| `NOT_ENIGMA_DEVICE` | Line isn't an enigma device. |
| `NOT_IN_ALLOWED_COUNTRY` | Not in allowed country list. |
| `NOT_IN_ALLOWED_IPS` | Not in allowed IP list. |
| `NOT_IN_ALLOWED_UAS` | Not in allowed user-agent list. |
| `NOT_IN_BOUQUET` | Line doesn't have access to this stream ID. |
| `PLAYER_API_DISABLED` | Player API has been disabled. |
| `PROXY_ACCESS_DENIED` | You cannot access this stream directly while proxy is enabled. |
| `PROXY_DETECT` | Proxy has been detected. |
| `PROXY_NO_API_ACCESS` | Can't access API's via proxy. |
| `RESTREAM_DETECT` | Restreaming has been detected. |
| `STALKER_CHANNEL_MISMATCH` | Stream ID doesn't match stalker token. |
| `STALKER_DECRYPT_FAILED` | Failed to decrypt stalker token. |
| `STALKER_INVALID_KEY` | Invalid stalker key. |
| `STALKER_IP_MISMATCH` | IP doesn't match stalker token. |
| `STALKER_KEY_EXPIRED` | Stalker token has expired. |
| `STREAM_OFFLINE` | Stream is currently offline. |
| `SUBTITLE_DOESNT_EXIST` | Subtitle file doesn't exist. |
| `THUMBNAIL_DOESNT_EXIST` | Thumbnail file doesn't exist. |
| `THUMBNAILS_NOT_ENABLED` | Thumbnail not enabled for this stream. |
| `TOKEN_ERROR` | AES token has incomplete data. |
| `TOKEN_EXPIRED` | AES token has expired. |
| `TS_DISABLED` | MPEG-TS has been disabled. |
| `USER_ALREADY_CONNECTED` | Line already connected on a different IP. |
| `USER_DISALLOW_EXT` | Extension is not in allowed list. |
| `VOD_DOESNT_EXIST` | VOD file doesn't exist. |
| `WAIT_TIME_EXPIRED` | Stream start has timed out, failed to start. |
The streaming-specific codes (`CACHE_INCOMPLETE`, `SUBTITLE_DOESNT_EXIST`, `NO_SERVERS_AVAILABLE`, `PROXY_ACCESS_DENIED`) were migrated from `stream/init.php` into the centralized registry.
---
## Error Handlers
Defined in `src/Core/Error/ErrorHandler.php`. These are plain functions (not class methods) loaded early in bootstrap.
### `generateError(string $rError, bool $rKill = true, ?int $rCode = null)`
Produces an HTTP error response. Behavior depends on the `debug_show_errors` setting:
```text
if debug_show_errors === true
render styled HTML page showing error key + description
if $rKill -> exit()
else (production)
if $rKill
if $rCode is set -> http_response_code($rCode) + exit()
else -> generate404()
// if !$rKill, does nothing in production mode
```
Parameters:
| Param | Type | Default | Meaning |
| --- | --- | --- | --- |
| `$rError` | `string` | -- | Key from `ErrorResponder::codes()` |
| `$rKill` | `bool` | `true` | Terminate script after output |
| `$rCode` | `int\|null` | `null` | Explicit HTTP response code (bypasses 404 in production) |
Examples:
```php
generateError('INVALID_CREDENTIALS'); // production: 404 + exit
generateError('API_IP_NOT_ALLOWED', true, 403); // production: 403 + exit
generateError('STREAM_OFFLINE', false); // production: no output, no exit
```
### `generate404(bool $rKill = true)`
Returns an nginx-style `404 Not Found` page and sets HTTP 404. The HTML includes padding comments to suppress browser-friendly error pages in MSIE and Chrome.
```php
generate404(); // 404 + exit
generate404(false); // 404, continue execution
```
---
## Logger Subsystem
Defined in `src/Core/Logging/Logger.php`. A `final` class that registers three global PHP handlers to capture all runtime errors and log them to a file.
### Initialization
```php
Logger::init(bool $showErrors, string $logFile): void
```
Registers:
1. `set_error_handler([Logger::class, 'handleError'])` -- PHP warnings, notices, errors
2. `set_exception_handler([Logger::class, 'handleException'])` -- uncaught `Throwable`
3. `register_shutdown_function([Logger::class, 'handleFatal'])` -- fatal errors at shutdown
Also configures `error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED)` and sets `display_errors` / `display_startup_errors` based on `$showErrors`.
### Where Logger::init() is called
Logger is initialized in two places, depending on the request path:
| Entry path | File | How |
| --- | --- | --- |
| Bootstrap (all contexts) | `src/bootstrap.php` | `XC_Bootstrap::loadConstants()` calls `Logger::init(PHP_ERRORS, LOGS_TMP_PATH . 'error_log.log')` |
| Streaming endpoints | `src/Core/Http/RequestGuard.php` | Loads settings from file cache, defines `PHP_ERRORS`, then calls `Logger::init(PHP_ERRORS, LOGS_TMP_PATH . 'error_log.log')` |
In both cases `PHP_ERRORS` mirrors the `debug_show_errors` setting (defaults to `false` when settings are unavailable).
### Error level mapping
`Logger::handleError()` maps PHP error constants to log level strings via `mapErrorLevel()`:
| PHP constant(s) | Log level |
| --- | --- |
| `E_ERROR`, `E_CORE_ERROR`, `E_COMPILE_ERROR` | `ERROR` |
| `E_WARNING`, `E_USER_WARNING` | `WARNING` |
| `E_NOTICE`, `E_USER_NOTICE` | `NOTICE` |
| All other `errno` values | `INFO` |
The shutdown handler (`handleFatal()`) checks `error_get_last()` for these fatal types and logs them as `FATAL`:
| PHP constant(s) at shutdown | Log level |
| --- | --- |
| `E_ERROR`, `E_PARSE`, `E_CORE_ERROR`, `E_COMPILE_ERROR` | `FATAL` |
Uncaught exceptions logged by `handleException()` always use the level `EXCEPTION`.
Errors suppressed with the `@` operator are ignored (the handler checks `error_reporting() & $errno`).
### Log format
Each log entry is written as a single line: `base64_encode(json_encode($data))` followed by a newline. This prevents line corruption from multi-line messages.
Decoded JSON structure:
```json
{
"type": "WARNING",
"log_message": "Undefined variable $foo",
"file": "/home/xc_vm/Domain/Stream/StreamService.php",
"line": 142,
"log_extra": "#0 /home/xc_vm/...(line): function()\n#1 ...",
"time": 1716220800,
"env": "fpm-fcgi"
}
```
| Field | Content |
| --- | --- |
| `type` | Log level: `ERROR`, `WARNING`, `NOTICE`, `INFO`, `EXCEPTION`, or `FATAL` |
| `log_message` | Error/exception message text |
| `file` | Absolute path to the source file |
| `line` | Line number where the error occurred |
| `log_extra` | Stack trace (formatted string). Empty for fatal errors. |
| `time` | Unix timestamp |
| `env` | PHP SAPI name (`cli`, `fpm-fcgi`, etc.) |
### Log file location
Default path: `LOGS_TMP_PATH . 'error_log.log'`
If the log directory does not exist, Logger creates it with permissions `0775`. When running as root (common in containers), the file is chowned to `xc_vm:xc_vm` with mode `0664`.
### Screen output
When `$showErrors` is `true`, Logger also renders errors directly:
- **CLI:** color-coded terminal output (red for FATAL/ERROR, yellow for WARNING, blue for NOTICE)
- **Web:** inline `<div>` with monospace font, red border, and stack trace in a `<pre>` block
---
## Logging Pipeline: File to Database
The Logger writes to `error_log.log` on disk. A separate subsystem reads that file and persists entries to the `panel_logs` database table:
1. **Logger** writes base64-encoded JSON lines to `error_log.log`
2. **FileLogger** (`src/Core/Logging/FileLogger.php`) provides a secondary logging interface used by application code (PDO errors, EPG errors, etc.) that writes to the same file in the same format
3. Entries are ingested into the `panel_logs` table
4. **DiagnosticsService** (`src/Core/Diagnostics/DiagnosticsService.php`) reads from `panel_logs` for:
- `downloadPanelLogs()` -- retrieves up to 1000 recent non-EPG errors, then truncates the table
- `submitPanelLogs()` -- sends logs to the central API server for analysis
5. The admin panel exposes these logs under **Management > Logs > Panel Errors**
### FileLogger noise filtering
`FileLogger::log()` skips entries that match:
- Messages containing `panel_logs` in the extra field (prevents recursive logging)
- Messages matching `timeout exceeded`, `lock wait timeout`, or `duplicate entry` (noisy MySQL errors)
---
## Other Loggers
The `src/Core/Logging/` directory contains additional specialized loggers:
| Class | File | Purpose |
| --- | --- | --- |
| `Logger` | `Logger.php` | Global PHP error/exception/fatal handler (described above) |
| `FileLogger` | `FileLogger.php` | Application-level logging (PDO errors, EPG, etc.) to `error_log.log` |
| `DatabaseLogger` | `DatabaseLogger.php` | Client streaming request events to `client_request.log` (ingested into `client_logs` table) |
| `UpdateLogger` | `UpdateLogger.php` | System update operations to `MAIN_HOME/update.log` (plain text, not base64) |
All loggers except `UpdateLogger` implement `LoggerInterface` and write base64-encoded JSON.
---
## Exception Types in the Codebase
The codebase defines a small number of custom exception classes. All uncaught exceptions are caught by `Logger::handleException()`, which logs the full exception chain (including `getPrevious()`).
| Exception class | Base class | Location |
| --- | --- | --- |
| `DropboxException` | `\Exception` | `src/Core/Storage/DropboxException.php` |
| `M3uParser\Exception` | `\Exception` | `src/vendor/gemorroj/m3u-parser/src/Exception.php` |
| `DataBuildingException` | `\RuntimeException` | `src/vendor/chrisyue/php-m3u8/src/Parser/DataBuildingException.php` |
| `DefinitionException` | `\RuntimeException` | `src/vendor/chrisyue/php-m3u8/src/Definition/DefinitionException.php` |
| `DumpingException` | `\RuntimeException` | `src/vendor/chrisyue/php-m3u8/src/Dumper/DumpingException.php` |
Most application code uses generic `Exception` throws or relies on PHP's built-in error system. The Logger's exception handler accepts any `Throwable`.
---
## Debug vs Production
### Production (default: `debug_show_errors = false`)
- `generateError()` returns a generic 404 page (or the explicit HTTP code), hiding the internal failure reason
- Logger still writes all errors to `error_log.log` on disk
- `display_errors` and `display_startup_errors` are set to `'0'`
- Errors are only visible through the admin panel (Panel Errors page) or log files
### Debug (`debug_show_errors = true`)
- `generateError()` shows a styled page with the error key and mapped description
- Logger additionally renders errors on screen (color-coded CLI output or inline HTML)
- `display_errors` and `display_startup_errors` are set to `'1'`
Do not enable debug display on production nodes.
---
## Bootstrap Error Handler Registration
The error handling infrastructure is loaded early in the boot sequence:
1. `bootstrap.php` defines `MAIN_HOME` and registers the Composer autoloader
2. `XC_Bootstrap::loadConstants()` loads (in order):
- `Core/Error/ErrorHandler.php` -- defines `generateError()` and `generate404()` (loaded globally via Composer `autoload.files`); the code catalogue itself is `ErrorResponder::codes()` in `Core/Error/ErrorResponder.php`
- Path and config files
- `Core/Logging/Logger.php` -- class definition
3. `Logger::init(PHP_ERRORS, LOGS_TMP_PATH . 'error_log.log')` is called, registering the three global handlers
4. From this point forward, all PHP errors, uncaught exceptions, and fatal crashes are captured
For streaming endpoints that bypass the full bootstrap, `RequestGuard.php` performs steps 2-3 independently: it loads settings from the file cache, determines `PHP_ERRORS`, and calls `Logger::init()`.
---
## Adding a New Error Code
1. Add a new entry to the array returned by `ErrorResponder::codes()` in `src/Core/Error/ErrorResponder.php`:
```php
'MY_NEW_ERROR' => 'Human-readable description.',
```
2. Use it in code:
```php
generateError('MY_NEW_ERROR');
```
Descriptions must stay in English for consistency with the existing registry.
---
## Related files
| File | Purpose |
| --- | --- |
| `src/Core/Error/ErrorResponder.php` | Centralized error code map (`ErrorResponder::codes()`) |
| `src/Core/Error/ErrorHandler.php` | `generateError()` and `generate404()` functions |
| `src/Core/Logging/Logger.php` | Global PHP error, exception, and fatal handlers |
| `src/Core/Logging/LoggerInterface.php` | Logging contract interface |
| `src/Core/Logging/FileLogger.php` | Application-level file logging (PDO, EPG, etc.) |
| `src/Core/Logging/DatabaseLogger.php` | Client streaming request event logging |
| `src/Core/Logging/UpdateLogger.php` | System update operation logging |
| `src/Core/Http/RequestGuard.php` | Streaming path: flood protection, host check, Logger init |
| `src/Core/Diagnostics/DiagnosticsService.php` | Reads `panel_logs` table for admin display and API submission |
| `src/bootstrap.php` | Includes error layer and Logger in all bootstrap contexts |