Advanced Debugging Techniques for MediaWiki Developers
Why “just look at the error message” rarely works
MediaWiki is a tangled stack of PHP, MySQL, JavaScript and caching layers. A fatal PHP notice on line 42 of SpecialPage.php may actually be the downstream symptom of a missing extension hook that fires earlier in the request. In short, the surface‑level message is often a red herring.
Turn the “debug switches” on (temporarily)
Before you start chasing ghosts, flip the built‑in flags. Put them near the top of LocalSettings.php – after the opening <?php, before any extension is loaded. This keeps the scope wide and the changes easy to spot later.
$wgShowExceptionDetails = true; // stack trace on fatal errors
$wgDebugToolbar = true; // toolbar with profiling data
$wgShowDebug = true; // raw log at page bottom
$wgDevelopmentWarnings = true; // deprecation + notices
Don’t forget to revert them once you’ve narrowed the bug. Leaving them on a public wiki is a security nightmare because you’ll end up exposing database credentials or internal paths.
PHP‑level diagnostics
- error_reporting – set to
E_ALL(or-1for “all”). - display_errors –
ini_set('display_errors', 1);forces PHP to print the error instead of silently logging it.
If your host disables display_startup_errors, prepend a small wrapper to index.php:
error_reporting(-1);
ini_set('display_startup_errors', 1);
ini_set('display_errors', 1);
SQL tracing
When a page stalls for “seconds” and you have no clue why, enable the query dump. This is cheap, but be aware it will bloat the output for high‑traffic wikis.
$wgDebugDumpSql = true; // dump every query in the debug log
For production‑grade profiling, switch to the Profiler extension or the built‑in $wgProfiler configuration – it records timing and memory usage per hook.
XDebug – your new best friend
Stepping through MediaWiki code with a proper IDE is a game‑changer. XDebug works with PHPStorm, VS Code, NetBeans, you name it. The trick is getting the remote host right, especially inside Docker or Vagrant.
Minimal XDebug config for a local Docker container
xdebug.mode=debug
xdebug.start_with_request=yes
xdebug.client_host=host.docker.internal ; Docker‑for‑Mac/Windows bridge
xdebug.client_port=9003
After you spin the container, set a breakpoint in includes/Parser.php and watch the parser stack unwind. It’s surprisingly satisfying to see the exact point where a template variable becomes null.
Don’t forget the xdebug.remote_connect_back=0 line if you’re on a shared server – otherwise every request will try to connect to the wrong IP.
Debugging CLI scripts (maintenance, PHPUnit)
Running a maintenance script under XDebug is as easy as prefixing the command with xdebug_on:
xdebug_on php maintenance/rebuildrecentchanges.php --wiki=mywiki
xdebug_off
That one‑liner launches the remote debugger before the script boots, letting you step through the whole CLI flow. Handy when a maintenance/run.php wrapper swallows an exception.
Logging – the quiet workhorse
MediaWiki’s wfDebug() and wfLog() functions write to $wgDebugLogFile. They’re less flashy than the toolbar but survive crashes, so you can inspect them after a fatal error has killed the process.
$wgDebugLogFile = "/var/log/mediawiki/debug.log";
$wgDebugLogGroups = [
'exception' => '/var/log/mediawiki/exception.log',
'sql' => '/var/log/mediawiki/sql.log',
];
When developing an extension, sprinkle a custom log group:
wfDebugLog( 'myext', "Entered onBeforePageDisplay for {$title->getPrefixedText()}" );
Later you can grep it:
grep myext /var/log/mediawiki/debug.log | tail -n 20
Structured logging (JSON)
If you run a micro‑service architecture (e.g., a Node.js backend serving JSON to MediaWiki via RESTBase), emit JSON lines. Tools like jq make them trivially searchable.
$msg = json_encode([
'event' => 'hook_executed',
'hook' => $hookName,
'title' => $title->getPrefixedText(),
'elapsed' => microtime(true) - $start,
]);
wfDebugLog( 'myext', $msg );
Client‑side debugging – don’t forget the browser
Most MediaWiki “bugs” end up in JavaScript: AJAX calls that return HTML snippets, or the new Vue‑powered edit widgets. Open the console (F12) and look for mw.log() messages – they’re routed to the same server log when you enable $wgDebugLogGroups['javascript'].
mw.log( 'ExtensionFoo: API response', data );
If you need to trace a failing API request, fire up the Network tab, copy the curl command that Chrome offers, and replay it with --verbose to see server‑side headers.
Advanced tricks for seasoned developers
- Use the Debug Toolbar’s “Edit-HTML” view – it shows the raw output just before it hits the browser. You can spot stray
<!--debug-->comments that reveal where$wgShowDebuginjected logs. - Temporarily replace
FatalErrorHandler::handle()with a custom stub that writes the full back‑trace to a file. This works even when PHP dies beforeerror_reportingruns. - Leverage
MediaWiki\HookContainerintrospection: call$hookContainer->getHookNames()to list all registered hooks for a given page, then verify your extension’s hook is actually attached. - Watch the APCu or OPcache hit‑rate. A stale cache can make you think the code never changed.
apc.php(oropcache-gui) shows you which files are cached. - Turn on
$wgEnableWriteAPI = truein a dev environment and fire off a write viaapi.php. The API logs (when$wgDebugLogFileincludesapi) reveal hidden permission checks that the UI hides.
Putting it all together – a typical debugging workflow
Imagine a new MyExtension that adds a parser function. Users report “blank page” on certain articles. Here’s a quick, human‑style run‑through:
- Enable
$wgDebugToolbarand$wgShowDebug. Refresh the problematic page – the debug bar shows a “Fatal error” with a stack trace. - Copy the filename from the trace (
Parser.phparound line 612). Insert awfDebug()right before the line to see the variable values. - Run the same request via
curlwhile XDebug is listening. Step in the IDE; you discover the parser function receivesnullfor the second argument because the hook never fires. - Check
ExtensionRegistration.php. A missingHooks::register()call is the culprit. Add it, clearcachewithphp maintenance/rebuildLocalisationCache.php. - Re‑run the page. The blank is gone. Verify the logs contain no stray
Warningmessages before committing.
Final pointers
Debugging is as much an art as a science. The tools above give you visibility, but the real skill lies in asking the right “what if?” questions. Does the error appear only on anonymous users? Maybe a caching layer (Varnish) is serving stale HTML. Is the problem reproducible after clearing LocalSettings.php caches? If not, you probably have a race condition in an extension’s init routine.
Whenever you feel stuck, take a step back, turn on a single flag, and observe. One small piece of output can cut through an hour‑long rabbit‑hole. And remember: the best debug log is the one you write yourself, not the one the framework spits out.