View .md

Debugging plugins

Find out why a FeatherPanel plugin does not load, a route returns 404 or a page stays empty. Log messages, the validate endpoint, worker restarts and a release checklist.

On this page

Where to look

ToolWhat it tells you
storage/logs/App.fplogEverything the plugin loader, the router and your own logger calls write. In Docker: /var/www/html/storage/logs/App.fplog in the backend container, on the featherpanel_logs volume.
Logs in the admin area (System group)The same log in the browser. Needs admin.root.
Admin > Plugins > your pluginWhether the plugin is loaded, unmet dependencies and missing required settings.
GET /api/admin/plugin-manager/{identifier}/validateA checklist, see below. Developer mode and admin.root only.
/var/log/cron.log in the backend containerOutput of the cron runner, including your cron classes.
Browser dev toolsErrors from Frontend/index.js, your iframe pages and failed /api/addons/... calls.

Write your own log lines with the panel logger:

use App\App;

$logger = App::getInstance(true)->getLogger();
$logger->info('myplugin: sync started');
$logger->warning('myplugin: remote API slow');
$logger->error('myplugin: sync failed: ' . $e->getMessage(), false);

error() and critical() also send the message to the panel’s error telemetry unless the second argument is false. debug() only writes when APP_DEBUG is on, which it is not in release builds, so use info() instead.

Developer mode and the debug console

Turn on developer mode (the app_developer_mode setting) in the admin settings. It unlocks:

  • Admin > Dev > Plugins: create plugins from templates, edit their manifest and settings, and add migrations, cron jobs, commands and public files with Create file,
  • plugin export,
  • the validate endpoint,
  • the panel debug console: type ##icanhasfeatherpanel in the global search. It has network probes, benchmarks, a log tail for admins, a debug snapshot you can copy, and links to the developer docs and the dev plugin page.

The validate endpoint

GET /api/admin/plugin-manager/{identifier}/validate returns:

FieldTrue when
config_validconf.yml passes the same checks the loader uses.
dependencies_metEvery dependencies entry is satisfied.
required_configs_setEvery key in requiredConfigs has a stored value.
identifier_validThe identifier only uses letters, digits and _.
flags_validEvery flag is a known flag.
files_existconf.yml and <plugin.name>.php exist in the plugin root.
overall_validAll of the above.

Log messages from the plugin loader

The loader checks each folder in storage/addons/ in this order and stops at the first problem:

Log messageCause and fix
Invalid plugin identifier: <folder>The folder name has characters other than letters, digits and _. Rename it.
Plugin config is empty for: <id>conf.yml is missing, empty or does not parse.
Missing key for plugin: <id> key: <key> then Invalid config for plugin: <id>A required plugin: key is missing. Add it, even if the value is an empty list.
Invalid type for plugin: <id> key: <key>A key has the wrong type, for example author as a string instead of a list.
Invalid flags for plugin: <id>flags contains an unknown value.
Skipping plugin "<folder>": conf.yml identifier "<x>" does not match folder nameRename the folder or fix plugin.identifier.
Plugin <id> has unmet dependencies!A php=, php-ext=, composer= or plugin= entry is not satisfied. The admin plugin page lists which.
Skipping plugin "<id>": entry class could not be resolved. Ensure plugin.name, the PHP class name, and namespace App\Addons\<id> all match.The loader looks for App\Addons\<id>\<plugin.name>. Check the namespace line, the class name, the file name and that the class implements App\Plugins\AppPlugin.
Failed to process plugin eventYour processEvents() threw. The message is not logged, so wrap its body in try/catch and log the exception yourself while you debug.
Failed to load plugin routes from <file>: <message>A route file threw while loading. Only that file is skipped.
Skipping addon route file whose name matches another addon identifierA file in your Routes/ has the same name as another installed plugin. Rename it.
Missing required config '<key>' for plugin: <id>A key from requiredConfigs has no stored value. The plugin still loads.

Errors when installing a package

Uploading or installing an .fpa checks the entry class before anything is copied. The API answers 422 with:

Error codeMeaning
ADDON_INVALIDNo conf.yml at the root of the archive. The files must not sit inside a folder.
ADDON_CONF_PARSE_FAILEDconf.yml is not valid YAML.
ADDON_IDENTIFIER_INVALIDThe identifier in conf.yml is missing or invalid.
ADDON_ENTRY_INVALIDThe entry class check failed. data.errors lists every problem, for example Entry class namespace "..." does not match required "App\Addons\<id>".
ADDON_MIGRATION_FAILEDA file in Migrations/ failed. The response includes the migration output.

Common problems

A route returns API_ROUTE_NOT_FOUND

  • The file is not under Routes/, does not end in .php, or does not return function (RouteCollection $routes).
  • The route file threw while loading. Search the log for Failed to load plugin routes.
  • The API runs in worker mode and the worker started before you added the file. Click Resync on the plugin page or restart the backend container. Any successful admin change under /api/admin/plugins makes the workers restart.

PHP changes do not show up

In worker mode, which the Docker image uses by default, each worker keeps your classes in memory until it restarts. Click Resync, save a plugin setting, or restart the backend container. To develop without restarts, set FRANKENPHP_WORKER=false on the backend container so every request loads the code again. See Middleware and router hooks.

Frontend/index.js or index.css changes do not show up

The panel joins every plugin’s index.js (and index.css) and caches the result for two minutes, and the browser may keep it for another minute. Wait, then hard reload. Each plugin’s script is wrapped in its own function, so a variable you expect on window must be assigned to window explicitly. If your script uses window.FeatherPanel, wait until it exists, as the Starter template does.

A sidebar page is empty or shows the wrong file

  • component is a path inside Frontend/Components/. The panel loads /components/<identifier>/<component>.
  • After copying files by hand, click Resync so public/components/<identifier> points at your folder.
  • Open the component URL directly in the browser to see the real error.

A cron job never runs

  • The class must be App\Addons\<identifier>\Cron\<file name>. The cron log shows Class ... not found otherwise.
  • The interval unit must be upper case: 5M, not 5m.
  • An earlier PHP Error (not Exception) in any job stops the whole run. Check the cron log for a fatal error.
  • Delete storage/caches/cron/<id>.fptj to force the next run.

A setting is always empty

default in conf.yml is only a placeholder in the form. Seed values in pluginInstall() or fall back in code. See Settings.

Checklist before you release

  • conf.yml has every required key, a bumped version, and an identifier that matches the folder.
  • The validate endpoint reports overall_valid: true.
  • The entry class is the only PHP file in the plugin root.
  • Route names and paths are prefixed with your identifier.
  • Every value from a request reaches SQL through prepared statements.
  • Listeners and processEvents() catch their own exceptions.
  • Nothing user-specific is kept in static properties.
  • Cron classes use App\Addons\<identifier>\Cron and upper-case intervals.
  • Migrations use IF NOT EXISTS and table names start with featherpanel_<identifier>_.
  • pluginUninstall() removes the tables and settings the plugin created.
  • The plugin installs from an .fpa on a clean panel. See Packaging and publishing.

Frequently asked questions

Where are the panel logs?

The application log is backend/storage/logs/App.fplog, which is /var/www/html/storage/logs/App.fplog in the Docker backend container. Admins with admin.root can also read it on the Logs page in the admin area.

My plugin shows as installed but not loaded. Why?

Usually the conf.yml is invalid, the identifier does not match the folder, a dependency is missing or the entry class name, file name and namespace do not match. The log names the exact reason; search it for your identifier.