Skip to content

[2.x] extension-manager: in-process re-check after global update can fatal with a torn autoloader #4764

Description

@imorland

Description

When the Extension Manager performs a global update (POST /api/extension-manager/global-update), the post-update "re-check for updates" runs synchronously, in the same PHP request that just rewrote vendor/. If that composer update bumped a dependency, the request is now executing against a half-swapped vendor/ (new PHP files on disk, but the autoloader/opcache for the live process still reflect the old set), and constructing a class from the freshly-updated dependency fatals — taking down the request before the update flow can finish.

A user reported being unable to complete the upgrade to v2.0.0-rc.4, hitting:

Error: Class "GuzzleHttp\Handler\CurlVersion" not found
  in vendor/guzzlehttp/guzzle/src/Utils.php:108
full stack trace (trimmed)
#0 …/guzzle/src/HandlerStack.php(49): GuzzleHttp\Utils::chooseHandler()
#1 …/guzzle/src/Client.php(69): GuzzleHttp\HandlerStack::create()
#2 …/illuminate/container/Container.php(1171): GuzzleHttp\Client->__construct(Array)
…
#13 …/extension-manager/src/Listener/ReCheckForUpdates.php(49): Illuminate\Bus\Dispatcher->dispatch(CheckForUpdates)
#14 …/illuminate/events/Dispatcher.php(519): ReCheckForUpdates->handle(FlarumUpdated)
#17 …/extension-manager/src/Command/GlobalUpdateHandler.php(55): Dispatcher->dispatch(FlarumUpdated)
#24 …/extension-manager/src/Api/Controller/GlobalUpdateController.php(35): Job\Dispatcher->dispatch(GlobalUpdate)

Root cause

GuzzleHttp\Handler\CurlVersion was introduced in Guzzle 7.12.0 (released 2026-06-16; 7.12.1 followed on 2026-06-18) — both stable releases. It does not exist in 7.11.x. Core requires guzzlehttp/guzzle: ^7.7, so a global update legitimately bumps an older install straight onto the new stable 7.12. 7.12's Utils::chooseHandler() references the new CurlVersion class — which the running process can't autoload, because it started under 7.11.x.

This is not a minimum-stability / dev-version problem — the user pulled a normal stable release. Guzzle 7.12.0 was published on 2026-06-16, ~1.5 days before rc.4 (2026-06-18), so the 7.12 bump was available to any composer update independently of rc.4; rc.4 is not what introduced the exposure.

The chain, all within the one request:

  1. GlobalUpdateHandler::handle() runs composer update --with-all-dependencies, rewriting vendor/ in place — bumping Guzzle 7.11.x → 7.12.x.
  2. It then synchronously dispatches FlarumUpdated (GlobalUpdateHandler.php:56).
  3. ReCheckForUpdates::handle() synchronously dispatches CheckForUpdates (ReCheckForUpdates.php:49).
  4. The container builds CheckForUpdatesHandler, which has a constructor-injected GuzzleHttp\Client $http — constructing it calls Utils::chooseHandler(), which needs GuzzleHttp\Handler\CurlVersion. The process is still running with 7.11.x's autoloader/class map → Class not found fatal.

Composer having returned exit 0 to step 1 does not mean the install is in a usable state: the request dies partway through the flow, the same process can no longer load the new classes, and the admin is left with a 500 and the upgrade not completed.

This won't reproduce on every upgrade — it needs the update to actually move a dependency whose new code the already-booted process can't load (as Guzzle 7.12 does here). But when it hits, the user cannot complete the update from the panel (see below).

Impact

  • The upgrade fails: a fatal/500 is surfaced to the admin and the update flow does not complete.
  • The Extension Manager exposes no repair/reinstall action — only update/require/remove — so re-running "Update" just re-enters the same GlobalUpdateHandler chain and can fail the same way. A user without CLI or host-panel composer access has no in-panel way to recover.

Fix

Don't perform the post-update re-check (the FlarumUpdatedReCheckForUpdates → Guzzle Client work) in the same request that rewrote vendor/:

  • Defer it to a queued job / fresh process (the extension already has queue machinery in Job/Dispatcher with sync/async support), or
  • Return after composer update and let a subsequent, fresh request perform the re-check.

More broadly, anything that constructs or touches freshly-updated vendor/ classes within the same request that ran composer update is unsafe and should run in a new process. Worth also considering a recovery path for a half-applied update that doesn't require CLI access.

Environment

  • flarum/extension-manager 2.x
  • Reported while upgrading to flarum/core v2.0.0-rc.4, but the trigger is guzzlehttp/guzzle bumping 7.11.x → 7.12.x (7.12.0 released 2026-06-16, 7.12.1 on 2026-06-18). Any global update that moves Guzzle onto 7.12 can hit this; it is independent of the Flarum release being installed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions