You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Garbage collection stops after exception in SoapClient #10026
The SOAP exception apparently stops the garbage collector. I experienced this problem with a large, long running process. Its memory usage is intensive but stable. Until it encounters a SOAP exception: its memory usage skyrockets, and gc_status() reports that no more gc run is done.
Still reproducible on PHP 8.3.33 (bundled ext-soap), ten years after #66151 and three years after this report. Two details that may help narrow down the bailout path.
1. It is the WSDL parse path, not SoapFault in general.
A failed WSDL parse flips gc_status()['protected'] to true permanently. No network is needed — a malformed local file is enough:
A client created without WSDL whose call fails behaves differently — same SoapFault class, collector untouched:
<?phpvar_dump(gc_status()['protected']); // bool(false)$c = newSoapClient(null, ['location' => 'http://127.0.0.1:9/nope', 'uri' => 'urn:x']);
try { $c->anyMethod(); } catch (Throwable$e) { /* SoapFault: Could not connect to host */ }
var_dump(gc_status()['protected']); // bool(false)
So the trigger is specifically the WSDL parsing path, not SOAP error handling as a whole.
2. Nothing in userland clears the flag.gc_disable() + gc_enable() does not, as shown above; gc_protected is only reset by gc_reset() at request start.
Why this is worse than "GC is delayed". While the flag is set, gc_possible_root() does not even buffer candidates, so the cycles are not collected late — they are lost for the remainder of the process.
Impact in practice. In a PHPUnit suite, a single test constructing SoapClient against an unreachable WSDL costs cycle collection for the whole process. Every subsequent Laravel application instance then becomes uncollectable garbage and holds its MySQL connection open; the run died on max_connections about 150 tests later, with 938 failures all reporting SQLSTATE[HY000] [1040] Too many connections. Nothing in that symptom points anywhere near SOAP, and tracing it took a full day.
The mechanism @cmb69 identified in 2022 still matches the code: zend_bailout() sets gc_protect(1) before the LONGJMP, on the assumption that the request ends there; ext/soap catches that bailout in its own zend_catch and resumes without restoring the flag. Since gc_protect() returns the previous value, the save/restore idiom such a fix would need is already in the API.
@ndossche thanks, that's the crux, and it also makes #10283 as is unsafe. In soap_real_error_handler() the client converts anyE_FATAL_ERRORS into a SoapFault, so memory exhaustion or a timeout inside a SOAP call would also leave the GC unprotected. I confirmed this on 8.4: a memory_limit fatal in __doRequest() comes back as a SoapFault and the script continues.
I've opened #24338 with a narrower variant on top of your commits:
SOAP's own errors (soap_errorN()) are marked, and only for those does the handler bail out without gc_protect.
Engine fatal errors keep the current protected bailout. An unmarked SOAP error behaves exactly as today, so the change can only narrow the problem, not widen it.
The cleanup-and-rethrow sites preserve the original decision instead of re-protecting.
One test checks that the memory-limit case stays protected.
Happy to rework it in whatever direction you prefer.
Activity
cmb69 commentedon Dec 1, 2022
The problem is Soap's error handling, which calls
_zend_bailout()and there:php-src/Zend/zend.c
Line 1185 in 64f5f53
The fix for the reported issue appears to be trivial:
However, there are more issues in other places (probably not only in ext/soap).
ndossche commentedon Jan 8, 2023
If no one is working on this yet, I can try to check other places too soon-ish, and write a PR.
cmb69 commentedon Jan 8, 2023
Great! I'm not planning to work on this. :)
Add regression test for phpGH-10026
likemusic commentedon Oct 14, 2026
Still reproducible on PHP 8.3.33 (bundled ext-soap), ten years after #66151 and three years after this report. Two details that may help narrow down the bailout path.
1. It is the WSDL parse path, not
SoapFaultin general.A failed WSDL parse flips
gc_status()['protected']totruepermanently. No network is needed — a malformed local file is enough:A client created without WSDL whose call fails behaves differently — same
SoapFaultclass, collector untouched:So the trigger is specifically the WSDL parsing path, not SOAP error handling as a whole.
2. Nothing in userland clears the flag.
gc_disable()+gc_enable()does not, as shown above;gc_protectedis only reset bygc_reset()at request start.Why this is worse than "GC is delayed". While the flag is set,
gc_possible_root()does not even buffer candidates, so the cycles are not collected late — they are lost for the remainder of the process.Impact in practice. In a PHPUnit suite, a single test constructing
SoapClientagainst an unreachable WSDL costs cycle collection for the whole process. Every subsequent Laravel application instance then becomes uncollectable garbage and holds its MySQL connection open; the run died onmax_connectionsabout 150 tests later, with 938 failures all reportingSQLSTATE[HY000] [1040] Too many connections. Nothing in that symptom points anywhere near SOAP, and tracing it took a full day.The mechanism @cmb69 identified in 2022 still matches the code:
zend_bailout()setsgc_protect(1)before theLONGJMP, on the assumption that the request ends there;ext/soapcatches that bailout in its ownzend_catchand resumes without restoring the flag. Sincegc_protect()returns the previous value, the save/restore idiom such a fix would need is already in the API.ndossche commentedon Oct 14, 2026
The fix is much more complicated to do correctly/safely because fatal errors etc can also cause bailouts, and they do need the gc protection.
likemusic commentedon Oct 15, 2026
@ndossche thanks, that's the crux, and it also makes #10283 as is unsafe. In
soap_real_error_handler()the client converts anyE_FATAL_ERRORSinto aSoapFault, so memory exhaustion or a timeout inside a SOAP call would also leave the GC unprotected. I confirmed this on 8.4: amemory_limitfatal in__doRequest()comes back as aSoapFaultand the script continues.I've opened #24338 with a narrower variant on top of your commits:
soap_errorN()) are marked, and only for those does the handler bail out withoutgc_protect.Happy to rework it in whatever direction you prefer.
ext/soap: report WSDL and schema errors without a bailout
ext/soap: report WSDL and schema errors without a bailout
ext/soap: report encoding errors without a bailout