When modules.processwire.com is unresponsive, the login hook in ProcessWireUpgradeCheck hangs the login request until PHP's max_execution_time kills it. The superuser's login succeeds, but the fatal error hits before the regenerated session is written, so they land back on the login page with no session. Every retry fails the same way, which locks the superuser out of the admin.
Environment: ProcessWireUpgradeCheck 9, ProcessWire 3.0.273, PHP 8.5, TFA enabled, default useLoginHook = 1.
What happens
- On superuser login,
loginHook() calls getModuleVersions(), which requests $config->moduleServiceURL (https://modules.processwire.com/export-json/...).
- Right now that host completes the TLS handshake and then never sends a response:
$ curl -s -o /dev/null -w "code=%{http_code} tls=%{time_appconnect}s total=%{time_total}s\n" --max-time 12 "https://modules.processwire.com/export-json/?apikey=pw300&limit=1"
code=000 tls=0.047770s total=12.003673s
- WireHttp's
fopen and curl attempts both honour the module's 4.5s timeout and fail. WireHttp then falls back to sendSocket(). The while(!feof($fs)) { fgets(...) } loop there (WireHttp.php ~line 1002) has no read timeout ($timeout only applies to fsockopen()'s connect), so it blocks until:
Fatal error: Maximum execution time of 30+2 seconds exceeded (terminated) in wire/core/Tools/WireHttp/WireHttp.php on line 1002
- The 12-hour cache is only saved at the end of
loginHook(), so it is never populated and every subsequent login repeats the hang.
The GitHub requests (branches, raw ProcessWire.php) are fine; only the modules directory request hangs.
Workaround: set useLoginHook to 0, e.g.
$c = $modules->getConfig('ProcessWireUpgradeCheck');
$c['useLoginHook'] = 0;
$modules->saveConfig('ProcessWireUpgradeCheck', $c);
Possible fixes
- In this module, pass
['use' => ['curl', 'fopen']] to the WireHttp calls so a timeout doesn't fall through to the socket method. Alternatively, run the check somewhere other than the login request, or save a short negative cache entry when a request fails, so one outage doesn't hit every login.
- In the core,
sendSocket() could call stream_set_timeout($fs, $timeout) and stop when stream_get_meta_data($fs)['timed_out'] is set. I can open that in processwire-issues if useful.
A smaller thing noticed while reading loginHook():
if(!empty($cacheData) && is_string($cacheData)) $cache = json_decode($cacheData, true);
This assigns to $cache (the WireCache instance) rather than $cacheData. It looks like it should be $cacheData = json_decode(...).
When modules.processwire.com is unresponsive, the login hook in ProcessWireUpgradeCheck hangs the login request until PHP's
max_execution_timekills it. The superuser's login succeeds, but the fatal error hits before the regenerated session is written, so they land back on the login page with no session. Every retry fails the same way, which locks the superuser out of the admin.Environment: ProcessWireUpgradeCheck 9, ProcessWire 3.0.273, PHP 8.5, TFA enabled, default
useLoginHook = 1.What happens
loginHook()callsgetModuleVersions(), which requests$config->moduleServiceURL(https://modules.processwire.com/export-json/...).fopenandcurlattempts both honour the module's 4.5s timeout and fail. WireHttp then falls back tosendSocket(). Thewhile(!feof($fs)) { fgets(...) }loop there (WireHttp.php ~line 1002) has no read timeout ($timeoutonly applies tofsockopen()'s connect), so it blocks until:loginHook(), so it is never populated and every subsequent login repeats the hang.The GitHub requests (branches, raw ProcessWire.php) are fine; only the modules directory request hangs.
Workaround: set
useLoginHookto 0, e.g.Possible fixes
['use' => ['curl', 'fopen']]to the WireHttp calls so a timeout doesn't fall through to the socket method. Alternatively, run the check somewhere other than the login request, or save a short negative cache entry when a request fails, so one outage doesn't hit every login.sendSocket()could callstream_set_timeout($fs, $timeout)and stop whenstream_get_meta_data($fs)['timed_out']is set. I can open that in processwire-issues if useful.A smaller thing noticed while reading
loginHook():This assigns to
$cache(the WireCache instance) rather than$cacheData. It looks like it should be$cacheData = json_decode(...).