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 -1 for “all”).
  • display_errorsini_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

  1. 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 $wgShowDebug injected logs.
  2. Temporarily replace FatalErrorHandler::handle() with a custom stub that writes the full back‑trace to a file. This works even when PHP dies before error_reporting runs.
  3. Leverage MediaWiki\HookContainer introspection: call $hookContainer->getHookNames() to list all registered hooks for a given page, then verify your extension’s hook is actually attached.
  4. Watch the APCu or OPcache hit‑rate. A stale cache can make you think the code never changed. apc.php (or opcache-gui) shows you which files are cached.
  5. Turn on $wgEnableWriteAPI = true in a dev environment and fire off a write via api.php. The API logs (when $wgDebugLogFile includes api) 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:

  1. Enable $wgDebugToolbar and $wgShowDebug. Refresh the problematic page – the debug bar shows a “Fatal error” with a stack trace.
  2. Copy the filename from the trace (Parser.php around line 612). Insert a wfDebug() right before the line to see the variable values.
  3. Run the same request via curl while XDebug is listening. Step in the IDE; you discover the parser function receives null for the second argument because the hook never fires.
  4. Check ExtensionRegistration.php. A missing Hooks::register() call is the culprit. Add it, clear cache with php maintenance/rebuildLocalisationCache.php.
  5. Re‑run the page. The blank is gone. Verify the logs contain no stray Warning messages 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.

Subscribe to MediaWiki Tips and Tricks

Don’t miss out on the latest articles. Sign up now to get access to the library of members-only articles.
jamie@example.com
Subscribe